2015-10-30 20:28:21 -04:00
< ? php
2025-06-30 09:04:05 -04:00
2015-10-30 20:28:21 -04:00
/**
2024-05-27 11:39:07 -04:00
* SPDX - FileCopyrightText : 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2016 ownCloud , Inc .
* SPDX - License - Identifier : AGPL - 3.0 - only
2015-10-30 20:28:21 -04:00
*/
namespace OCA\DAV\CalDAV ;
2020-01-05 15:32:33 -05:00
use DateTime ;
2024-05-15 06:55:40 -04:00
use DateTimeImmutable ;
2021-10-28 12:37:20 -04:00
use DateTimeInterface ;
2025-04-03 19:54:08 -04:00
use Generator ;
2021-03-12 05:20:04 -05:00
use OCA\DAV\AppInfo\Application ;
2025-05-14 05:21:28 -04:00
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity ;
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper ;
2024-01-30 12:35:44 -05:00
use OCA\DAV\CalDAV\Sharing\Backend ;
2016-01-26 06:06:02 -05:00
use OCA\DAV\Connector\Sabre\Principal ;
2019-11-22 14:52:10 -05:00
use OCA\DAV\DAV\Sharing\IShareable ;
2020-07-28 03:35:51 -04:00
use OCA\DAV\Events\CachedCalendarObjectCreatedEvent ;
use OCA\DAV\Events\CachedCalendarObjectDeletedEvent ;
use OCA\DAV\Events\CachedCalendarObjectUpdatedEvent ;
use OCA\DAV\Events\CalendarCreatedEvent ;
use OCA\DAV\Events\CalendarDeletedEvent ;
2021-03-12 05:20:04 -05:00
use OCA\DAV\Events\CalendarMovedToTrashEvent ;
2020-07-28 03:35:51 -04:00
use OCA\DAV\Events\CalendarPublishedEvent ;
2021-03-12 05:20:04 -05:00
use OCA\DAV\Events\CalendarRestoredEvent ;
2020-07-28 03:35:51 -04:00
use OCA\DAV\Events\CalendarShareUpdatedEvent ;
use OCA\DAV\Events\CalendarUnpublishedEvent ;
use OCA\DAV\Events\CalendarUpdatedEvent ;
use OCA\DAV\Events\SubscriptionCreatedEvent ;
use OCA\DAV\Events\SubscriptionDeletedEvent ;
use OCA\DAV\Events\SubscriptionUpdatedEvent ;
2022-10-03 14:03:29 -04:00
use OCP\AppFramework\Db\TTransactional ;
2025-04-03 19:54:08 -04:00
use OCP\Calendar\CalendarExportOptions ;
2025-03-06 13:07:50 -05:00
use OCP\Calendar\Events\CalendarObjectCreatedEvent ;
use OCP\Calendar\Events\CalendarObjectDeletedEvent ;
use OCP\Calendar\Events\CalendarObjectMovedEvent ;
use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent ;
use OCP\Calendar\Events\CalendarObjectRestoredEvent ;
use OCP\Calendar\Events\CalendarObjectUpdatedEvent ;
2022-07-06 13:16:38 -04:00
use OCP\Calendar\Exceptions\CalendarException ;
2021-12-06 14:01:22 -05:00
use OCP\DB\Exception ;
2019-11-22 14:52:10 -05:00
use OCP\DB\QueryBuilder\IQueryBuilder ;
2020-07-28 03:35:51 -04:00
use OCP\EventDispatcher\IEventDispatcher ;
2025-08-15 07:54:56 -04:00
use OCP\ICache ;
use OCP\ICacheFactory ;
2021-03-12 05:20:04 -05:00
use OCP\IConfig ;
2016-01-25 11:18:47 -05:00
use OCP\IDBConnection ;
2016-08-18 09:10:18 -04:00
use OCP\IUserManager ;
2016-09-03 04:52:05 -04:00
use OCP\Security\ISecureRandom ;
2022-03-31 09:34:57 -04:00
use Psr\Log\LoggerInterface ;
2021-03-12 05:20:04 -05:00
use RuntimeException ;
2015-10-30 20:28:21 -04:00
use Sabre\CalDAV\Backend\AbstractBackend ;
use Sabre\CalDAV\Backend\SchedulingSupport ;
use Sabre\CalDAV\Backend\SubscriptionSupport ;
use Sabre\CalDAV\Backend\SyncSupport ;
2015-11-20 07:35:23 -05:00
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp ;
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet ;
2015-10-30 20:28:21 -04:00
use Sabre\DAV ;
2021-03-12 05:20:04 -05:00
use Sabre\DAV\Exception\BadRequest ;
2015-10-30 20:28:21 -04:00
use Sabre\DAV\Exception\Forbidden ;
2016-08-01 09:07:22 -04:00
use Sabre\DAV\Exception\NotFound ;
2016-04-19 05:33:37 -04:00
use Sabre\DAV\PropPatch ;
2019-11-22 14:52:10 -05:00
use Sabre\Uri ;
2017-11-06 19:31:28 -05:00
use Sabre\VObject\Component ;
2017-03-25 06:56:40 -04:00
use Sabre\VObject\Component\VCalendar ;
2017-11-06 19:31:28 -05:00
use Sabre\VObject\Component\VTimeZone ;
2015-10-30 20:28:21 -04:00
use Sabre\VObject\DateTimeParser ;
2017-10-22 06:16:58 -04:00
use Sabre\VObject\InvalidDataException ;
use Sabre\VObject\ParseException ;
2017-11-06 19:31:28 -05:00
use Sabre\VObject\Property ;
2015-10-30 20:28:21 -04:00
use Sabre\VObject\Reader ;
2016-04-19 05:33:37 -04:00
use Sabre\VObject\Recur\EventIterator ;
2024-09-12 09:32:44 -04:00
use Sabre\VObject\Recur\MaxInstancesExceededException ;
2024-07-17 15:26:05 -04:00
use Sabre\VObject\Recur\NoInstancesException ;
2021-12-29 09:26:49 -05:00
use function array_column ;
2024-03-11 11:27:23 -04:00
use function array_map ;
2021-03-12 05:20:04 -05:00
use function array_merge ;
use function array_values ;
use function explode ;
use function is_array ;
2021-12-02 09:06:05 -05:00
use function is_resource ;
2021-03-12 05:20:04 -05:00
use function pathinfo ;
2021-12-02 09:06:05 -05:00
use function rewind ;
2021-12-29 08:18:45 -05:00
use function settype ;
2021-03-12 05:20:04 -05:00
use function sprintf ;
use function str_replace ;
use function strtolower ;
use function time ;
2015-10-30 20:28:21 -04:00
/**
* Class CalDavBackend
*
* Code is heavily inspired by https :// github . com / fruux / sabre - dav / blob / master / lib / CalDAV / Backend / PDO . php
*
* @ package OCA\DAV\CalDAV
2025-04-07 08:59:20 -04:00
*
* @ psalm - type CalendarInfo = array {
* id : int ,
* uri : string ,
* principaluri : string ,
* '{http://calendarserver.org/ns/}getctag' : string ,
* '{http://sabredav.org/ns}sync-token' : int ,
* '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' : \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet ,
* '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp' : \Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp ,
* '{DAV:}displayname' : string ,
* '{urn:ietf:params:xml:ns:caldav}calendar-timezone' : ? string ,
* '{http://nextcloud.com/ns}owner-displayname' : string ,
* }
2015-10-30 20:28:21 -04:00
*/
class CalDavBackend extends AbstractBackend implements SyncSupport , SubscriptionSupport , SchedulingSupport {
2022-10-03 14:03:29 -04:00
use TTransactional ;
2020-04-10 10:54:27 -04:00
public const CALENDAR_TYPE_CALENDAR = 0 ;
public const CALENDAR_TYPE_SUBSCRIPTION = 1 ;
2025-05-14 05:21:28 -04:00
public const CALENDAR_TYPE_FEDERATED = 2 ;
2018-06-28 07:07:33 -04:00
2020-04-10 10:54:27 -04:00
public const PERSONAL_CALENDAR_URI = 'personal' ;
public const PERSONAL_CALENDAR_NAME = 'Personal' ;
2016-09-20 08:09:08 -04:00
2020-04-10 10:54:27 -04:00
public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar' ;
public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar' ;
2018-05-28 14:12:13 -04:00
2015-10-30 20:28:21 -04:00
/**
* We need to specify a max date , because we need to stop * somewhere *
*
* On 32 bit system the maximum for a signed integer is 2147483647 , so
* MAX_DATE cannot be higher than date ( 'Y-m-d' , 2147483647 ) which results
* in 2038 - 01 - 19 to avoid problems when the date is converted
* to a unix timestamp .
*/
2020-04-10 10:54:27 -04:00
public const MAX_DATE = '2038-01-01' ;
2015-10-30 20:28:21 -04:00
2020-04-10 10:54:27 -04:00
public const ACCESS_PUBLIC = 4 ;
public const CLASSIFICATION_PUBLIC = 0 ;
public const CLASSIFICATION_PRIVATE = 1 ;
public const CLASSIFICATION_CONFIDENTIAL = 2 ;
2016-04-19 05:33:37 -04:00
2015-10-30 20:28:21 -04:00
/**
2021-12-29 08:18:45 -05:00
* List of CalDAV properties , and how they map to database field names and their type
2015-10-30 20:28:21 -04:00
* Add your own properties by simply adding on to this array .
*
* @ var array
2021-12-29 08:18:45 -05:00
* @ psalm - var array < string , string [] >
2015-10-30 20:28:21 -04:00
*/
2022-05-12 11:32:30 -04:00
public array $propertyMap = [
2021-12-29 08:18:45 -05:00
'{DAV:}displayname' => [ 'displayname' , 'string' ],
'{urn:ietf:params:xml:ns:caldav}calendar-description' => [ 'description' , 'string' ],
'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => [ 'timezone' , 'string' ],
2021-12-29 09:10:38 -05:00
'{http://apple.com/ns/ical/}calendar-order' => [ 'calendarorder' , 'int' ],
2021-12-29 08:18:45 -05:00
'{http://apple.com/ns/ical/}calendar-color' => [ 'calendarcolor' , 'string' ],
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_NEXTCLOUD . '}deleted-at' => [ 'deleted_at' , 'int' ],
2015-10-30 20:28:21 -04:00
];
/**
2016-01-25 11:18:47 -05:00
* List of subscription properties , and how they map to database field names .
2015-10-30 20:28:21 -04:00
*
* @ var array
*/
2022-05-12 11:32:30 -04:00
public array $subscriptionPropertyMap = [
2021-12-29 09:26:49 -05:00
'{DAV:}displayname' => [ 'displayname' , 'string' ],
'{http://apple.com/ns/ical/}refreshrate' => [ 'refreshrate' , 'string' ],
'{http://apple.com/ns/ical/}calendar-order' => [ 'calendarorder' , 'int' ],
'{http://apple.com/ns/ical/}calendar-color' => [ 'calendarcolor' , 'string' ],
'{http://calendarserver.org/ns/}subscribed-strip-todos' => [ 'striptodos' , 'bool' ],
'{http://calendarserver.org/ns/}subscribed-strip-alarms' => [ 'stripalarms' , 'string' ],
'{http://calendarserver.org/ns/}subscribed-strip-attachments' => [ 'stripattachments' , 'string' ],
2015-10-30 20:28:21 -04:00
];
2021-11-11 04:57:04 -05:00
/**
* properties to index
*
* This list has to be kept in sync with ICalendarQuery :: SEARCH_PROPERTY_ *
*
* @ see \OCP\Calendar\ICalendarQuery
*/
private const INDEXED_PROPERTIES = [
'CATEGORIES' ,
'COMMENT' ,
'DESCRIPTION' ,
'LOCATION' ,
'RESOURCES' ,
'STATUS' ,
'SUMMARY' ,
'ATTENDEE' ,
'CONTACT' ,
'ORGANIZER'
];
2017-03-25 06:56:40 -04:00
/** @var array parameters to index */
2022-05-12 11:32:30 -04:00
public static array $indexParameters = [
2017-03-25 06:56:40 -04:00
'ATTENDEE' => [ 'CN' ],
'ORGANIZER' => [ 'CN' ],
];
2016-08-18 09:10:18 -04:00
/**
* @ var string [] Map of uid => display name
*/
2022-05-12 11:32:30 -04:00
protected array $userDisplayNames ;
2016-09-03 04:52:05 -04:00
2024-09-08 19:33:16 -04:00
private string $dbObjectsTable = 'calendarobjects' ;
2022-05-12 11:32:30 -04:00
private string $dbObjectPropertiesTable = 'calendarobjects_props' ;
2024-09-08 19:33:16 -04:00
private string $dbObjectInvitationsTable = 'calendar_invitations' ;
2023-07-25 12:09:11 -04:00
private array $cachedObjects = [];
2017-10-22 06:16:58 -04:00
2025-08-15 07:54:56 -04:00
private readonly ICache $publishStatusCache ;
2023-09-26 05:07:49 -04:00
public function __construct (
private IDBConnection $db ,
private Principal $principalBackend ,
private IUserManager $userManager ,
private ISecureRandom $random ,
private LoggerInterface $logger ,
private IEventDispatcher $dispatcher ,
private IConfig $config ,
2024-01-30 12:35:44 -05:00
private Sharing\Backend $calendarSharingBackend ,
2025-05-14 05:21:28 -04:00
private FederatedCalendarMapper $federatedCalendarMapper ,
2025-08-15 07:54:56 -04:00
ICacheFactory $cacheFactory ,
2023-09-26 05:07:49 -04:00
private bool $legacyEndpoint = false ,
) {
2025-08-15 07:54:56 -04:00
$this -> publishStatusCache = $cacheFactory -> createInMemory ();
2015-10-30 20:28:21 -04:00
}
2016-08-30 09:11:33 -04:00
/**
2025-06-04 09:05:38 -04:00
* Return the number of calendars owned by the given principal .
2016-08-30 09:11:33 -04:00
*
2025-06-04 09:05:38 -04:00
* Calendars shared with the given principal are not counted !
2016-08-30 09:11:33 -04:00
*
2025-06-04 09:05:38 -04:00
* By default , this excludes the automatically generated birthday calendar .
2016-08-30 09:11:33 -04:00
*/
2025-06-04 09:05:38 -04:00
public function getCalendarsForUserCount ( string $principalUri , bool $excludeBirthday = true ) : int {
2016-08-30 09:11:33 -04:00
$principalUri = $this -> convertPrincipal ( $principalUri , true );
$query = $this -> db -> getQueryBuilder ();
2018-10-19 10:44:28 -04:00
$query -> select ( $query -> func () -> count ( '*' ))
2020-11-09 04:40:55 -05:00
-> from ( 'calendars' );
if ( $principalUri === '' ) {
$query -> where ( $query -> expr () -> emptyString ( 'principaluri' ));
} else {
$query -> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )));
}
2016-08-30 09:11:33 -04:00
if ( $excludeBirthday ) {
$query -> andWhere ( $query -> expr () -> neq ( 'uri' , $query -> createNamedParameter ( BirthdayService :: BIRTHDAY_CALENDAR_URI )));
}
2021-04-23 05:43:16 -04:00
$result = $query -> executeQuery ();
2021-01-03 09:28:31 -05:00
$column = ( int ) $result -> fetchOne ();
2020-11-05 04:50:53 -05:00
$result -> closeCursor ();
return $column ;
2016-08-30 09:11:33 -04:00
}
2023-08-11 10:36:10 -04:00
/**
* Return the number of subscriptions for a principal
*/
public function getSubscriptionsForUserCount ( string $principalUri ) : int {
$principalUri = $this -> convertPrincipal ( $principalUri , true );
$query = $this -> db -> getQueryBuilder ();
$query -> select ( $query -> func () -> count ( '*' ))
-> from ( 'calendarsubscriptions' );
if ( $principalUri === '' ) {
$query -> where ( $query -> expr () -> emptyString ( 'principaluri' ));
} else {
$query -> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )));
}
$result = $query -> executeQuery ();
$column = ( int ) $result -> fetchOne ();
$result -> closeCursor ();
return $column ;
}
2021-03-12 05:20:04 -05:00
/**
* @ return array { id : int , deleted_at : int }[]
*/
public function getDeletedCalendars ( int $deletedBefore ) : array {
$qb = $this -> db -> getQueryBuilder ();
$qb -> select ([ 'id' , 'deleted_at' ])
-> from ( 'calendars' )
-> where ( $qb -> expr () -> isNotNull ( 'deleted_at' ))
-> andWhere ( $qb -> expr () -> lt ( 'deleted_at' , $qb -> createNamedParameter ( $deletedBefore )));
$result = $qb -> executeQuery ();
2023-09-27 12:36:45 -04:00
$calendars = [];
while (( $row = $result -> fetch ()) !== false ) {
$calendars [] = [
2021-03-12 05:20:04 -05:00
'id' => ( int ) $row [ 'id' ],
'deleted_at' => ( int ) $row [ 'deleted_at' ],
];
2023-09-27 12:36:45 -04:00
}
$result -> closeCursor ();
return $calendars ;
2021-03-12 05:20:04 -05:00
}
2015-10-30 20:28:21 -04:00
/**
* Returns a list of calendars for a principal .
*
* Every project is an array with the following keys :
* * id , a unique id that will be used by other functions to modify the
* calendar . This can be the same as the uri or a database key .
* * uri , which the basename of the uri with which the calendar is
* accessed .
* * principaluri . The owner of the calendar . Almost always the same as
* principalUri passed to this method .
*
* Furthermore it can contain webdav properties in clark notation . A very
* common one is '{DAV:}displayname' .
*
* Many clients also require :
* { urn : ietf : params : xml : ns : caldav } supported - calendar - component - set
* For this property , you can just return an instance of
* Sabre\CalDAV\Property\SupportedCalendarComponentSet .
*
* If you return { http :// sabredav . org / ns } read - only and set the value to 1 ,
* ACL will automatically be put in read - only mode .
*
* @ param string $principalUri
* @ return array
*/
2020-04-10 10:51:06 -04:00
public function getCalendarsForUser ( $principalUri ) {
2023-02-10 06:17:27 -05:00
return $this -> atomic ( function () use ( $principalUri ) {
$principalUriOriginal = $principalUri ;
$principalUri = $this -> convertPrincipal ( $principalUri , true );
$fields = array_column ( $this -> propertyMap , 0 );
$fields [] = 'id' ;
$fields [] = 'uri' ;
$fields [] = 'synctoken' ;
$fields [] = 'components' ;
$fields [] = 'principaluri' ;
$fields [] = 'transparent' ;
// Making fields a comma-delimited list
$query = $this -> db -> getQueryBuilder ();
$query -> select ( $fields )
-> from ( 'calendars' )
-> orderBy ( 'calendarorder' , 'ASC' );
2020-11-09 04:40:55 -05:00
2023-02-10 06:17:27 -05:00
if ( $principalUri === '' ) {
$query -> where ( $query -> expr () -> emptyString ( 'principaluri' ));
} else {
$query -> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )));
}
2020-11-09 04:40:55 -05:00
2023-02-10 06:17:27 -05:00
$result = $query -> executeQuery ();
2015-10-30 20:28:21 -04:00
2023-02-10 06:17:27 -05:00
$calendars = [];
while ( $row = $result -> fetch ()) {
$row [ 'principaluri' ] = ( string ) $row [ 'principaluri' ];
$components = [];
if ( $row [ 'components' ]) {
$components = explode ( ',' , $row [ 'components' ]);
}
2015-10-30 20:28:21 -04:00
2023-02-10 06:17:27 -05:00
$calendar = [
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
'principaluri' => $this -> convertPrincipal ( $row [ 'principaluri' ], ! $this -> legacyEndpoint ),
2024-09-15 15:01:34 -04:00
'{' . Plugin :: NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ( $row [ 'synctoken' ] ? : '0' ),
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2023-02-10 06:17:27 -05:00
'{' . Plugin :: NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet ( $components ),
'{' . Plugin :: NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp ( $row [ 'transparent' ] ? 'transparent' : 'opaque' ),
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}owner-principal' => $this -> convertPrincipal ( $principalUri , ! $this -> legacyEndpoint ),
];
2015-10-30 20:28:21 -04:00
2023-02-10 06:17:27 -05:00
$calendar = $this -> rowToCalendar ( $row , $calendar );
$calendar = $this -> addOwnerPrincipalToCalendar ( $calendar );
$calendar = $this -> addResourceTypeToCalendar ( $row , $calendar );
2017-04-19 10:18:44 -04:00
2023-02-10 06:17:27 -05:00
if ( ! isset ( $calendars [ $calendar [ 'id' ]])) {
$calendars [ $calendar [ 'id' ]] = $calendar ;
}
2016-02-05 11:25:07 -05:00
}
2023-02-10 06:17:27 -05:00
$result -> closeCursor ();
2016-01-26 06:06:02 -05:00
2023-02-10 06:17:27 -05:00
// query for shared calendars
$principals = $this -> principalBackend -> getGroupMembership ( $principalUriOriginal , true );
$principals = array_merge ( $principals , $this -> principalBackend -> getCircleMembership ( $principalUriOriginal ));
$principals [] = $principalUri ;
2016-01-26 06:06:02 -05:00
2023-02-10 06:17:27 -05:00
$fields = array_column ( $this -> propertyMap , 0 );
2024-01-30 12:35:44 -05:00
$fields = array_map ( function ( string $field ) {
return 'a.' . $field ;
}, $fields );
2023-02-10 06:17:27 -05:00
$fields [] = 'a.id' ;
$fields [] = 'a.uri' ;
$fields [] = 'a.synctoken' ;
$fields [] = 'a.components' ;
$fields [] = 'a.principaluri' ;
$fields [] = 'a.transparent' ;
$fields [] = 's.access' ;
2024-01-30 12:35:44 -05:00
$select = $this -> db -> getQueryBuilder ();
$subSelect = $this -> db -> getQueryBuilder ();
$subSelect -> select ( 'resourceid' )
-> from ( 'dav_shares' , 'd' )
-> where ( $subSelect -> expr () -> eq ( 'd.access' , $select -> createNamedParameter ( Backend :: ACCESS_UNSHARED , IQueryBuilder :: PARAM_INT ), IQueryBuilder :: PARAM_INT ))
2025-07-08 05:54:35 -04:00
-> andWhere ( $subSelect -> expr () -> in ( 'd.principaluri' , $select -> createNamedParameter ( $principals , IQueryBuilder :: PARAM_STR_ARRAY ), IQueryBuilder :: PARAM_STR_ARRAY ));
2024-01-30 12:35:44 -05:00
$select -> select ( $fields )
2023-02-10 06:17:27 -05:00
-> from ( 'dav_shares' , 's' )
2024-01-30 12:35:44 -05:00
-> join ( 's' , 'calendars' , 'a' , $select -> expr () -> eq ( 's.resourceid' , 'a.id' , IQueryBuilder :: PARAM_INT ))
-> where ( $select -> expr () -> in ( 's.principaluri' , $select -> createNamedParameter ( $principals , IQueryBuilder :: PARAM_STR_ARRAY ), IQueryBuilder :: PARAM_STR_ARRAY ))
-> andWhere ( $select -> expr () -> eq ( 's.type' , $select -> createNamedParameter ( 'calendar' , IQueryBuilder :: PARAM_STR ), IQueryBuilder :: PARAM_STR ))
-> andWhere ( $select -> expr () -> notIn ( 'a.id' , $select -> createFunction ( $subSelect -> getSQL ()), IQueryBuilder :: PARAM_INT_ARRAY ));
2016-01-26 06:06:02 -05:00
2024-01-30 12:35:44 -05:00
$results = $select -> executeQuery ();
2017-03-25 18:07:09 -04:00
2023-02-10 06:17:27 -05:00
$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}read-only' ;
2024-01-30 12:35:44 -05:00
while ( $row = $results -> fetch ()) {
2023-02-10 06:17:27 -05:00
$row [ 'principaluri' ] = ( string ) $row [ 'principaluri' ];
if ( $row [ 'principaluri' ] === $principalUri ) {
2017-03-02 05:34:27 -05:00
continue ;
}
2023-02-10 06:17:27 -05:00
$readOnly = ( int ) $row [ 'access' ] === Backend :: ACCESS_READ ;
if ( isset ( $calendars [ $row [ 'id' ]])) {
if ( $readOnly ) {
2024-01-30 12:35:44 -05:00
// New share can not have more permissions than the old one.
2023-02-10 06:17:27 -05:00
continue ;
}
if ( isset ( $calendars [ $row [ 'id' ]][ $readOnlyPropertyName ])
&& $calendars [ $row [ 'id' ]][ $readOnlyPropertyName ] === 0 ) {
// Old share is already read-write, no more permissions can be gained
continue ;
}
2017-03-02 05:34:27 -05:00
}
2017-03-02 05:24:56 -05:00
2023-02-10 06:17:27 -05:00
[, $name ] = Uri\split ( $row [ 'principaluri' ]);
$uri = $row [ 'uri' ] . '_shared_by_' . $name ;
$row [ 'displayname' ] = $row [ 'displayname' ] . ' (' . ( $this -> userManager -> getDisplayName ( $name ) ? ? ( $name ? ? '' )) . ')' ;
$components = [];
if ( $row [ 'components' ]) {
$components = explode ( ',' , $row [ 'components' ]);
}
$calendar = [
'id' => $row [ 'id' ],
'uri' => $uri ,
'principaluri' => $this -> convertPrincipal ( $principalUri , ! $this -> legacyEndpoint ),
2024-09-15 15:01:34 -04:00
'{' . Plugin :: NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ( $row [ 'synctoken' ] ? : '0' ),
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2023-02-10 06:17:27 -05:00
'{' . Plugin :: NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet ( $components ),
'{' . Plugin :: NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp ( 'transparent' ),
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}owner-principal' => $this -> convertPrincipal ( $row [ 'principaluri' ], ! $this -> legacyEndpoint ),
$readOnlyPropertyName => $readOnly ,
];
2016-01-26 06:06:02 -05:00
2023-02-10 06:17:27 -05:00
$calendar = $this -> rowToCalendar ( $row , $calendar );
$calendar = $this -> addOwnerPrincipalToCalendar ( $calendar );
$calendar = $this -> addResourceTypeToCalendar ( $row , $calendar );
2017-04-19 10:18:44 -04:00
2023-02-10 06:17:27 -05:00
$calendars [ $calendar [ 'id' ]] = $calendar ;
}
$result -> closeCursor ();
2016-01-26 06:06:02 -05:00
2023-02-10 06:17:27 -05:00
return array_values ( $calendars );
}, $this -> db );
2015-10-30 20:28:21 -04:00
}
2018-06-28 07:07:33 -04:00
/**
* @ param $principalUri
* @ return array
*/
2016-09-25 07:35:53 -04:00
public function getUsersOwnCalendars ( $principalUri ) {
$principalUri = $this -> convertPrincipal ( $principalUri , true );
2021-12-29 08:18:45 -05:00
$fields = array_column ( $this -> propertyMap , 0 );
2016-09-25 07:35:53 -04:00
$fields [] = 'id' ;
$fields [] = 'uri' ;
$fields [] = 'synctoken' ;
$fields [] = 'components' ;
$fields [] = 'principaluri' ;
$fields [] = 'transparent' ;
// Making fields a comma-delimited list
$query = $this -> db -> getQueryBuilder ();
$query -> select ( $fields ) -> from ( 'calendars' )
-> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )))
-> orderBy ( 'calendarorder' , 'ASC' );
2021-04-23 05:43:16 -04:00
$stmt = $query -> executeQuery ();
2016-09-25 07:35:53 -04:00
$calendars = [];
2021-04-23 05:37:22 -04:00
while ( $row = $stmt -> fetch ()) {
2020-11-09 04:40:55 -05:00
$row [ 'principaluri' ] = ( string ) $row [ 'principaluri' ];
2016-09-25 07:35:53 -04:00
$components = [];
if ( $row [ 'components' ]) {
2023-01-23 12:23:52 -05:00
$components = explode ( ',' , $row [ 'components' ]);
2016-09-25 07:35:53 -04:00
}
$calendar = [
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
2016-12-15 10:59:46 -05:00
'principaluri' => $this -> convertPrincipal ( $row [ 'principaluri' ], ! $this -> legacyEndpoint ),
2024-09-15 15:01:34 -04:00
'{' . Plugin :: NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ( $row [ 'synctoken' ] ? : '0' ),
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2016-09-25 07:35:53 -04:00
'{' . Plugin :: NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet ( $components ),
'{' . Plugin :: NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp ( $row [ 'transparent' ] ? 'transparent' : 'opaque' ),
];
2017-04-19 10:18:44 -04:00
2021-12-29 08:18:45 -05:00
$calendar = $this -> rowToCalendar ( $row , $calendar );
2021-03-12 05:20:04 -05:00
$calendar = $this -> addOwnerPrincipalToCalendar ( $calendar );
$calendar = $this -> addResourceTypeToCalendar ( $row , $calendar );
2017-04-19 10:18:44 -04:00
2016-09-25 07:35:53 -04:00
if ( ! isset ( $calendars [ $calendar [ 'id' ]])) {
$calendars [ $calendar [ 'id' ]] = $calendar ;
}
}
$stmt -> closeCursor ();
return array_values ( $calendars );
}
2016-08-01 09:07:22 -04:00
/**
* @ return array
*/
2016-07-08 10:13:34 -04:00
public function getPublicCalendars () {
2021-12-29 08:18:45 -05:00
$fields = array_column ( $this -> propertyMap , 0 );
2016-07-08 10:13:34 -04:00
$fields [] = 'a.id' ;
$fields [] = 'a.uri' ;
$fields [] = 'a.synctoken' ;
$fields [] = 'a.components' ;
$fields [] = 'a.principaluri' ;
$fields [] = 'a.transparent' ;
$fields [] = 's.access' ;
2016-08-01 10:44:28 -04:00
$fields [] = 's.publicuri' ;
2016-07-08 10:13:34 -04:00
$calendars = [];
$query = $this -> db -> getQueryBuilder ();
$result = $query -> select ( $fields )
-> from ( 'dav_shares' , 's' )
-> join ( 's' , 'calendars' , 'a' , $query -> expr () -> eq ( 's.resourceid' , 'a.id' ))
-> where ( $query -> expr () -> in ( 's.access' , $query -> createNamedParameter ( self :: ACCESS_PUBLIC )))
-> andWhere ( $query -> expr () -> eq ( 's.type' , $query -> createNamedParameter ( 'calendar' )))
2021-04-23 05:43:16 -04:00
-> executeQuery ();
2016-07-08 10:13:34 -04:00
while ( $row = $result -> fetch ()) {
2020-11-09 04:40:55 -05:00
$row [ 'principaluri' ] = ( string ) $row [ 'principaluri' ];
2021-01-12 04:15:48 -05:00
[, $name ] = Uri\split ( $row [ 'principaluri' ]);
2016-07-08 10:13:34 -04:00
$row [ 'displayname' ] = $row [ 'displayname' ] . " ( $name ) " ;
$components = [];
if ( $row [ 'components' ]) {
2023-01-23 12:23:52 -05:00
$components = explode ( ',' , $row [ 'components' ]);
2016-07-08 10:13:34 -04:00
}
$calendar = [
'id' => $row [ 'id' ],
2016-08-01 10:44:28 -04:00
'uri' => $row [ 'publicuri' ],
2016-12-15 10:59:46 -05:00
'principaluri' => $this -> convertPrincipal ( $row [ 'principaluri' ], ! $this -> legacyEndpoint ),
2024-09-15 15:01:34 -04:00
'{' . Plugin :: NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ( $row [ 'synctoken' ] ? : '0' ),
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2016-07-08 10:13:34 -04:00
'{' . Plugin :: NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet ( $components ),
'{' . Plugin :: NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp ( $row [ 'transparent' ] ? 'transparent' : 'opaque' ),
2016-12-15 10:59:46 -05:00
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}owner-principal' => $this -> convertPrincipal ( $row [ 'principaluri' ], $this -> legacyEndpoint ),
2025-08-20 07:37:47 -04:00
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}read-only' => true ,
2016-07-08 10:52:20 -04:00
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}public' => ( int ) $row [ 'access' ] === self :: ACCESS_PUBLIC ,
2016-07-08 10:13:34 -04:00
];
2021-12-29 08:18:45 -05:00
$calendar = $this -> rowToCalendar ( $row , $calendar );
2021-03-12 05:20:04 -05:00
$calendar = $this -> addOwnerPrincipalToCalendar ( $calendar );
$calendar = $this -> addResourceTypeToCalendar ( $row , $calendar );
2017-04-19 10:18:44 -04:00
2016-07-08 10:13:34 -04:00
if ( ! isset ( $calendars [ $calendar [ 'id' ]])) {
$calendars [ $calendar [ 'id' ]] = $calendar ;
}
}
$result -> closeCursor ();
return array_values ( $calendars );
2016-08-01 09:07:22 -04:00
}
/**
* @ param string $uri
2016-08-01 10:44:28 -04:00
* @ return array
2016-08-01 09:07:22 -04:00
* @ throws NotFound
*/
public function getPublicCalendar ( $uri ) {
2021-12-29 08:18:45 -05:00
$fields = array_column ( $this -> propertyMap , 0 );
2016-08-01 10:44:28 -04:00
$fields [] = 'a.id' ;
$fields [] = 'a.uri' ;
$fields [] = 'a.synctoken' ;
$fields [] = 'a.components' ;
$fields [] = 'a.principaluri' ;
$fields [] = 'a.transparent' ;
$fields [] = 's.access' ;
$fields [] = 's.publicuri' ;
$query = $this -> db -> getQueryBuilder ();
$result = $query -> select ( $fields )
-> from ( 'dav_shares' , 's' )
-> join ( 's' , 'calendars' , 'a' , $query -> expr () -> eq ( 's.resourceid' , 'a.id' ))
-> where ( $query -> expr () -> in ( 's.access' , $query -> createNamedParameter ( self :: ACCESS_PUBLIC )))
-> andWhere ( $query -> expr () -> eq ( 's.type' , $query -> createNamedParameter ( 'calendar' )))
-> andWhere ( $query -> expr () -> eq ( 's.publicuri' , $query -> createNamedParameter ( $uri )))
2021-04-23 05:43:16 -04:00
-> executeQuery ();
2016-08-01 10:44:28 -04:00
2021-04-23 05:37:22 -04:00
$row = $result -> fetch ();
2016-08-01 10:44:28 -04:00
$result -> closeCursor ();
if ( $row === false ) {
throw new NotFound ( 'Node with name \'' . $uri . '\' could not be found' );
2016-08-01 09:07:22 -04:00
}
2016-08-01 10:44:28 -04:00
2020-11-09 04:40:55 -05:00
$row [ 'principaluri' ] = ( string ) $row [ 'principaluri' ];
2021-01-12 04:15:48 -05:00
[, $name ] = Uri\split ( $row [ 'principaluri' ]);
2016-08-11 10:55:57 -04:00
$row [ 'displayname' ] = $row [ 'displayname' ] . ' ' . " ( $name ) " ;
2016-08-01 10:44:28 -04:00
$components = [];
if ( $row [ 'components' ]) {
2023-01-23 12:23:52 -05:00
$components = explode ( ',' , $row [ 'components' ]);
2016-08-01 10:44:28 -04:00
}
$calendar = [
'id' => $row [ 'id' ],
2016-09-03 04:52:05 -04:00
'uri' => $row [ 'publicuri' ],
2016-12-15 10:59:46 -05:00
'principaluri' => $this -> convertPrincipal ( $row [ 'principaluri' ], ! $this -> legacyEndpoint ),
2024-09-15 15:01:34 -04:00
'{' . Plugin :: NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ( $row [ 'synctoken' ] ? : '0' ),
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2016-08-01 10:44:28 -04:00
'{' . Plugin :: NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet ( $components ),
'{' . Plugin :: NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp ( $row [ 'transparent' ] ? 'transparent' : 'opaque' ),
2016-12-15 10:59:46 -05:00
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}owner-principal' => $this -> convertPrincipal ( $row [ 'principaluri' ], ! $this -> legacyEndpoint ),
2025-08-20 07:37:47 -04:00
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}read-only' => true ,
2016-08-01 10:44:28 -04:00
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}public' => ( int ) $row [ 'access' ] === self :: ACCESS_PUBLIC ,
];
2021-12-29 08:18:45 -05:00
$calendar = $this -> rowToCalendar ( $row , $calendar );
2021-03-12 05:20:04 -05:00
$calendar = $this -> addOwnerPrincipalToCalendar ( $calendar );
$calendar = $this -> addResourceTypeToCalendar ( $row , $calendar );
2017-04-19 10:18:44 -04:00
2016-08-03 05:09:45 -04:00
return $calendar ;
2016-08-18 09:10:18 -04:00
}
2016-02-03 09:43:45 -05:00
/**
* @ param string $principal
* @ param string $uri
* @ return array | null
*/
2016-01-25 11:18:47 -05:00
public function getCalendarByUri ( $principal , $uri ) {
2021-12-29 08:18:45 -05:00
$fields = array_column ( $this -> propertyMap , 0 );
2016-01-25 11:18:47 -05:00
$fields [] = 'id' ;
$fields [] = 'uri' ;
$fields [] = 'synctoken' ;
$fields [] = 'components' ;
$fields [] = 'principaluri' ;
$fields [] = 'transparent' ;
// Making fields a comma-delimited list
$query = $this -> db -> getQueryBuilder ();
$query -> select ( $fields ) -> from ( 'calendars' )
-> where ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $uri )))
-> andWhere ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principal )))
-> setMaxResults ( 1 );
2021-04-23 05:43:16 -04:00
$stmt = $query -> executeQuery ();
2016-01-25 11:18:47 -05:00
2021-04-23 05:37:22 -04:00
$row = $stmt -> fetch ();
2016-01-25 11:18:47 -05:00
$stmt -> closeCursor ();
if ( $row === false ) {
return null ;
}
2020-11-09 04:40:55 -05:00
$row [ 'principaluri' ] = ( string ) $row [ 'principaluri' ];
2016-01-25 11:18:47 -05:00
$components = [];
if ( $row [ 'components' ]) {
2023-01-23 12:23:52 -05:00
$components = explode ( ',' , $row [ 'components' ]);
2016-01-25 11:18:47 -05:00
}
$calendar = [
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
2016-12-15 10:59:46 -05:00
'principaluri' => $this -> convertPrincipal ( $row [ 'principaluri' ], ! $this -> legacyEndpoint ),
2024-09-15 15:01:34 -04:00
'{' . Plugin :: NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ( $row [ 'synctoken' ] ? : '0' ),
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2016-01-25 11:18:47 -05:00
'{' . Plugin :: NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet ( $components ),
'{' . Plugin :: NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp ( $row [ 'transparent' ] ? 'transparent' : 'opaque' ),
];
2021-12-29 08:18:45 -05:00
$calendar = $this -> rowToCalendar ( $row , $calendar );
2021-03-12 05:20:04 -05:00
$calendar = $this -> addOwnerPrincipalToCalendar ( $calendar );
$calendar = $this -> addResourceTypeToCalendar ( $row , $calendar );
2017-04-19 10:18:44 -04:00
2016-01-25 11:18:47 -05:00
return $calendar ;
}
2018-06-28 07:07:33 -04:00
/**
2025-04-07 08:59:20 -04:00
* @ psalm - return CalendarInfo | null
* @ return array | null
2018-06-28 07:07:33 -04:00
*/
2022-06-14 09:26:15 -04:00
public function getCalendarById ( int $calendarId ) : ? array {
2021-12-29 08:18:45 -05:00
$fields = array_column ( $this -> propertyMap , 0 );
2016-01-25 11:18:47 -05:00
$fields [] = 'id' ;
$fields [] = 'uri' ;
$fields [] = 'synctoken' ;
$fields [] = 'components' ;
$fields [] = 'principaluri' ;
$fields [] = 'transparent' ;
// Making fields a comma-delimited list
$query = $this -> db -> getQueryBuilder ();
$query -> select ( $fields ) -> from ( 'calendars' )
-> where ( $query -> expr () -> eq ( 'id' , $query -> createNamedParameter ( $calendarId )))
-> setMaxResults ( 1 );
2021-04-23 05:43:16 -04:00
$stmt = $query -> executeQuery ();
2016-01-25 11:18:47 -05:00
2021-04-23 05:37:22 -04:00
$row = $stmt -> fetch ();
2016-01-25 11:18:47 -05:00
$stmt -> closeCursor ();
if ( $row === false ) {
return null ;
}
2020-11-09 04:40:55 -05:00
$row [ 'principaluri' ] = ( string ) $row [ 'principaluri' ];
2016-01-25 11:18:47 -05:00
$components = [];
if ( $row [ 'components' ]) {
2023-01-23 12:23:52 -05:00
$components = explode ( ',' , $row [ 'components' ]);
2016-01-25 11:18:47 -05:00
}
$calendar = [
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
2016-12-15 10:59:46 -05:00
'principaluri' => $this -> convertPrincipal ( $row [ 'principaluri' ], ! $this -> legacyEndpoint ),
2024-09-15 15:01:34 -04:00
'{' . Plugin :: NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ( $row [ 'synctoken' ] ? : '0' ),
2022-06-14 09:26:15 -04:00
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? ? 0 ,
2016-01-25 11:18:47 -05:00
'{' . Plugin :: NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet ( $components ),
'{' . Plugin :: NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp ( $row [ 'transparent' ] ? 'transparent' : 'opaque' ),
];
2021-12-29 08:18:45 -05:00
$calendar = $this -> rowToCalendar ( $row , $calendar );
2021-03-12 05:20:04 -05:00
$calendar = $this -> addOwnerPrincipalToCalendar ( $calendar );
$calendar = $this -> addResourceTypeToCalendar ( $row , $calendar );
2017-04-19 10:18:44 -04:00
2016-01-25 11:18:47 -05:00
return $calendar ;
}
2018-06-28 07:07:33 -04:00
/**
* @ param $subscriptionId
*/
public function getSubscriptionById ( $subscriptionId ) {
2021-12-29 09:26:49 -05:00
$fields = array_column ( $this -> subscriptionPropertyMap , 0 );
2018-06-28 07:07:33 -04:00
$fields [] = 'id' ;
$fields [] = 'uri' ;
$fields [] = 'source' ;
$fields [] = 'synctoken' ;
$fields [] = 'principaluri' ;
$fields [] = 'lastmodified' ;
$query = $this -> db -> getQueryBuilder ();
$query -> select ( $fields )
-> from ( 'calendarsubscriptions' )
-> where ( $query -> expr () -> eq ( 'id' , $query -> createNamedParameter ( $subscriptionId )))
-> orderBy ( 'calendarorder' , 'asc' );
2021-04-23 05:43:16 -04:00
$stmt = $query -> executeQuery ();
2018-06-28 07:07:33 -04:00
2021-04-23 05:37:22 -04:00
$row = $stmt -> fetch ();
2018-06-28 07:07:33 -04:00
$stmt -> closeCursor ();
if ( $row === false ) {
return null ;
}
2020-11-09 04:40:55 -05:00
$row [ 'principaluri' ] = ( string ) $row [ 'principaluri' ];
2018-06-28 07:07:33 -04:00
$subscription = [
2020-10-05 09:12:57 -04:00
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
2018-06-28 07:07:33 -04:00
'principaluri' => $row [ 'principaluri' ],
2020-10-05 09:12:57 -04:00
'source' => $row [ 'source' ],
2018-06-28 07:07:33 -04:00
'lastmodified' => $row [ 'lastmodified' ],
'{' . Plugin :: NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet ([ 'VTODO' , 'VEVENT' ]),
2024-09-15 15:01:34 -04:00
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2018-06-28 07:07:33 -04:00
];
2025-02-28 07:28:07 -05:00
return $this -> rowToSubscription ( $row , $subscription );
}
public function getSubscriptionByUri ( string $principal , string $uri ) : ? array {
$fields = array_column ( $this -> subscriptionPropertyMap , 0 );
$fields [] = 'id' ;
$fields [] = 'uri' ;
$fields [] = 'source' ;
$fields [] = 'synctoken' ;
$fields [] = 'principaluri' ;
$fields [] = 'lastmodified' ;
$query = $this -> db -> getQueryBuilder ();
$query -> select ( $fields )
-> from ( 'calendarsubscriptions' )
-> where ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $uri )))
-> andWhere ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principal )))
-> setMaxResults ( 1 );
$stmt = $query -> executeQuery ();
$row = $stmt -> fetch ();
$stmt -> closeCursor ();
if ( $row === false ) {
return null ;
}
$row [ 'principaluri' ] = ( string ) $row [ 'principaluri' ];
$subscription = [
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
'principaluri' => $row [ 'principaluri' ],
'source' => $row [ 'source' ],
'lastmodified' => $row [ 'lastmodified' ],
'{' . Plugin :: NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet ([ 'VTODO' , 'VEVENT' ]),
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
];
2021-12-29 09:26:49 -05:00
return $this -> rowToSubscription ( $row , $subscription );
2018-06-28 07:07:33 -04:00
}
2015-10-30 20:28:21 -04:00
/**
* Creates a new calendar for a principal .
*
* If the creation was a success , an id must be returned that can be used to reference
* this calendar in other methods , such as updateCalendar .
*
* @ param string $principalUri
* @ param string $calendarUri
* @ param array $properties
2016-01-25 11:18:47 -05:00
* @ return int
2022-07-06 13:16:38 -04:00
*
* @ throws CalendarException
2015-10-30 20:28:21 -04:00
*/
2020-04-10 10:51:06 -04:00
public function createCalendar ( $principalUri , $calendarUri , array $properties ) {
2022-07-06 13:16:38 -04:00
if ( strlen ( $calendarUri ) > 255 ) {
throw new CalendarException ( 'URI too long. Calendar not created' );
}
2015-10-30 20:28:21 -04:00
$values = [
2016-12-14 18:40:12 -05:00
'principaluri' => $this -> convertPrincipal ( $principalUri , true ),
2020-10-05 09:12:57 -04:00
'uri' => $calendarUri ,
'synctoken' => 1 ,
'transparent' => 0 ,
2024-06-11 08:00:14 -04:00
'components' => 'VEVENT,VTODO,VJOURNAL' ,
2020-10-05 09:12:57 -04:00
'displayname' => $calendarUri
2015-10-30 20:28:21 -04:00
];
// Default value
$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' ;
if ( isset ( $properties [ $sccs ])) {
if ( ! ( $properties [ $sccs ] instanceof SupportedCalendarComponentSet )) {
throw new DAV\Exception ( 'The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet' );
}
2023-01-23 12:23:52 -05:00
$values [ 'components' ] = implode ( ',' , $properties [ $sccs ] -> getValue ());
2020-04-10 04:35:09 -04:00
} elseif ( isset ( $properties [ 'components' ])) {
2019-07-22 10:58:54 -04:00
// Allow to provide components internally without having
// to create a SupportedCalendarComponentSet object
$values [ 'components' ] = $properties [ 'components' ];
2015-10-30 20:28:21 -04:00
}
2019-07-22 10:58:54 -04:00
2015-10-30 20:28:21 -04:00
$transp = '{' . Plugin :: NS_CALDAV . '}schedule-calendar-transp' ;
if ( isset ( $properties [ $transp ])) {
2017-03-30 11:58:12 -04:00
$values [ 'transparent' ] = ( int )( $properties [ $transp ] -> getValue () === 'transparent' );
2015-10-30 20:28:21 -04:00
}
2021-12-29 08:18:45 -05:00
foreach ( $this -> propertyMap as $xmlName => [ $dbName , $type ]) {
2015-10-30 20:28:21 -04:00
if ( isset ( $properties [ $xmlName ])) {
$values [ $dbName ] = $properties [ $xmlName ];
}
}
2023-01-23 12:23:52 -05:00
[ $calendarId , $calendarData ] = $this -> atomic ( function () use ( $values ) {
2022-10-03 14:03:29 -04:00
$query = $this -> db -> getQueryBuilder ();
$query -> insert ( 'calendars' );
foreach ( $values as $column => $value ) {
$query -> setValue ( $column , $query -> createNamedParameter ( $value ));
}
$query -> executeStatement ();
$calendarId = $query -> getLastInsertId ();
$calendarData = $this -> getCalendarById ( $calendarId );
return [ $calendarId , $calendarData ];
}, $this -> db );
2016-09-26 07:50:39 -04:00
2020-07-28 03:35:51 -04:00
$this -> dispatcher -> dispatchTyped ( new CalendarCreatedEvent (( int ) $calendarId , $calendarData ));
2016-09-26 07:50:39 -04:00
return $calendarId ;
2015-10-30 20:28:21 -04:00
}
/**
* Updates properties for a calendar .
*
* The list of mutations is stored in a Sabre\DAV\PropPatch object .
* To do the actual updates , you must tell this object which properties
* you ' re going to process with the handle () method .
*
* Calling the handle method is like telling the PropPatch object " I
* promise I can handle updating this property " .
*
* Read the PropPatch documentation for more info and examples .
*
2017-07-20 16:48:13 -04:00
* @ param mixed $calendarId
2016-04-19 05:33:37 -04:00
* @ param PropPatch $propPatch
2015-10-30 20:28:21 -04:00
* @ return void
*/
2020-04-10 10:51:06 -04:00
public function updateCalendar ( $calendarId , PropPatch $propPatch ) {
2024-02-29 03:42:51 -05:00
$supportedProperties = array_keys ( $this -> propertyMap );
$supportedProperties [] = '{' . Plugin :: NS_CALDAV . '}schedule-calendar-transp' ;
$propPatch -> handle ( $supportedProperties , function ( $mutations ) use ( $calendarId ) {
$newValues = [];
foreach ( $mutations as $propertyName => $propertyValue ) {
switch ( $propertyName ) {
case '{' . Plugin :: NS_CALDAV . '}schedule-calendar-transp' :
$fieldName = 'transparent' ;
$newValues [ $fieldName ] = ( int )( $propertyValue -> getValue () === 'transparent' );
break ;
default :
$fieldName = $this -> propertyMap [ $propertyName ][ 0 ];
$newValues [ $fieldName ] = $propertyValue ;
break ;
2015-10-30 20:28:21 -04:00
}
2024-02-29 03:42:51 -05:00
}
[ $calendarData , $shares ] = $this -> atomic ( function () use ( $calendarId , $newValues ) {
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> update ( 'calendars' );
foreach ( $newValues as $fieldName => $value ) {
$query -> set ( $fieldName , $query -> createNamedParameter ( $value ));
}
$query -> where ( $query -> expr () -> eq ( 'id' , $query -> createNamedParameter ( $calendarId )));
$query -> executeStatement ();
2015-10-30 20:28:21 -04:00
2024-03-11 11:27:23 -04:00
$this -> addChanges ( $calendarId , [ '' ], 2 );
2015-10-30 20:28:21 -04:00
2023-02-10 06:17:27 -05:00
$calendarData = $this -> getCalendarById ( $calendarId );
$shares = $this -> getShares ( $calendarId );
2024-02-29 03:42:51 -05:00
return [ $calendarData , $shares ];
}, $this -> db );
2016-09-26 07:50:39 -04:00
2024-02-29 03:42:51 -05:00
$this -> dispatcher -> dispatchTyped ( new CalendarUpdatedEvent ( $calendarId , $calendarData , $shares , $mutations ));
return true ;
});
2015-10-30 20:28:21 -04:00
}
/**
* Delete a calendar and all it ' s objects
*
* @ param mixed $calendarId
* @ return void
*/
2021-03-12 05:20:04 -05:00
public function deleteCalendar ( $calendarId , bool $forceDeletePermanently = false ) {
2025-08-15 07:54:56 -04:00
$this -> publishStatusCache -> remove (( string ) $calendarId );
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $calendarId , $forceDeletePermanently ) : void {
2023-02-10 06:17:27 -05:00
// The calendar is deleted right away if this is either enforced by the caller
// or the special contacts birthday calendar or when the preference of an empty
// retention (0 seconds) is set, which signals a disabled trashbin.
2021-03-12 05:20:04 -05:00
$calendarData = $this -> getCalendarById ( $calendarId );
2023-02-10 06:17:27 -05:00
$isBirthdayCalendar = isset ( $calendarData [ 'uri' ]) && $calendarData [ 'uri' ] === BirthdayService :: BIRTHDAY_CALENDAR_URI ;
$trashbinDisabled = $this -> config -> getAppValue ( Application :: APP_ID , RetentionService :: RETENTION_CONFIG_KEY ) === '0' ;
if ( $forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled ) {
$calendarData = $this -> getCalendarById ( $calendarId );
$shares = $this -> getShares ( $calendarId );
2021-03-12 05:20:04 -05:00
2024-09-08 19:33:16 -04:00
$this -> purgeCalendarInvitations ( $calendarId );
2023-02-10 06:17:27 -05:00
$qbDeleteCalendarObjectProps = $this -> db -> getQueryBuilder ();
$qbDeleteCalendarObjectProps -> delete ( $this -> dbObjectPropertiesTable )
-> where ( $qbDeleteCalendarObjectProps -> expr () -> eq ( 'calendarid' , $qbDeleteCalendarObjectProps -> createNamedParameter ( $calendarId )))
-> andWhere ( $qbDeleteCalendarObjectProps -> expr () -> eq ( 'calendartype' , $qbDeleteCalendarObjectProps -> createNamedParameter ( self :: CALENDAR_TYPE_CALENDAR )))
-> executeStatement ();
$qbDeleteCalendarObjects = $this -> db -> getQueryBuilder ();
$qbDeleteCalendarObjects -> delete ( 'calendarobjects' )
-> where ( $qbDeleteCalendarObjects -> expr () -> eq ( 'calendarid' , $qbDeleteCalendarObjects -> createNamedParameter ( $calendarId )))
-> andWhere ( $qbDeleteCalendarObjects -> expr () -> eq ( 'calendartype' , $qbDeleteCalendarObjects -> createNamedParameter ( self :: CALENDAR_TYPE_CALENDAR )))
-> executeStatement ();
$qbDeleteCalendarChanges = $this -> db -> getQueryBuilder ();
$qbDeleteCalendarChanges -> delete ( 'calendarchanges' )
-> where ( $qbDeleteCalendarChanges -> expr () -> eq ( 'calendarid' , $qbDeleteCalendarChanges -> createNamedParameter ( $calendarId )))
-> andWhere ( $qbDeleteCalendarChanges -> expr () -> eq ( 'calendartype' , $qbDeleteCalendarChanges -> createNamedParameter ( self :: CALENDAR_TYPE_CALENDAR )))
-> executeStatement ();
$this -> calendarSharingBackend -> deleteAllShares ( $calendarId );
$qbDeleteCalendar = $this -> db -> getQueryBuilder ();
$qbDeleteCalendar -> delete ( 'calendars' )
-> where ( $qbDeleteCalendar -> expr () -> eq ( 'id' , $qbDeleteCalendar -> createNamedParameter ( $calendarId )))
-> executeStatement ();
// Only dispatch if we actually deleted anything
if ( $calendarData ) {
$this -> dispatcher -> dispatchTyped ( new CalendarDeletedEvent ( $calendarId , $calendarData , $shares ));
}
} else {
$qbMarkCalendarDeleted = $this -> db -> getQueryBuilder ();
$qbMarkCalendarDeleted -> update ( 'calendars' )
-> set ( 'deleted_at' , $qbMarkCalendarDeleted -> createNamedParameter ( time ()))
-> where ( $qbMarkCalendarDeleted -> expr () -> eq ( 'id' , $qbMarkCalendarDeleted -> createNamedParameter ( $calendarId )))
-> executeStatement ();
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$calendarData = $this -> getCalendarById ( $calendarId );
$shares = $this -> getShares ( $calendarId );
if ( $calendarData ) {
$this -> dispatcher -> dispatchTyped ( new CalendarMovedToTrashEvent (
$calendarId ,
$calendarData ,
$shares
));
}
2021-03-12 05:20:04 -05:00
}
2023-02-10 06:17:27 -05:00
}, $this -> db );
2015-10-30 20:28:21 -04:00
}
2021-03-12 05:20:04 -05:00
public function restoreCalendar ( int $id ) : void {
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $id ) : void {
2023-02-10 06:17:27 -05:00
$qb = $this -> db -> getQueryBuilder ();
$update = $qb -> update ( 'calendars' )
-> set ( 'deleted_at' , $qb -> createNamedParameter ( null ))
-> where ( $qb -> expr () -> eq ( 'id' , $qb -> createNamedParameter ( $id , IQueryBuilder :: PARAM_INT ), IQueryBuilder :: PARAM_INT ));
$update -> executeStatement ();
$calendarData = $this -> getCalendarById ( $id );
$shares = $this -> getShares ( $id );
if ( $calendarData === null ) {
throw new RuntimeException ( 'Calendar data that was just written can\'t be read back. Check your database configuration.' );
}
$this -> dispatcher -> dispatchTyped ( new CalendarRestoredEvent (
$id ,
$calendarData ,
$shares
));
}, $this -> db );
2021-03-12 05:20:04 -05:00
}
2025-04-03 19:54:08 -04:00
/**
* Returns all calendar entries as a stream of data
*
* @ since 32.0 . 0
*
* @ return Generator < array >
*/
public function exportCalendar ( int $calendarId , int $calendarType = self :: CALENDAR_TYPE_CALENDAR , ? CalendarExportOptions $options = null ) : Generator {
// extract options
$rangeStart = $options ? -> getRangeStart ();
$rangeCount = $options ? -> getRangeCount ();
// construct query
$qb = $this -> db -> getQueryBuilder ();
$qb -> select ( '*' )
-> from ( 'calendarobjects' )
-> where ( $qb -> expr () -> eq ( 'calendarid' , $qb -> createNamedParameter ( $calendarId )))
-> andWhere ( $qb -> expr () -> eq ( 'calendartype' , $qb -> createNamedParameter ( $calendarType )))
-> andWhere ( $qb -> expr () -> isNull ( 'deleted_at' ));
if ( $rangeStart !== null ) {
$qb -> andWhere ( $qb -> expr () -> gt ( 'uid' , $qb -> createNamedParameter ( $rangeStart )));
}
if ( $rangeCount !== null ) {
$qb -> setMaxResults ( $rangeCount );
}
if ( $rangeStart !== null || $rangeCount !== null ) {
$qb -> orderBy ( 'uid' , 'ASC' );
}
$rs = $qb -> executeQuery ();
// iterate through results
try {
while (( $row = $rs -> fetch ()) !== false ) {
yield $row ;
}
} finally {
$rs -> closeCursor ();
}
}
2025-06-04 09:05:38 -04:00
2024-07-24 10:11:47 -04:00
/**
* Returns all calendar objects with limited metadata for a calendar
*
* Every item contains an array with the following keys :
* * id - the table row id
* * etag - An arbitrary string
* * uri - a unique key which will be used to construct the uri . This can
* be any arbitrary string .
* * calendardata - The iCalendar - compatible calendar data
*
* @ param mixed $calendarId
* @ param int $calendarType
* @ return array
*/
public function getLimitedCalendarObjects ( int $calendarId , int $calendarType = self :: CALENDAR_TYPE_CALENDAR ) : array {
$query = $this -> db -> getQueryBuilder ();
$query -> select ([ 'id' , 'uid' , 'etag' , 'uri' , 'calendardata' ])
-> from ( 'calendarobjects' )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $calendarId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( $calendarType )))
-> andWhere ( $query -> expr () -> isNull ( 'deleted_at' ));
$stmt = $query -> executeQuery ();
$result = [];
while (( $row = $stmt -> fetch ()) !== false ) {
$result [ $row [ 'uid' ]] = [
'id' => $row [ 'id' ],
'etag' => $row [ 'etag' ],
'uri' => $row [ 'uri' ],
'calendardata' => $row [ 'calendardata' ],
];
}
$stmt -> closeCursor ();
return $result ;
}
2016-09-23 08:30:24 -04:00
/**
* Delete all of an user ' s shares
*
* @ param string $principaluri
* @ return void
*/
2020-04-10 10:51:06 -04:00
public function deleteAllSharesByUser ( $principaluri ) {
2018-06-28 07:07:33 -04:00
$this -> calendarSharingBackend -> deleteAllSharesByUser ( $principaluri );
2016-09-23 08:30:24 -04:00
}
2015-10-30 20:28:21 -04:00
/**
* Returns all calendar objects within a calendar .
*
* Every item contains an array with the following keys :
* * calendardata - The iCalendar - compatible calendar data
* * uri - a unique key which will be used to construct the uri . This can
* be any arbitrary string , but making sure it ends with '.ics' is a
* good idea . This is only the basename , or filename , not the full
* path .
* * lastmodified - a timestamp of the last modification time
* * etag - An arbitrary string , surrounded by double - quotes . ( e . g .:
* '"abcdef"' )
* * size - The size of the calendar objects , in bytes .
* * component - optional , a string containing the type of object , such
* as 'vevent' or 'vtodo' . If specified , this will be used to populate
* the Content - Type header .
*
* Note that the etag is optional , but it ' s highly encouraged to return for
* speed reasons .
*
* The calendardata is also optional . If it ' s not returned
* 'getCalendarObject' will be called later , which * is * expected to return
* calendardata .
*
* If neither etag or size are specified , the calendardata will be
* used / fetched to determine these numbers . If both are specified the
* amount of times this is needed is reduced by a great degree .
*
2020-08-19 11:54:00 -04:00
* @ param mixed $calendarId
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2015-10-30 20:28:21 -04:00
* @ return array
*/
2020-10-05 09:12:57 -04:00
public function getCalendarObjects ( $calendarId , $calendarType = self :: CALENDAR_TYPE_CALENDAR ) : array {
2015-10-30 20:28:21 -04:00
$query = $this -> db -> getQueryBuilder ();
2016-05-31 11:16:28 -04:00
$query -> select ([ 'id' , 'uri' , 'lastmodified' , 'etag' , 'calendarid' , 'size' , 'componenttype' , 'classification' ])
2015-10-30 20:28:21 -04:00
-> from ( 'calendarobjects' )
2020-08-19 11:54:00 -04:00
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $calendarId )))
2021-03-12 05:20:04 -05:00
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( $calendarType )))
-> andWhere ( $query -> expr () -> isNull ( 'deleted_at' ));
2021-04-23 05:43:16 -04:00
$stmt = $query -> executeQuery ();
2015-10-30 20:28:21 -04:00
$result = [];
2023-09-27 12:36:45 -04:00
while (( $row = $stmt -> fetch ()) !== false ) {
2015-10-30 20:28:21 -04:00
$result [] = [
2020-10-05 09:12:57 -04:00
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
2018-06-28 07:07:33 -04:00
'lastmodified' => $row [ 'lastmodified' ],
2020-10-05 09:12:57 -04:00
'etag' => '"' . $row [ 'etag' ] . '"' ,
'calendarid' => $row [ 'calendarid' ],
'size' => ( int ) $row [ 'size' ],
'component' => strtolower ( $row [ 'componenttype' ]),
'classification' => ( int ) $row [ 'classification' ]
2015-10-30 20:28:21 -04:00
];
}
2020-11-09 04:40:55 -05:00
$stmt -> closeCursor ();
2015-10-30 20:28:21 -04:00
return $result ;
}
2021-03-12 05:20:04 -05:00
public function getDeletedCalendarObjects ( int $deletedBefore ) : array {
$query = $this -> db -> getQueryBuilder ();
$query -> select ([ 'co.id' , 'co.uri' , 'co.lastmodified' , 'co.etag' , 'co.calendarid' , 'co.calendartype' , 'co.size' , 'co.componenttype' , 'co.classification' , 'co.deleted_at' ])
-> from ( 'calendarobjects' , 'co' )
-> join ( 'co' , 'calendars' , 'c' , $query -> expr () -> eq ( 'c.id' , 'co.calendarid' , IQueryBuilder :: PARAM_INT ))
-> where ( $query -> expr () -> isNotNull ( 'co.deleted_at' ))
-> andWhere ( $query -> expr () -> lt ( 'co.deleted_at' , $query -> createNamedParameter ( $deletedBefore )));
$stmt = $query -> executeQuery ();
$result = [];
2023-09-27 12:36:45 -04:00
while (( $row = $stmt -> fetch ()) !== false ) {
2021-03-12 05:20:04 -05:00
$result [] = [
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
'lastmodified' => $row [ 'lastmodified' ],
'etag' => '"' . $row [ 'etag' ] . '"' ,
'calendarid' => ( int ) $row [ 'calendarid' ],
'calendartype' => ( int ) $row [ 'calendartype' ],
'size' => ( int ) $row [ 'size' ],
'component' => strtolower ( $row [ 'componenttype' ]),
'classification' => ( int ) $row [ 'classification' ],
2021-12-29 08:18:45 -05:00
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_NEXTCLOUD . '}deleted-at' => $row [ 'deleted_at' ] === null ? $row [ 'deleted_at' ] : ( int ) $row [ 'deleted_at' ],
2021-03-12 05:20:04 -05:00
];
}
$stmt -> closeCursor ();
return $result ;
}
2021-06-15 05:35:03 -04:00
/**
* Return all deleted calendar objects by the given principal that are not
* in deleted calendars .
*
* @ param string $principalUri
* @ return array
2021-12-06 14:01:22 -05:00
* @ throws Exception
2021-06-15 05:35:03 -04:00
*/
2021-03-12 05:20:04 -05:00
public function getDeletedCalendarObjectsByPrincipal ( string $principalUri ) : array {
$query = $this -> db -> getQueryBuilder ();
$query -> select ([ 'co.id' , 'co.uri' , 'co.lastmodified' , 'co.etag' , 'co.calendarid' , 'co.size' , 'co.componenttype' , 'co.classification' , 'co.deleted_at' ])
2021-05-31 12:57:40 -04:00
-> selectAlias ( 'c.uri' , 'calendaruri' )
2021-03-12 05:20:04 -05:00
-> from ( 'calendarobjects' , 'co' )
-> join ( 'co' , 'calendars' , 'c' , $query -> expr () -> eq ( 'c.id' , 'co.calendarid' , IQueryBuilder :: PARAM_INT ))
-> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )))
2021-06-15 05:35:03 -04:00
-> andWhere ( $query -> expr () -> isNotNull ( 'co.deleted_at' ))
-> andWhere ( $query -> expr () -> isNull ( 'c.deleted_at' ));
2021-03-12 05:20:04 -05:00
$stmt = $query -> executeQuery ();
$result = [];
while ( $row = $stmt -> fetch ()) {
$result [] = [
'id' => $row [ 'id' ],
2021-05-31 15:01:54 -04:00
'uri' => $row [ 'uri' ],
2021-03-12 05:20:04 -05:00
'lastmodified' => $row [ 'lastmodified' ],
'etag' => '"' . $row [ 'etag' ] . '"' ,
'calendarid' => $row [ 'calendarid' ],
2021-05-31 12:57:40 -04:00
'calendaruri' => $row [ 'calendaruri' ],
2021-03-12 05:20:04 -05:00
'size' => ( int ) $row [ 'size' ],
'component' => strtolower ( $row [ 'componenttype' ]),
'classification' => ( int ) $row [ 'classification' ],
2021-12-29 08:18:45 -05:00
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_NEXTCLOUD . '}deleted-at' => $row [ 'deleted_at' ] === null ? $row [ 'deleted_at' ] : ( int ) $row [ 'deleted_at' ],
2021-03-12 05:20:04 -05:00
];
}
$stmt -> closeCursor ();
return $result ;
}
2015-10-30 20:28:21 -04:00
/**
* Returns information from a single calendar object , based on it ' s object
* uri .
*
* The object uri is only the basename , or filename and not a full path .
*
* The returned array must have the same keys as getCalendarObjects . The
* 'calendardata' object is required here though , while it ' s not required
* for getCalendarObjects .
*
* This method must return null if the object did not exist .
*
2020-08-19 11:54:00 -04:00
* @ param mixed $calendarId
2015-10-30 20:28:21 -04:00
* @ param string $objectUri
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2015-10-30 20:28:21 -04:00
* @ return array | null
*/
2021-03-12 05:20:04 -05:00
public function getCalendarObject ( $calendarId , $objectUri , int $calendarType = self :: CALENDAR_TYPE_CALENDAR ) {
2023-07-25 12:09:11 -04:00
$key = $calendarId . '::' . $objectUri . '::' . $calendarType ;
if ( isset ( $this -> cachedObjects [ $key ])) {
return $this -> cachedObjects [ $key ];
}
2015-10-30 20:28:21 -04:00
$query = $this -> db -> getQueryBuilder ();
2024-09-08 19:33:16 -04:00
$query -> select ([ 'id' , 'uri' , 'uid' , 'lastmodified' , 'etag' , 'calendarid' , 'size' , 'calendardata' , 'componenttype' , 'classification' , 'deleted_at' ])
2018-06-28 07:07:33 -04:00
-> from ( 'calendarobjects' )
2020-08-19 11:54:00 -04:00
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $calendarId )))
2018-06-28 07:07:33 -04:00
-> andWhere ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $objectUri )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( $calendarType )));
2021-04-23 05:43:16 -04:00
$stmt = $query -> executeQuery ();
2021-04-23 05:37:22 -04:00
$row = $stmt -> fetch ();
2020-11-09 04:40:55 -05:00
$stmt -> closeCursor ();
2015-10-30 20:28:21 -04:00
2018-06-28 07:07:33 -04:00
if ( ! $row ) {
return null ;
}
2015-10-30 20:28:21 -04:00
2023-07-25 12:09:11 -04:00
$object = $this -> rowToCalendarObject ( $row );
$this -> cachedObjects [ $key ] = $object ;
return $object ;
}
private function rowToCalendarObject ( array $row ) : array {
2015-10-30 20:28:21 -04:00
return [
2020-10-05 09:12:57 -04:00
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
2024-09-08 19:33:16 -04:00
'uid' => $row [ 'uid' ],
2020-10-05 09:12:57 -04:00
'lastmodified' => $row [ 'lastmodified' ],
'etag' => '"' . $row [ 'etag' ] . '"' ,
'calendarid' => $row [ 'calendarid' ],
'size' => ( int ) $row [ 'size' ],
'calendardata' => $this -> readBlob ( $row [ 'calendardata' ]),
'component' => strtolower ( $row [ 'componenttype' ]),
2022-05-18 05:10:36 -04:00
'classification' => ( int ) $row [ 'classification' ],
'{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_NEXTCLOUD . '}deleted-at' => $row [ 'deleted_at' ] === null ? $row [ 'deleted_at' ] : ( int ) $row [ 'deleted_at' ],
2015-10-30 20:28:21 -04:00
];
}
/**
* Returns a list of calendar objects .
*
* This method should work identical to getCalendarObject , but instead
* return all the calendar objects in the list as an array .
*
* If the backend supports this , it may allow for some speed - ups .
*
* @ param mixed $calendarId
2015-11-20 10:42:34 -05:00
* @ param string [] $uris
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2015-10-30 20:28:21 -04:00
* @ return array
*/
2020-10-05 09:12:57 -04:00
public function getMultipleCalendarObjects ( $calendarId , array $uris , $calendarType = self :: CALENDAR_TYPE_CALENDAR ) : array {
2016-09-14 10:29:33 -04:00
if ( empty ( $uris )) {
return [];
}
$chunks = array_chunk ( $uris , 100 );
2016-09-15 03:47:39 -04:00
$objects = [];
2016-09-14 10:29:33 -04:00
2015-10-30 20:28:21 -04:00
$query = $this -> db -> getQueryBuilder ();
2016-05-31 11:16:28 -04:00
$query -> select ([ 'id' , 'uri' , 'lastmodified' , 'etag' , 'calendarid' , 'size' , 'calendardata' , 'componenttype' , 'classification' ])
2016-09-14 10:29:33 -04:00
-> from ( 'calendarobjects' )
2020-08-19 11:54:00 -04:00
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $calendarId )))
2018-06-28 07:07:33 -04:00
-> andWhere ( $query -> expr () -> in ( 'uri' , $query -> createParameter ( 'uri' )))
2021-03-12 05:20:04 -05:00
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( $calendarType )))
-> andWhere ( $query -> expr () -> isNull ( 'deleted_at' ));
2015-10-30 20:28:21 -04:00
2016-09-14 10:29:33 -04:00
foreach ( $chunks as $uris ) {
$query -> setParameter ( 'uri' , $uris , IQueryBuilder :: PARAM_STR_ARRAY );
2021-04-23 05:43:16 -04:00
$result = $query -> executeQuery ();
2015-10-30 20:28:21 -04:00
2016-09-15 03:47:39 -04:00
while ( $row = $result -> fetch ()) {
$objects [] = [
2020-10-05 09:12:57 -04:00
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
2015-10-30 20:28:21 -04:00
'lastmodified' => $row [ 'lastmodified' ],
2020-10-05 09:12:57 -04:00
'etag' => '"' . $row [ 'etag' ] . '"' ,
'calendarid' => $row [ 'calendarid' ],
'size' => ( int ) $row [ 'size' ],
2015-10-30 20:28:21 -04:00
'calendardata' => $this -> readBlob ( $row [ 'calendardata' ]),
2020-10-05 09:12:57 -04:00
'component' => strtolower ( $row [ 'componenttype' ]),
2016-05-31 12:10:31 -04:00
'classification' => ( int ) $row [ 'classification' ]
2016-09-14 10:29:33 -04:00
];
}
2016-09-15 03:47:39 -04:00
$result -> closeCursor ();
2015-10-30 20:28:21 -04:00
}
2018-06-28 07:07:33 -04:00
2016-09-15 03:47:39 -04:00
return $objects ;
2015-10-30 20:28:21 -04:00
}
/**
* Creates a new calendar object .
*
* The object uri is only the basename , or filename and not a full path .
*
* It is possible return an etag from this function , which will be used in
* the response to this PUT request . Note that the ETag must be surrounded
* by double - quotes .
*
* However , you should only really return this ETag if you don ' t mangle the
* calendar - data . If the result of a subsequent GET to this object is not
* the exact same as this request body , you should omit the ETag .
*
* @ param mixed $calendarId
* @ param string $objectUri
* @ param string $calendarData
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2015-11-20 10:42:34 -05:00
* @ return string
2015-10-30 20:28:21 -04:00
*/
2020-10-05 09:12:57 -04:00
public function createCalendarObject ( $calendarId , $objectUri , $calendarData , $calendarType = self :: CALENDAR_TYPE_CALENDAR ) {
2023-07-25 12:09:11 -04:00
$this -> cachedObjects = [];
2015-10-30 20:28:21 -04:00
$extraData = $this -> getDenormalizedData ( $calendarData );
2023-02-10 06:17:27 -05:00
return $this -> atomic ( function () use ( $calendarId , $objectUri , $calendarData , $extraData , $calendarType ) {
// Try to detect duplicates
$qb = $this -> db -> getQueryBuilder ();
$qb -> select ( $qb -> func () -> count ( '*' ))
-> from ( 'calendarobjects' )
-> where ( $qb -> expr () -> eq ( 'calendarid' , $qb -> createNamedParameter ( $calendarId )))
-> andWhere ( $qb -> expr () -> eq ( 'uid' , $qb -> createNamedParameter ( $extraData [ 'uid' ])))
-> andWhere ( $qb -> expr () -> eq ( 'calendartype' , $qb -> createNamedParameter ( $calendarType )))
-> andWhere ( $qb -> expr () -> isNull ( 'deleted_at' ));
$result = $qb -> executeQuery ();
$count = ( int ) $result -> fetchOne ();
$result -> closeCursor ();
2017-11-01 17:00:53 -04:00
2023-02-10 06:17:27 -05:00
if ( $count !== 0 ) {
throw new BadRequest ( 'Calendar object with uid already exists in this calendar collection.' );
}
// For a more specific error message we also try to explicitly look up the UID but as a deleted entry
$qbDel = $this -> db -> getQueryBuilder ();
$qbDel -> select ( '*' )
-> from ( 'calendarobjects' )
-> where ( $qbDel -> expr () -> eq ( 'calendarid' , $qbDel -> createNamedParameter ( $calendarId )))
-> andWhere ( $qbDel -> expr () -> eq ( 'uid' , $qbDel -> createNamedParameter ( $extraData [ 'uid' ])))
-> andWhere ( $qbDel -> expr () -> eq ( 'calendartype' , $qbDel -> createNamedParameter ( $calendarType )))
-> andWhere ( $qbDel -> expr () -> isNotNull ( 'deleted_at' ));
$result = $qbDel -> executeQuery ();
$found = $result -> fetch ();
$result -> closeCursor ();
if ( $found !== false ) {
// the object existed previously but has been deleted
// remove the trashbin entry and continue as if it was a new object
$this -> deleteCalendarObject ( $calendarId , $found [ 'uri' ]);
}
2017-11-01 17:00:53 -04:00
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> insert ( 'calendarobjects' )
-> values ([
'calendarid' => $query -> createNamedParameter ( $calendarId ),
'uri' => $query -> createNamedParameter ( $objectUri ),
'calendardata' => $query -> createNamedParameter ( $calendarData , IQueryBuilder :: PARAM_LOB ),
'lastmodified' => $query -> createNamedParameter ( time ()),
'etag' => $query -> createNamedParameter ( $extraData [ 'etag' ]),
'size' => $query -> createNamedParameter ( $extraData [ 'size' ]),
'componenttype' => $query -> createNamedParameter ( $extraData [ 'componentType' ]),
'firstoccurence' => $query -> createNamedParameter ( $extraData [ 'firstOccurence' ]),
'lastoccurence' => $query -> createNamedParameter ( $extraData [ 'lastOccurence' ]),
'classification' => $query -> createNamedParameter ( $extraData [ 'classification' ]),
'uid' => $query -> createNamedParameter ( $extraData [ 'uid' ]),
'calendartype' => $query -> createNamedParameter ( $calendarType ),
])
-> executeStatement ();
2015-10-30 20:28:21 -04:00
2023-02-10 06:17:27 -05:00
$this -> updateProperties ( $calendarId , $objectUri , $calendarData , $calendarType );
2024-03-11 11:27:23 -04:00
$this -> addChanges ( $calendarId , [ $objectUri ], 1 , $calendarType );
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
$objectRow = $this -> getCalendarObject ( $calendarId , $objectUri , $calendarType );
assert ( $objectRow !== null );
2022-06-14 09:26:15 -04:00
2023-02-10 06:17:27 -05:00
if ( $calendarType === self :: CALENDAR_TYPE_CALENDAR ) {
$calendarRow = $this -> getCalendarById ( $calendarId );
$shares = $this -> getShares ( $calendarId );
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$this -> dispatcher -> dispatchTyped ( new CalendarObjectCreatedEvent ( $calendarId , $calendarRow , $shares , $objectRow ));
2025-05-14 05:21:28 -04:00
} elseif ( $calendarType === self :: CALENDAR_TYPE_SUBSCRIPTION ) {
2023-02-10 06:17:27 -05:00
$subscriptionRow = $this -> getSubscriptionById ( $calendarId );
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$this -> dispatcher -> dispatchTyped ( new CachedCalendarObjectCreatedEvent ( $calendarId , $subscriptionRow , [], $objectRow ));
2025-05-14 05:21:28 -04:00
} elseif ( $calendarType === self :: CALENDAR_TYPE_FEDERATED ) {
// TODO: implement custom event for federated calendars
2023-02-10 06:17:27 -05:00
}
2015-10-30 20:28:21 -04:00
2023-02-10 06:17:27 -05:00
return '"' . $extraData [ 'etag' ] . '"' ;
}, $this -> db );
2015-10-30 20:28:21 -04:00
}
/**
* Updates an existing calendarobject , based on it ' s uri .
*
* The object uri is only the basename , or filename and not a full path .
*
* It is possible return an etag from this function , which will be used in
* the response to this PUT request . Note that the ETag must be surrounded
* by double - quotes .
*
* However , you should only really return this ETag if you don ' t mangle the
* calendar - data . If the result of a subsequent GET to this object is not
* the exact same as this request body , you should omit the ETag .
*
* @ param mixed $calendarId
* @ param string $objectUri
* @ param string $calendarData
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2015-11-20 10:42:34 -05:00
* @ return string
2015-10-30 20:28:21 -04:00
*/
2020-10-05 09:12:57 -04:00
public function updateCalendarObject ( $calendarId , $objectUri , $calendarData , $calendarType = self :: CALENDAR_TYPE_CALENDAR ) {
2023-07-25 12:09:11 -04:00
$this -> cachedObjects = [];
2015-10-30 20:28:21 -04:00
$extraData = $this -> getDenormalizedData ( $calendarData );
2023-02-10 06:17:27 -05:00
return $this -> atomic ( function () use ( $calendarId , $objectUri , $calendarData , $extraData , $calendarType ) {
$query = $this -> db -> getQueryBuilder ();
$query -> update ( 'calendarobjects' )
-> set ( 'calendardata' , $query -> createNamedParameter ( $calendarData , IQueryBuilder :: PARAM_LOB ))
-> set ( 'lastmodified' , $query -> createNamedParameter ( time ()))
-> set ( 'etag' , $query -> createNamedParameter ( $extraData [ 'etag' ]))
-> set ( 'size' , $query -> createNamedParameter ( $extraData [ 'size' ]))
-> set ( 'componenttype' , $query -> createNamedParameter ( $extraData [ 'componentType' ]))
-> set ( 'firstoccurence' , $query -> createNamedParameter ( $extraData [ 'firstOccurence' ]))
-> set ( 'lastoccurence' , $query -> createNamedParameter ( $extraData [ 'lastOccurence' ]))
-> set ( 'classification' , $query -> createNamedParameter ( $extraData [ 'classification' ]))
-> set ( 'uid' , $query -> createNamedParameter ( $extraData [ 'uid' ]))
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $calendarId )))
-> andWhere ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $objectUri )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( $calendarType )))
-> executeStatement ();
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
$this -> updateProperties ( $calendarId , $objectUri , $calendarData , $calendarType );
2024-03-11 11:27:23 -04:00
$this -> addChanges ( $calendarId , [ $objectUri ], 2 , $calendarType );
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$objectRow = $this -> getCalendarObject ( $calendarId , $objectUri , $calendarType );
if ( is_array ( $objectRow )) {
if ( $calendarType === self :: CALENDAR_TYPE_CALENDAR ) {
$calendarRow = $this -> getCalendarById ( $calendarId );
$shares = $this -> getShares ( $calendarId );
$this -> dispatcher -> dispatchTyped ( new CalendarObjectUpdatedEvent ( $calendarId , $calendarRow , $shares , $objectRow ));
2025-05-14 05:21:28 -04:00
} elseif ( $calendarType === self :: CALENDAR_TYPE_SUBSCRIPTION ) {
2023-02-10 06:17:27 -05:00
$subscriptionRow = $this -> getSubscriptionById ( $calendarId );
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$this -> dispatcher -> dispatchTyped ( new CachedCalendarObjectUpdatedEvent ( $calendarId , $subscriptionRow , [], $objectRow ));
2025-05-14 05:21:28 -04:00
} elseif ( $calendarType === self :: CALENDAR_TYPE_FEDERATED ) {
// TODO: implement custom event for federated calendars
2023-02-10 06:17:27 -05:00
}
2018-06-28 07:07:33 -04:00
}
2015-10-30 20:28:21 -04:00
2023-02-10 06:17:27 -05:00
return '"' . $extraData [ 'etag' ] . '"' ;
}, $this -> db );
2015-10-30 20:28:21 -04:00
}
2021-12-06 14:01:22 -05:00
/**
* Moves a calendar object from calendar to calendar .
*
2025-04-16 17:43:12 -04:00
* @ param string $sourcePrincipalUri
* @ param int $sourceObjectId
* @ param string $targetPrincipalUri
2021-12-06 14:01:22 -05:00
* @ param int $targetCalendarId
2025-04-16 17:43:12 -04:00
* @ param string $tragetObjectUri
2021-12-06 14:01:22 -05:00
* @ param int $calendarType
* @ return bool
* @ throws Exception
*/
2025-04-16 17:43:12 -04:00
public function moveCalendarObject ( string $sourcePrincipalUri , int $sourceObjectId , string $targetPrincipalUri , int $targetCalendarId , string $tragetObjectUri , int $calendarType = self :: CALENDAR_TYPE_CALENDAR ) : bool {
2023-07-25 12:09:11 -04:00
$this -> cachedObjects = [];
2025-04-16 17:43:12 -04:00
return $this -> atomic ( function () use ( $sourcePrincipalUri , $sourceObjectId , $targetPrincipalUri , $targetCalendarId , $tragetObjectUri , $calendarType ) {
$object = $this -> getCalendarObjectById ( $sourcePrincipalUri , $sourceObjectId );
2023-02-10 06:17:27 -05:00
if ( empty ( $object )) {
return false ;
}
2021-12-06 14:01:22 -05:00
2025-04-16 17:43:12 -04:00
$sourceCalendarId = $object [ 'calendarid' ];
$sourceObjectUri = $object [ 'uri' ];
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> update ( 'calendarobjects' )
-> set ( 'calendarid' , $query -> createNamedParameter ( $targetCalendarId , IQueryBuilder :: PARAM_INT ))
2025-04-16 17:43:12 -04:00
-> set ( 'uri' , $query -> createNamedParameter ( $tragetObjectUri , IQueryBuilder :: PARAM_STR ))
-> where ( $query -> expr () -> eq ( 'id' , $query -> createNamedParameter ( $sourceObjectId , IQueryBuilder :: PARAM_INT ), IQueryBuilder :: PARAM_INT ))
2023-02-10 06:17:27 -05:00
-> executeStatement ();
2021-12-06 14:01:22 -05:00
2025-04-16 17:43:12 -04:00
$this -> purgeProperties ( $sourceCalendarId , $sourceObjectId );
$this -> updateProperties ( $targetCalendarId , $tragetObjectUri , $object [ 'calendardata' ], $calendarType );
2021-12-06 14:01:22 -05:00
2025-04-16 17:43:12 -04:00
$this -> addChanges ( $sourceCalendarId , [ $sourceObjectUri ], 3 , $calendarType );
$this -> addChanges ( $targetCalendarId , [ $tragetObjectUri ], 1 , $calendarType );
2021-12-06 14:01:22 -05:00
2025-04-16 17:43:12 -04:00
$object = $this -> getCalendarObjectById ( $targetPrincipalUri , $sourceObjectId );
2023-02-10 06:17:27 -05:00
// Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
if ( empty ( $object )) {
return false ;
}
2021-12-06 14:01:22 -05:00
2023-02-10 06:17:27 -05:00
$targetCalendarRow = $this -> getCalendarById ( $targetCalendarId );
// the calendar this event is being moved to does not exist any longer
if ( empty ( $targetCalendarRow )) {
return false ;
}
2021-12-06 14:01:22 -05:00
2023-02-10 06:17:27 -05:00
if ( $calendarType === self :: CALENDAR_TYPE_CALENDAR ) {
$sourceShares = $this -> getShares ( $sourceCalendarId );
$targetShares = $this -> getShares ( $targetCalendarId );
$sourceCalendarRow = $this -> getCalendarById ( $sourceCalendarId );
$this -> dispatcher -> dispatchTyped ( new CalendarObjectMovedEvent ( $sourceCalendarId , $sourceCalendarRow , $targetCalendarId , $targetCalendarRow , $sourceShares , $targetShares , $object ));
}
return true ;
}, $this -> db );
2021-12-06 14:01:22 -05:00
}
2015-10-30 20:28:21 -04:00
/**
* Deletes an existing calendar object .
*
* The object uri is only the basename , or filename and not a full path .
*
* @ param mixed $calendarId
* @ param string $objectUri
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2021-03-12 05:20:04 -05:00
* @ param bool $forceDeletePermanently
2015-10-30 20:28:21 -04:00
* @ return void
*/
2021-03-12 05:20:04 -05:00
public function deleteCalendarObject ( $calendarId , $objectUri , $calendarType = self :: CALENDAR_TYPE_CALENDAR , bool $forceDeletePermanently = false ) {
2023-07-25 12:09:11 -04:00
$this -> cachedObjects = [];
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $calendarId , $objectUri , $calendarType , $forceDeletePermanently ) : void {
2023-02-10 06:17:27 -05:00
$data = $this -> getCalendarObject ( $calendarId , $objectUri , $calendarType );
2021-03-12 05:20:04 -05:00
2023-02-10 06:17:27 -05:00
if ( $data === null ) {
// Nothing to delete
return ;
}
2021-03-12 05:20:04 -05:00
2023-02-10 06:17:27 -05:00
if ( $forceDeletePermanently || $this -> config -> getAppValue ( Application :: APP_ID , RetentionService :: RETENTION_CONFIG_KEY ) === '0' ) {
$stmt = $this -> db -> prepare ( 'DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?' );
$stmt -> execute ([ $calendarId , $objectUri , $calendarType ]);
2021-03-12 05:20:04 -05:00
2023-02-10 06:17:27 -05:00
$this -> purgeProperties ( $calendarId , $data [ 'id' ]);
2021-03-12 05:20:04 -05:00
2024-09-08 19:33:16 -04:00
$this -> purgeObjectInvitations ( $data [ 'uid' ]);
2023-02-10 06:17:27 -05:00
if ( $calendarType === self :: CALENDAR_TYPE_CALENDAR ) {
$calendarRow = $this -> getCalendarById ( $calendarId );
$shares = $this -> getShares ( $calendarId );
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$this -> dispatcher -> dispatchTyped ( new CalendarObjectDeletedEvent ( $calendarId , $calendarRow , $shares , $data ));
} else {
$subscriptionRow = $this -> getSubscriptionById ( $calendarId );
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$this -> dispatcher -> dispatchTyped ( new CachedCalendarObjectDeletedEvent ( $calendarId , $subscriptionRow , [], $data ));
}
2021-03-12 05:20:04 -05:00
} else {
2023-02-10 06:17:27 -05:00
$pathInfo = pathinfo ( $data [ 'uri' ]);
if ( ! empty ( $pathInfo [ 'extension' ])) {
// Append a suffix to "free" the old URI for recreation
$newUri = sprintf (
'%s-deleted.%s' ,
$pathInfo [ 'filename' ],
$pathInfo [ 'extension' ]
);
} else {
$newUri = sprintf (
'%s-deleted' ,
$pathInfo [ 'filename' ]
);
}
2021-03-12 05:20:04 -05:00
2023-02-10 06:17:27 -05:00
// Try to detect conflicts before the DB does
// As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
$newObject = $this -> getCalendarObject ( $calendarId , $newUri , $calendarType );
if ( $newObject !== null ) {
throw new Forbidden ( " A calendar object with URI $newUri already exists in calendar $calendarId , therefore this object can't be moved into the trashbin " );
}
2015-10-30 20:28:21 -04:00
2023-02-10 06:17:27 -05:00
$qb = $this -> db -> getQueryBuilder ();
$markObjectDeletedQuery = $qb -> update ( 'calendarobjects' )
-> set ( 'deleted_at' , $qb -> createNamedParameter ( time (), IQueryBuilder :: PARAM_INT ))
-> set ( 'uri' , $qb -> createNamedParameter ( $newUri ))
-> where (
$qb -> expr () -> eq ( 'calendarid' , $qb -> createNamedParameter ( $calendarId )),
$qb -> expr () -> eq ( 'calendartype' , $qb -> createNamedParameter ( $calendarType , IQueryBuilder :: PARAM_INT ), IQueryBuilder :: PARAM_INT ),
$qb -> expr () -> eq ( 'uri' , $qb -> createNamedParameter ( $objectUri ))
);
$markObjectDeletedQuery -> executeStatement ();
$calendarData = $this -> getCalendarById ( $calendarId );
if ( $calendarData !== null ) {
$this -> dispatcher -> dispatchTyped (
new CalendarObjectMovedToTrashEvent (
$calendarId ,
$calendarData ,
$this -> getShares ( $calendarId ),
$data
)
);
}
2021-03-12 05:20:04 -05:00
}
2017-03-25 06:56:40 -04:00
2024-03-11 11:27:23 -04:00
$this -> addChanges ( $calendarId , [ $objectUri ], 3 , $calendarType );
2023-02-10 06:17:27 -05:00
}, $this -> db );
2015-10-30 20:28:21 -04:00
}
2021-03-12 05:20:04 -05:00
/**
* @ param mixed $objectData
*
* @ throws Forbidden
*/
public function restoreCalendarObject ( array $objectData ) : void {
2023-07-25 12:09:11 -04:00
$this -> cachedObjects = [];
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $objectData ) : void {
2023-02-10 06:17:27 -05:00
$id = ( int ) $objectData [ 'id' ];
$restoreUri = str_replace ( '-deleted.ics' , '.ics' , $objectData [ 'uri' ]);
$targetObject = $this -> getCalendarObject (
$objectData [ 'calendarid' ],
$restoreUri
);
if ( $targetObject !== null ) {
throw new Forbidden ( " Can not restore calendar $id because a calendar object with the URI $restoreUri already exists " );
}
2021-03-12 05:20:04 -05:00
2023-02-10 06:17:27 -05:00
$qb = $this -> db -> getQueryBuilder ();
$update = $qb -> update ( 'calendarobjects' )
-> set ( 'uri' , $qb -> createNamedParameter ( $restoreUri ))
-> set ( 'deleted_at' , $qb -> createNamedParameter ( null ))
-> where ( $qb -> expr () -> eq ( 'id' , $qb -> createNamedParameter ( $id , IQueryBuilder :: PARAM_INT ), IQueryBuilder :: PARAM_INT ));
$update -> executeStatement ();
// Make sure this change is tracked in the changes table
$qb2 = $this -> db -> getQueryBuilder ();
$selectObject = $qb2 -> select ( 'calendardata' , 'uri' , 'calendarid' , 'calendartype' )
-> selectAlias ( 'componenttype' , 'component' )
-> from ( 'calendarobjects' )
-> where ( $qb2 -> expr () -> eq ( 'id' , $qb2 -> createNamedParameter ( $id , IQueryBuilder :: PARAM_INT ), IQueryBuilder :: PARAM_INT ));
$result = $selectObject -> executeQuery ();
$row = $result -> fetch ();
$result -> closeCursor ();
if ( $row === false ) {
// Welp, this should possibly not have happened, but let's ignore
return ;
}
2024-03-11 11:27:23 -04:00
$this -> addChanges ( $row [ 'calendarid' ], [ $row [ 'uri' ]], 1 , ( int ) $row [ 'calendartype' ]);
2021-03-12 05:20:04 -05:00
2023-02-10 06:17:27 -05:00
$calendarRow = $this -> getCalendarById (( int ) $row [ 'calendarid' ]);
if ( $calendarRow === null ) {
throw new RuntimeException ( 'Calendar object data that was just written can\'t be read back. Check your database configuration.' );
}
$this -> dispatcher -> dispatchTyped (
new CalendarObjectRestoredEvent (
( int ) $objectData [ 'calendarid' ],
$calendarRow ,
$this -> getShares (( int ) $row [ 'calendarid' ]),
$row
)
);
}, $this -> db );
2021-03-12 05:20:04 -05:00
}
2015-10-30 20:28:21 -04:00
/**
* Performs a calendar - query on the contents of this calendar .
*
* The calendar - query is defined in RFC4791 : CalDAV . Using the
* calendar - query it is possible for a client to request a specific set of
* object , based on contents of iCalendar properties , date - ranges and
* iCalendar component types ( VTODO , VEVENT ) .
*
* This method should just return a list of ( relative ) urls that match this
* query .
*
* The list of filters are specified as an array . The exact array is
* documented by Sabre\CalDAV\CalendarQueryParser .
*
* Note that it is extremely likely that getCalendarObject for every path
* returned from this method will be called almost immediately after . You
* may want to anticipate this to speed up these requests .
*
* This method provides a default implementation , which parses * all * the
* iCalendar objects in the specified calendar .
*
* This default may well be good enough for personal use , and calendars
* that aren ' t very large . But if you anticipate high usage , big calendars
2016-01-29 12:25:27 -05:00
* or high loads , you are strongly advised to optimize certain paths .
2015-10-30 20:28:21 -04:00
*
* The best way to do so is override this method and to optimize
* specifically for 'common filters' .
*
* Requests that are extremely common are :
* * requests for just VEVENTS
* * requests for just VTODO
* * requests with a time - range - filter on either VEVENT or VTODO .
*
* .. and combinations of these requests . It may not be worth it to try to
* handle every possible situation and just rely on the ( relatively
* easy to use ) CalendarQueryValidator to handle the rest .
*
* Note that especially time - range - filters may be difficult to parse . A
* time - range filter specified on a VEVENT must for instance also handle
* recurrence rules correctly .
2022-07-26 17:21:57 -04:00
* A good example of how to interpret all these filters can also simply
2015-10-30 20:28:21 -04:00
* be found in Sabre\CalDAV\CalendarQueryFilter . This class is as correct
* as possible , so it gives you a good idea on what type of stuff you need
* to think of .
*
2020-08-19 11:54:00 -04:00
* @ param mixed $calendarId
2015-10-30 20:28:21 -04:00
* @ param array $filters
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2015-10-30 20:28:21 -04:00
* @ return array
*/
2020-10-05 09:12:57 -04:00
public function calendarQuery ( $calendarId , array $filters , $calendarType = self :: CALENDAR_TYPE_CALENDAR ) : array {
2015-10-30 20:28:21 -04:00
$componentType = null ;
$requirePostFilter = true ;
$timeRange = null ;
// if no filters were specified, we don't need to filter after a query
if ( ! $filters [ 'prop-filters' ] && ! $filters [ 'comp-filters' ]) {
$requirePostFilter = false ;
}
// Figuring out if there's a component filter
if ( count ( $filters [ 'comp-filters' ]) > 0 && ! $filters [ 'comp-filters' ][ 0 ][ 'is-not-defined' ]) {
$componentType = $filters [ 'comp-filters' ][ 0 ][ 'name' ];
// Checking if we need post-filters
if ( ! $filters [ 'prop-filters' ] && ! $filters [ 'comp-filters' ][ 0 ][ 'comp-filters' ] && ! $filters [ 'comp-filters' ][ 0 ][ 'time-range' ] && ! $filters [ 'comp-filters' ][ 0 ][ 'prop-filters' ]) {
$requirePostFilter = false ;
}
// There was a time-range filter
2020-10-16 01:47:29 -04:00
if ( $componentType === 'VEVENT' && isset ( $filters [ 'comp-filters' ][ 0 ][ 'time-range' ]) && is_array ( $filters [ 'comp-filters' ][ 0 ][ 'time-range' ])) {
2015-10-30 20:28:21 -04:00
$timeRange = $filters [ 'comp-filters' ][ 0 ][ 'time-range' ];
// If start time OR the end time is not specified, we can do a
// 100% accurate mysql query.
if ( ! $filters [ 'prop-filters' ] && ! $filters [ 'comp-filters' ][ 0 ][ 'comp-filters' ] && ! $filters [ 'comp-filters' ][ 0 ][ 'prop-filters' ] && ( ! $timeRange [ 'start' ] || ! $timeRange [ 'end' ])) {
$requirePostFilter = false ;
}
}
}
$query = $this -> db -> getQueryBuilder ();
2024-09-08 19:33:16 -04:00
$query -> select ([ 'id' , 'uri' , 'uid' , 'lastmodified' , 'etag' , 'calendarid' , 'size' , 'calendardata' , 'componenttype' , 'classification' , 'deleted_at' ])
2015-10-30 20:28:21 -04:00
-> from ( 'calendarobjects' )
2020-08-19 11:54:00 -04:00
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $calendarId )))
2021-03-12 05:20:04 -05:00
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( $calendarType )))
-> andWhere ( $query -> expr () -> isNull ( 'deleted_at' ));
2015-10-30 20:28:21 -04:00
if ( $componentType ) {
$query -> andWhere ( $query -> expr () -> eq ( 'componenttype' , $query -> createNamedParameter ( $componentType )));
}
if ( $timeRange && $timeRange [ 'start' ]) {
2023-02-02 05:30:49 -05:00
$query -> andWhere ( $query -> expr () -> gt ( 'lastoccurence' , $query -> createNamedParameter ( $timeRange [ 'start' ] -> getTimeStamp ())));
2015-10-30 20:28:21 -04:00
}
if ( $timeRange && $timeRange [ 'end' ]) {
2023-02-02 05:30:49 -05:00
$query -> andWhere ( $query -> expr () -> lt ( 'firstoccurence' , $query -> createNamedParameter ( $timeRange [ 'end' ] -> getTimeStamp ())));
2015-10-30 20:28:21 -04:00
}
2021-04-23 05:43:16 -04:00
$stmt = $query -> executeQuery ();
2015-10-30 20:28:21 -04:00
$result = [];
2021-04-23 05:37:22 -04:00
while ( $row = $stmt -> fetch ()) {
2023-07-25 12:09:11 -04:00
// if we leave it as a blob we can't read it both from the post filter and the rowToCalendarObject
if ( isset ( $row [ 'calendardata' ])) {
$row [ 'calendardata' ] = $this -> readBlob ( $row [ 'calendardata' ]);
}
2015-10-30 20:28:21 -04:00
if ( $requirePostFilter ) {
2017-10-22 06:16:58 -04:00
// validateFilterForObject will parse the calendar data
// catch parsing errors
try {
$matches = $this -> validateFilterForObject ( $row , $filters );
} catch ( ParseException $ex ) {
2022-03-31 09:34:57 -04:00
$this -> logger -> error ( 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row [ 'uri' ], [
2017-10-22 06:16:58 -04:00
'app' => 'dav' ,
2022-03-31 09:34:57 -04:00
'exception' => $ex ,
2017-10-22 06:16:58 -04:00
]);
continue ;
} catch ( InvalidDataException $ex ) {
2022-03-31 09:34:57 -04:00
$this -> logger -> error ( 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row [ 'uri' ], [
2017-10-22 06:16:58 -04:00
'app' => 'dav' ,
2022-03-31 09:34:57 -04:00
'exception' => $ex ,
2017-10-22 06:16:58 -04:00
]);
continue ;
2024-09-12 09:32:44 -04:00
} catch ( MaxInstancesExceededException $ex ) {
$this -> logger -> warning ( 'Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row [ 'uri' ], [
'app' => 'dav' ,
'exception' => $ex ,
]);
continue ;
2017-10-22 06:16:58 -04:00
}
if ( ! $matches ) {
2015-10-30 20:28:21 -04:00
continue ;
}
}
$result [] = $row [ 'uri' ];
2023-07-25 12:09:11 -04:00
$key = $calendarId . '::' . $row [ 'uri' ] . '::' . $calendarType ;
$this -> cachedObjects [ $key ] = $this -> rowToCalendarObject ( $row );
2015-10-30 20:28:21 -04:00
}
return $result ;
}
2017-03-25 06:56:40 -04:00
/**
* custom Nextcloud search extension for CalDAV
*
2018-06-28 07:07:33 -04:00
* TODO - this should optionally cover cached calendar objects as well
*
2017-03-25 06:56:40 -04:00
* @ param string $principalUri
* @ param array $filters
* @ param integer | null $limit
* @ param integer | null $offset
* @ return array
*/
2020-10-05 09:12:57 -04:00
public function calendarSearch ( $principalUri , array $filters , $limit = null , $offset = null ) {
2023-02-10 06:17:27 -05:00
return $this -> atomic ( function () use ( $principalUri , $filters , $limit , $offset ) {
$calendars = $this -> getCalendarsForUser ( $principalUri );
$ownCalendars = [];
$sharedCalendars = [];
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
$uriMapper = [];
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
foreach ( $calendars as $calendar ) {
if ( $calendar [ '{http://owncloud.org/ns}owner-principal' ] === $principalUri ) {
$ownCalendars [] = $calendar [ 'id' ];
} else {
$sharedCalendars [] = $calendar [ 'id' ];
}
$uriMapper [ $calendar [ 'id' ]] = $calendar [ 'uri' ];
}
if ( count ( $ownCalendars ) === 0 && count ( $sharedCalendars ) === 0 ) {
return [];
2017-03-25 06:56:40 -04:00
}
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
// Calendar id expressions
$calendarExpressions = [];
foreach ( $ownCalendars as $id ) {
$calendarExpressions [] = $query -> expr () -> andX (
$query -> expr () -> eq ( 'c.calendarid' ,
$query -> createNamedParameter ( $id )),
$query -> expr () -> eq ( 'c.calendartype' ,
$query -> createNamedParameter ( self :: CALENDAR_TYPE_CALENDAR )));
}
foreach ( $sharedCalendars as $id ) {
$calendarExpressions [] = $query -> expr () -> andX (
$query -> expr () -> eq ( 'c.calendarid' ,
$query -> createNamedParameter ( $id )),
$query -> expr () -> eq ( 'c.classification' ,
$query -> createNamedParameter ( self :: CLASSIFICATION_PUBLIC )),
$query -> expr () -> eq ( 'c.calendartype' ,
$query -> createNamedParameter ( self :: CALENDAR_TYPE_CALENDAR )));
}
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
if ( count ( $calendarExpressions ) === 1 ) {
$calExpr = $calendarExpressions [ 0 ];
} else {
$calExpr = call_user_func_array ([ $query -> expr (), 'orX' ], $calendarExpressions );
}
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
// Component expressions
$compExpressions = [];
foreach ( $filters [ 'comps' ] as $comp ) {
$compExpressions [] = $query -> expr ()
-> eq ( 'c.componenttype' , $query -> createNamedParameter ( $comp ));
}
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
if ( count ( $compExpressions ) === 1 ) {
$compExpr = $compExpressions [ 0 ];
} else {
$compExpr = call_user_func_array ([ $query -> expr (), 'orX' ], $compExpressions );
}
2017-04-25 13:26:47 -04:00
2023-02-10 06:17:27 -05:00
if ( ! isset ( $filters [ 'props' ])) {
$filters [ 'props' ] = [];
}
if ( ! isset ( $filters [ 'params' ])) {
$filters [ 'params' ] = [];
}
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
$propParamExpressions = [];
foreach ( $filters [ 'props' ] as $prop ) {
$propParamExpressions [] = $query -> expr () -> andX (
$query -> expr () -> eq ( 'i.name' , $query -> createNamedParameter ( $prop )),
$query -> expr () -> isNull ( 'i.parameter' )
);
}
foreach ( $filters [ 'params' ] as $param ) {
$propParamExpressions [] = $query -> expr () -> andX (
$query -> expr () -> eq ( 'i.name' , $query -> createNamedParameter ( $param [ 'property' ])),
$query -> expr () -> eq ( 'i.parameter' , $query -> createNamedParameter ( $param [ 'parameter' ]))
);
}
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
if ( count ( $propParamExpressions ) === 1 ) {
$propParamExpr = $propParamExpressions [ 0 ];
} else {
$propParamExpr = call_user_func_array ([ $query -> expr (), 'orX' ], $propParamExpressions );
}
2017-04-25 13:26:47 -04:00
2023-02-10 06:17:27 -05:00
$query -> select ([ 'c.calendarid' , 'c.uri' ])
-> from ( $this -> dbObjectPropertiesTable , 'i' )
-> join ( 'i' , 'calendarobjects' , 'c' , $query -> expr () -> eq ( 'i.objectid' , 'c.id' ))
-> where ( $calExpr )
-> andWhere ( $compExpr )
-> andWhere ( $propParamExpr )
-> andWhere ( $query -> expr () -> iLike ( 'i.value' ,
$query -> createNamedParameter ( '%' . $this -> db -> escapeLikeParameter ( $filters [ 'search-term' ]) . '%' )))
-> andWhere ( $query -> expr () -> isNull ( 'deleted_at' ));
if ( $offset ) {
$query -> setFirstResult ( $offset );
}
if ( $limit ) {
$query -> setMaxResults ( $limit );
}
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
$stmt = $query -> executeQuery ();
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
$result = [];
while ( $row = $stmt -> fetch ()) {
$path = $uriMapper [ $row [ 'calendarid' ]] . '/' . $row [ 'uri' ];
if ( ! in_array ( $path , $result )) {
$result [] = $path ;
}
2017-04-25 13:26:47 -04:00
}
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
return $result ;
}, $this -> db );
2017-03-25 06:56:40 -04:00
}
2017-11-06 19:31:28 -05:00
/**
* used for Nextcloud ' s calendar API
*
* @ param array $calendarInfo
* @ param string $pattern
* @ param array $searchProperties
* @ param array $options
* @ param integer | null $limit
* @ param integer | null $offset
*
* @ return array
*/
2023-09-26 05:07:49 -04:00
public function search (
array $calendarInfo ,
$pattern ,
array $searchProperties ,
array $options ,
$limit ,
$offset ,
) {
2017-11-06 19:31:28 -05:00
$outerQuery = $this -> db -> getQueryBuilder ();
$innerQuery = $this -> db -> getQueryBuilder ();
2024-03-26 16:39:22 -04:00
if ( isset ( $calendarInfo [ 'source' ])) {
$calendarType = self :: CALENDAR_TYPE_SUBSCRIPTION ;
2025-05-14 05:21:28 -04:00
} elseif ( isset ( $calendarInfo [ 'federated' ])) {
$calendarType = self :: CALENDAR_TYPE_FEDERATED ;
2024-03-26 16:39:22 -04:00
} else {
$calendarType = self :: CALENDAR_TYPE_CALENDAR ;
}
2017-11-06 19:31:28 -05:00
$innerQuery -> selectDistinct ( 'op.objectid' )
-> from ( $this -> dbObjectPropertiesTable , 'op' )
-> andWhere ( $innerQuery -> expr () -> eq ( 'op.calendarid' ,
2018-06-28 07:07:33 -04:00
$outerQuery -> createNamedParameter ( $calendarInfo [ 'id' ])))
-> andWhere ( $innerQuery -> expr () -> eq ( 'op.calendartype' ,
2024-03-26 16:39:22 -04:00
$outerQuery -> createNamedParameter ( $calendarType )));
2017-11-06 19:31:28 -05:00
2023-08-07 13:35:08 -04:00
$outerQuery -> select ( 'c.id' , 'c.calendardata' , 'c.componenttype' , 'c.uid' , 'c.uri' )
-> from ( 'calendarobjects' , 'c' )
-> where ( $outerQuery -> expr () -> isNull ( 'deleted_at' ));
2017-11-06 19:31:28 -05:00
// only return public items for shared calendars for now
2021-09-27 08:58:16 -04:00
if ( isset ( $calendarInfo [ '{http://owncloud.org/ns}owner-principal' ]) === false || $calendarInfo [ 'principaluri' ] !== $calendarInfo [ '{http://owncloud.org/ns}owner-principal' ]) {
2023-08-07 10:49:13 -04:00
$outerQuery -> andWhere ( $outerQuery -> expr () -> eq ( 'c.classification' ,
2017-11-06 19:31:28 -05:00
$outerQuery -> createNamedParameter ( self :: CLASSIFICATION_PUBLIC )));
}
2021-09-27 08:58:16 -04:00
if ( ! empty ( $searchProperties )) {
2024-07-04 05:26:17 -04:00
$or = [];
2021-09-27 08:58:16 -04:00
foreach ( $searchProperties as $searchProperty ) {
2024-07-04 05:26:17 -04:00
$or [] = $innerQuery -> expr () -> eq ( 'op.name' ,
$outerQuery -> createNamedParameter ( $searchProperty ));
2021-09-27 08:58:16 -04:00
}
2024-07-04 05:26:17 -04:00
$innerQuery -> andWhere ( $innerQuery -> expr () -> orX ( ... $or ));
2017-11-06 19:31:28 -05:00
}
if ( $pattern !== '' ) {
$innerQuery -> andWhere ( $innerQuery -> expr () -> iLike ( 'op.value' ,
$outerQuery -> createNamedParameter ( '%'
. $this -> db -> escapeLikeParameter ( $pattern ) . '%' )));
}
2024-05-07 09:55:47 -04:00
$start = null ;
$end = null ;
$hasLimit = is_int ( $limit );
$hasTimeRange = false ;
if ( isset ( $options [ 'timerange' ][ 'start' ]) && $options [ 'timerange' ][ 'start' ] instanceof DateTimeInterface ) {
/** @var DateTimeInterface $start */
$start = $options [ 'timerange' ][ 'start' ];
$outerQuery -> andWhere (
$outerQuery -> expr () -> gt (
'lastoccurence' ,
$outerQuery -> createNamedParameter ( $start -> getTimestamp ())
)
);
$hasTimeRange = true ;
}
if ( isset ( $options [ 'timerange' ][ 'end' ]) && $options [ 'timerange' ][ 'end' ] instanceof DateTimeInterface ) {
/** @var DateTimeInterface $end */
$end = $options [ 'timerange' ][ 'end' ];
$outerQuery -> andWhere (
$outerQuery -> expr () -> lt (
'firstoccurence' ,
$outerQuery -> createNamedParameter ( $end -> getTimestamp ())
)
);
$hasTimeRange = true ;
2017-11-09 09:08:30 -05:00
}
2017-11-06 19:31:28 -05:00
2023-01-23 12:23:52 -05:00
if ( isset ( $options [ 'uid' ])) {
2022-06-23 16:17:53 -04:00
$outerQuery -> andWhere ( $outerQuery -> expr () -> eq ( 'uid' , $outerQuery -> createNamedParameter ( $options [ 'uid' ])));
}
2021-09-27 08:58:16 -04:00
if ( ! empty ( $options [ 'types' ])) {
2024-07-04 05:26:17 -04:00
$or = [];
2017-11-09 09:08:30 -05:00
foreach ( $options [ 'types' ] as $type ) {
2024-07-04 05:26:17 -04:00
$or [] = $outerQuery -> expr () -> eq ( 'componenttype' ,
$outerQuery -> createNamedParameter ( $type ));
2017-11-09 09:08:30 -05:00
}
2024-07-04 05:26:17 -04:00
$outerQuery -> andWhere ( $outerQuery -> expr () -> orX ( ... $or ));
2017-11-06 19:31:28 -05:00
}
2021-09-27 08:58:16 -04:00
$outerQuery -> andWhere ( $outerQuery -> expr () -> in ( 'c.id' , $outerQuery -> createFunction ( $innerQuery -> getSQL ())));
2017-11-06 19:31:28 -05:00
2024-05-15 06:55:40 -04:00
// Without explicit order by its undefined in which order the SQL server returns the events.
// For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful.
$outerQuery -> addOrderBy ( 'id' );
2024-05-07 09:55:47 -04:00
$offset = ( int ) $offset ;
$outerQuery -> setFirstResult ( $offset );
2017-11-06 19:31:28 -05:00
2023-09-27 12:36:45 -04:00
$calendarObjects = [];
2021-12-02 09:06:05 -05:00
2024-05-07 09:55:47 -04:00
if ( $hasLimit && $hasTimeRange ) {
/**
* Event recurrences are evaluated at runtime because the database only knows the first and last occurrence .
*
* Given , a user created 8 events with a yearly reoccurrence and two for events tomorrow .
* The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days .
*
* If limit 7 is applied to the SQL query , we find the 7 events with a yearly reoccurrence
* and discard the events after evaluating the reoccurrence rules because they are not due within
* the next 14 days and end up with an empty result even if there are two events to show .
*
* The workaround for search requests with a limit and time range is asking for more row than requested
* and retrying if we have not reached the limit .
*
* 25 rows and 3 retries is entirely arbitrary .
*/
$maxResults = ( int ) max ( $limit , 25 );
$outerQuery -> setMaxResults ( $maxResults );
for ( $attempt = $objectsCount = 0 ; $attempt < 3 && $objectsCount < $limit ; $attempt ++ ) {
$objectsCount = array_push ( $calendarObjects , ... $this -> searchCalendarObjects ( $outerQuery , $start , $end ));
$outerQuery -> setFirstResult ( $offset += $maxResults );
2021-12-02 09:06:05 -05:00
}
2024-05-07 09:55:47 -04:00
$calendarObjects = array_slice ( $calendarObjects , 0 , $limit , false );
} else {
$outerQuery -> setMaxResults ( $limit );
$calendarObjects = $this -> searchCalendarObjects ( $outerQuery , $start , $end );
2023-09-27 12:36:45 -04:00
}
2017-11-06 19:31:28 -05:00
2024-05-15 06:55:40 -04:00
$calendarObjects = array_map ( function ( $o ) use ( $options ) {
2017-11-06 19:31:28 -05:00
$calendarData = Reader :: read ( $o [ 'calendardata' ]);
2023-09-20 11:45:54 -04:00
// Expand recurrences if an explicit time range is requested
if ( $calendarData instanceof VCalendar
&& isset ( $options [ 'timerange' ][ 'start' ], $options [ 'timerange' ][ 'end' ])) {
$calendarData = $calendarData -> expand (
$options [ 'timerange' ][ 'start' ],
$options [ 'timerange' ][ 'end' ],
);
}
2017-11-06 19:31:28 -05:00
$comps = $calendarData -> getComponents ();
$objects = [];
$timezones = [];
foreach ( $comps as $comp ) {
if ( $comp instanceof VTimeZone ) {
$timezones [] = $comp ;
} else {
$objects [] = $comp ;
}
}
return [
'id' => $o [ 'id' ],
'type' => $o [ 'componenttype' ],
'uid' => $o [ 'uid' ],
'uri' => $o [ 'uri' ],
2020-04-09 07:53:40 -04:00
'objects' => array_map ( function ( $c ) {
2017-11-06 19:31:28 -05:00
return $this -> transformSearchData ( $c );
}, $objects ),
2020-04-09 07:53:40 -04:00
'timezones' => array_map ( function ( $c ) {
2017-11-06 19:31:28 -05:00
return $this -> transformSearchData ( $c );
}, $timezones ),
];
}, $calendarObjects );
2024-05-15 06:55:40 -04:00
usort ( $calendarObjects , function ( array $a , array $b ) {
/** @var DateTimeImmutable $startA */
$startA = $a [ 'objects' ][ 0 ][ 'DTSTART' ][ 0 ] ? ? new DateTimeImmutable ( self :: MAX_DATE );
/** @var DateTimeImmutable $startB */
$startB = $b [ 'objects' ][ 0 ][ 'DTSTART' ][ 0 ] ? ? new DateTimeImmutable ( self :: MAX_DATE );
return $startA -> getTimestamp () <=> $startB -> getTimestamp ();
});
return $calendarObjects ;
2017-11-06 19:31:28 -05:00
}
2024-05-07 09:55:47 -04:00
private function searchCalendarObjects ( IQueryBuilder $query , ? DateTimeInterface $start , ? DateTimeInterface $end ) : array {
$calendarObjects = [];
$filterByTimeRange = ( $start instanceof DateTimeInterface ) || ( $end instanceof DateTimeInterface );
$result = $query -> executeQuery ();
while (( $row = $result -> fetch ()) !== false ) {
if ( $filterByTimeRange === false ) {
// No filter required
$calendarObjects [] = $row ;
continue ;
}
2024-09-12 09:32:44 -04:00
try {
$isValid = $this -> validateFilterForObject ( $row , [
'name' => 'VCALENDAR' ,
'comp-filters' => [
[
'name' => 'VEVENT' ,
'comp-filters' => [],
'prop-filters' => [],
'is-not-defined' => false ,
'time-range' => [
'start' => $start ,
'end' => $end ,
],
2024-05-07 09:55:47 -04:00
],
],
2024-09-12 09:32:44 -04:00
'prop-filters' => [],
'is-not-defined' => false ,
'time-range' => null ,
]);
} catch ( MaxInstancesExceededException $ex ) {
$this -> logger -> warning ( 'Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row [ 'uri' ], [
'app' => 'dav' ,
'exception' => $ex ,
]);
continue ;
}
2024-05-07 09:55:47 -04:00
if ( is_resource ( $row [ 'calendardata' ])) {
// Put the stream back to the beginning so it can be read another time
rewind ( $row [ 'calendardata' ]);
}
if ( $isValid ) {
$calendarObjects [] = $row ;
}
}
$result -> closeCursor ();
return $calendarObjects ;
}
2017-11-06 19:31:28 -05:00
/**
* @ param Component $comp
* @ return array
*/
private function transformSearchData ( Component $comp ) {
$data = [];
/** @var Component[] $subComponents */
$subComponents = $comp -> getComponents ();
/** @var Property[] $properties */
2020-04-09 07:53:40 -04:00
$properties = array_filter ( $comp -> children (), function ( $c ) {
2017-11-06 19:31:28 -05:00
return $c instanceof Property ;
});
$validationRules = $comp -> getValidationRules ();
foreach ( $subComponents as $subComponent ) {
$name = $subComponent -> name ;
if ( ! isset ( $data [ $name ])) {
$data [ $name ] = [];
}
$data [ $name ][] = $this -> transformSearchData ( $subComponent );
}
foreach ( $properties as $property ) {
$name = $property -> name ;
if ( ! isset ( $validationRules [ $name ])) {
$validationRules [ $name ] = '*' ;
}
$rule = $validationRules [ $property -> name ];
if ( $rule === '+' || $rule === '*' ) { // multiple
if ( ! isset ( $data [ $name ])) {
$data [ $name ] = [];
}
$data [ $name ][] = $this -> transformSearchProperty ( $property );
} else { // once
$data [ $name ] = $this -> transformSearchProperty ( $property );
}
}
return $data ;
}
/**
* @ param Property $prop
* @ return array
*/
private function transformSearchProperty ( Property $prop ) {
// No need to check Date, as it extends DateTime
if ( $prop instanceof Property\ICalendar\DateTime ) {
$value = $prop -> getDateTime ();
} else {
$value = $prop -> getValue ();
}
return [
$value ,
$prop -> parameters ()
];
}
2020-07-27 10:14:15 -04:00
/**
* @ param string $principalUri
* @ param string $pattern
* @ param array $componentTypes
* @ param array $searchProperties
* @ param array $searchParameters
* @ param array $options
* @ return array
*/
public function searchPrincipalUri ( string $principalUri ,
2023-09-26 05:07:49 -04:00
string $pattern ,
array $componentTypes ,
array $searchProperties ,
array $searchParameters ,
array $options = [],
) : array {
2023-02-10 06:17:27 -05:00
return $this -> atomic ( function () use ( $principalUri , $pattern , $componentTypes , $searchProperties , $searchParameters , $options ) {
$escapePattern = ! \array_key_exists ( 'escape_like_param' , $options ) || $options [ 'escape_like_param' ] !== false ;
$calendarObjectIdQuery = $this -> db -> getQueryBuilder ();
2024-07-04 05:26:17 -04:00
$calendarOr = [];
$searchOr = [];
2023-02-10 06:17:27 -05:00
// Fetch calendars and subscription
$calendars = $this -> getCalendarsForUser ( $principalUri );
$subscriptions = $this -> getSubscriptionsForUser ( $principalUri );
foreach ( $calendars as $calendar ) {
2024-07-04 05:26:17 -04:00
$calendarAnd = $calendarObjectIdQuery -> expr () -> andX (
$calendarObjectIdQuery -> expr () -> eq ( 'cob.calendarid' , $calendarObjectIdQuery -> createNamedParameter (( int ) $calendar [ 'id' ])),
$calendarObjectIdQuery -> expr () -> eq ( 'cob.calendartype' , $calendarObjectIdQuery -> createNamedParameter ( self :: CALENDAR_TYPE_CALENDAR )),
);
2023-02-10 06:17:27 -05:00
// If it's shared, limit search to public events
if ( isset ( $calendar [ '{http://owncloud.org/ns}owner-principal' ])
&& $calendar [ 'principaluri' ] !== $calendar [ '{http://owncloud.org/ns}owner-principal' ]) {
$calendarAnd -> add ( $calendarObjectIdQuery -> expr () -> eq ( 'co.classification' , $calendarObjectIdQuery -> createNamedParameter ( self :: CLASSIFICATION_PUBLIC )));
}
2020-07-27 10:14:15 -04:00
2024-07-04 05:26:17 -04:00
$calendarOr [] = $calendarAnd ;
2020-07-27 10:14:15 -04:00
}
2023-02-10 06:17:27 -05:00
foreach ( $subscriptions as $subscription ) {
2024-07-04 05:26:17 -04:00
$subscriptionAnd = $calendarObjectIdQuery -> expr () -> andX (
$calendarObjectIdQuery -> expr () -> eq ( 'cob.calendarid' , $calendarObjectIdQuery -> createNamedParameter (( int ) $subscription [ 'id' ])),
$calendarObjectIdQuery -> expr () -> eq ( 'cob.calendartype' , $calendarObjectIdQuery -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )),
);
2023-02-10 06:17:27 -05:00
// If it's shared, limit search to public events
if ( isset ( $subscription [ '{http://owncloud.org/ns}owner-principal' ])
&& $subscription [ 'principaluri' ] !== $subscription [ '{http://owncloud.org/ns}owner-principal' ]) {
$subscriptionAnd -> add ( $calendarObjectIdQuery -> expr () -> eq ( 'co.classification' , $calendarObjectIdQuery -> createNamedParameter ( self :: CLASSIFICATION_PUBLIC )));
}
2020-07-27 10:14:15 -04:00
2024-07-04 05:26:17 -04:00
$calendarOr [] = $subscriptionAnd ;
2020-07-27 10:14:15 -04:00
}
2023-02-10 06:17:27 -05:00
foreach ( $searchProperties as $property ) {
2024-07-04 05:26:17 -04:00
$propertyAnd = $calendarObjectIdQuery -> expr () -> andX (
$calendarObjectIdQuery -> expr () -> eq ( 'cob.name' , $calendarObjectIdQuery -> createNamedParameter ( $property , IQueryBuilder :: PARAM_STR )),
$calendarObjectIdQuery -> expr () -> isNull ( 'cob.parameter' ),
);
2020-07-27 10:14:15 -04:00
2024-07-04 05:26:17 -04:00
$searchOr [] = $propertyAnd ;
2023-02-10 06:17:27 -05:00
}
foreach ( $searchParameters as $property => $parameter ) {
2024-07-04 05:26:17 -04:00
$parameterAnd = $calendarObjectIdQuery -> expr () -> andX (
$calendarObjectIdQuery -> expr () -> eq ( 'cob.name' , $calendarObjectIdQuery -> createNamedParameter ( $property , IQueryBuilder :: PARAM_STR )),
$calendarObjectIdQuery -> expr () -> eq ( 'cob.parameter' , $calendarObjectIdQuery -> createNamedParameter ( $parameter , IQueryBuilder :: PARAM_STR_ARRAY )),
);
2020-07-27 10:14:15 -04:00
2024-07-04 05:26:17 -04:00
$searchOr [] = $parameterAnd ;
2023-02-10 06:17:27 -05:00
}
2020-07-27 10:14:15 -04:00
2024-07-04 05:26:17 -04:00
if ( empty ( $calendarOr )) {
2023-02-10 06:17:27 -05:00
return [];
}
2024-07-04 05:26:17 -04:00
if ( empty ( $searchOr )) {
2023-02-10 06:17:27 -05:00
return [];
}
2020-07-27 10:14:15 -04:00
2023-02-10 06:17:27 -05:00
$calendarObjectIdQuery -> selectDistinct ( 'cob.objectid' )
-> from ( $this -> dbObjectPropertiesTable , 'cob' )
-> leftJoin ( 'cob' , 'calendarobjects' , 'co' , $calendarObjectIdQuery -> expr () -> eq ( 'co.id' , 'cob.objectid' ))
-> andWhere ( $calendarObjectIdQuery -> expr () -> in ( 'co.componenttype' , $calendarObjectIdQuery -> createNamedParameter ( $componentTypes , IQueryBuilder :: PARAM_STR_ARRAY )))
2024-07-04 05:26:17 -04:00
-> andWhere ( $calendarObjectIdQuery -> expr () -> orX ( ... $calendarOr ))
-> andWhere ( $calendarObjectIdQuery -> expr () -> orX ( ... $searchOr ))
2023-02-10 06:17:27 -05:00
-> andWhere ( $calendarObjectIdQuery -> expr () -> isNull ( 'deleted_at' ));
2024-03-28 11:13:19 -04:00
if ( $pattern !== '' ) {
2023-02-10 06:17:27 -05:00
if ( ! $escapePattern ) {
$calendarObjectIdQuery -> andWhere ( $calendarObjectIdQuery -> expr () -> ilike ( 'cob.value' , $calendarObjectIdQuery -> createNamedParameter ( $pattern )));
} else {
$calendarObjectIdQuery -> andWhere ( $calendarObjectIdQuery -> expr () -> ilike ( 'cob.value' , $calendarObjectIdQuery -> createNamedParameter ( '%' . $this -> db -> escapeLikeParameter ( $pattern ) . '%' )));
}
2020-07-27 10:14:15 -04:00
}
2023-02-10 06:17:27 -05:00
if ( isset ( $options [ 'limit' ])) {
$calendarObjectIdQuery -> setMaxResults ( $options [ 'limit' ]);
}
if ( isset ( $options [ 'offset' ])) {
$calendarObjectIdQuery -> setFirstResult ( $options [ 'offset' ]);
}
2023-09-26 05:07:49 -04:00
if ( isset ( $options [ 'timerange' ])) {
if ( isset ( $options [ 'timerange' ][ 'start' ]) && $options [ 'timerange' ][ 'start' ] instanceof DateTimeInterface ) {
$calendarObjectIdQuery -> andWhere ( $calendarObjectIdQuery -> expr () -> gt (
'lastoccurence' ,
$calendarObjectIdQuery -> createNamedParameter ( $options [ 'timerange' ][ 'start' ] -> getTimeStamp ()),
));
}
if ( isset ( $options [ 'timerange' ][ 'end' ]) && $options [ 'timerange' ][ 'end' ] instanceof DateTimeInterface ) {
$calendarObjectIdQuery -> andWhere ( $calendarObjectIdQuery -> expr () -> lt (
'firstoccurence' ,
$calendarObjectIdQuery -> createNamedParameter ( $options [ 'timerange' ][ 'end' ] -> getTimeStamp ()),
));
}
}
2020-07-27 10:14:15 -04:00
2023-02-10 06:17:27 -05:00
$result = $calendarObjectIdQuery -> executeQuery ();
2023-09-27 12:36:45 -04:00
$matches = [];
while (( $row = $result -> fetch ()) !== false ) {
$matches [] = ( int ) $row [ 'objectid' ];
}
2023-02-10 06:17:27 -05:00
$result -> closeCursor ();
2020-07-27 10:14:15 -04:00
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> select ( 'calendardata' , 'uri' , 'calendarid' , 'calendartype' )
-> from ( 'calendarobjects' )
-> where ( $query -> expr () -> in ( 'id' , $query -> createNamedParameter ( $matches , IQueryBuilder :: PARAM_INT_ARRAY )));
2020-07-27 10:14:15 -04:00
2023-02-10 06:17:27 -05:00
$result = $query -> executeQuery ();
2023-09-27 12:36:45 -04:00
$calendarObjects = [];
while (( $array = $result -> fetch ()) !== false ) {
2023-02-10 06:17:27 -05:00
$array [ 'calendarid' ] = ( int ) $array [ 'calendarid' ];
$array [ 'calendartype' ] = ( int ) $array [ 'calendartype' ];
$array [ 'calendardata' ] = $this -> readBlob ( $array [ 'calendardata' ]);
2020-07-27 10:14:15 -04:00
2023-09-27 12:36:45 -04:00
$calendarObjects [] = $array ;
}
$result -> closeCursor ();
return $calendarObjects ;
2023-02-10 06:17:27 -05:00
}, $this -> db );
2020-07-27 10:14:15 -04:00
}
2015-10-30 20:28:21 -04:00
/**
* Searches through all of a users calendars and calendar objects to find
* an object with a specific UID .
*
* This method should return the path to this object , relative to the
* calendar home , so this path usually only contains two parts :
*
* calendarpath / objectpath . ics
*
* If the uid is not found , return null .
*
* This method should only consider * objects that the principal owns , so
* any calendars owned by other principals that also appear in this
* collection should be ignored .
*
* @ param string $principalUri
* @ param string $uid
* @ return string | null
*/
2025-04-03 20:27:49 -04:00
public function getCalendarObjectByUID ( $principalUri , $uid , $calendarUri = null ) {
2015-10-30 20:28:21 -04:00
$query = $this -> db -> getQueryBuilder ();
2016-03-10 04:24:08 -05:00
$query -> selectAlias ( 'c.uri' , 'calendaruri' ) -> selectAlias ( 'co.uri' , 'objecturi' )
2015-10-30 20:28:21 -04:00
-> from ( 'calendarobjects' , 'co' )
2016-03-10 04:24:08 -05:00
-> leftJoin ( 'co' , 'calendars' , 'c' , $query -> expr () -> eq ( 'co.calendarid' , 'c.id' ))
2015-10-30 20:28:21 -04:00
-> where ( $query -> expr () -> eq ( 'c.principaluri' , $query -> createNamedParameter ( $principalUri )))
2021-03-12 05:20:04 -05:00
-> andWhere ( $query -> expr () -> eq ( 'co.uid' , $query -> createNamedParameter ( $uid )))
-> andWhere ( $query -> expr () -> isNull ( 'co.deleted_at' ));
2025-04-03 20:27:49 -04:00
if ( $calendarUri !== null ) {
$query -> andWhere ( $query -> expr () -> eq ( 'c.uri' , $query -> createNamedParameter ( $calendarUri )));
}
2021-04-23 05:43:16 -04:00
$stmt = $query -> executeQuery ();
2021-04-23 05:47:39 -04:00
$row = $stmt -> fetch ();
$stmt -> closeCursor ();
if ( $row ) {
2015-10-30 20:28:21 -04:00
return $row [ 'calendaruri' ] . '/' . $row [ 'objecturi' ];
}
return null ;
}
2021-03-12 05:20:04 -05:00
public function getCalendarObjectById ( string $principalUri , int $id ) : ? array {
$query = $this -> db -> getQueryBuilder ();
$query -> select ([ 'co.id' , 'co.uri' , 'co.lastmodified' , 'co.etag' , 'co.calendarid' , 'co.size' , 'co.calendardata' , 'co.componenttype' , 'co.classification' , 'co.deleted_at' ])
2021-05-31 12:57:40 -04:00
-> selectAlias ( 'c.uri' , 'calendaruri' )
2021-03-12 05:20:04 -05:00
-> from ( 'calendarobjects' , 'co' )
-> join ( 'co' , 'calendars' , 'c' , $query -> expr () -> eq ( 'c.id' , 'co.calendarid' , IQueryBuilder :: PARAM_INT ))
-> where ( $query -> expr () -> eq ( 'c.principaluri' , $query -> createNamedParameter ( $principalUri )))
-> andWhere ( $query -> expr () -> eq ( 'co.id' , $query -> createNamedParameter ( $id , IQueryBuilder :: PARAM_INT ), IQueryBuilder :: PARAM_INT ));
$stmt = $query -> executeQuery ();
$row = $stmt -> fetch ();
$stmt -> closeCursor ();
if ( ! $row ) {
return null ;
}
return [
'id' => $row [ 'id' ],
2021-05-31 15:01:54 -04:00
'uri' => $row [ 'uri' ],
2021-03-12 05:20:04 -05:00
'lastmodified' => $row [ 'lastmodified' ],
'etag' => '"' . $row [ 'etag' ] . '"' ,
'calendarid' => $row [ 'calendarid' ],
2021-05-31 12:57:40 -04:00
'calendaruri' => $row [ 'calendaruri' ],
2021-03-12 05:20:04 -05:00
'size' => ( int ) $row [ 'size' ],
'calendardata' => $this -> readBlob ( $row [ 'calendardata' ]),
'component' => strtolower ( $row [ 'componenttype' ]),
'classification' => ( int ) $row [ 'classification' ],
'deleted_at' => isset ( $row [ 'deleted_at' ]) ? (( int ) $row [ 'deleted_at' ]) : null ,
];
}
2015-10-30 20:28:21 -04:00
/**
* The getChanges method returns all the changes that have happened , since
* the specified syncToken in the specified calendar .
*
* This function should return an array , such as the following :
*
* [
* 'syncToken' => 'The current synctoken' ,
* 'added' => [
* 'new.txt' ,
* ],
* 'modified' => [
* 'modified.txt' ,
* ],
* 'deleted' => [
* 'foo.php.bak' ,
* 'old.txt'
* ]
* );
*
* The returned syncToken property should reflect the * current * syncToken
* of the calendar , as reported in the { http :// sabredav . org / ns } sync - token
* property This is * needed here too , to ensure the operation is atomic .
*
* If the $syncToken argument is specified as null , this is an initial
* sync , and all members should be reported .
*
* The modified property is an array of nodenames that have changed since
* the last token .
*
* The deleted property is an array with nodenames , that have been deleted
* from collection .
*
* The $syncLevel argument is basically the 'depth' of the report . If it ' s
* 1 , you only have to report changes that happened only directly in
* immediate descendants . If it ' s 2 , it should also include changes from
* the nodes below the child collections . ( grandchildren )
*
* The $limit argument allows a client to specify how many results should
* be returned at most . If the limit is not specified , it should be treated
* as infinite .
*
* If the limit ( infinite or not ) is higher than you ' re willing to return ,
* you should throw a Sabre\DAV\Exception\TooMuchMatches () exception .
*
* If the syncToken is expired ( due to data cleanup ) or unknown , you must
* return null .
*
* The limit is 'suggestive' . You are free to ignore it .
*
* @ param string $calendarId
* @ param string $syncToken
* @ param int $syncLevel
2021-02-11 15:42:23 -05:00
* @ param int | null $limit
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2024-04-02 11:13:35 -04:00
* @ return ? array
2015-10-30 20:28:21 -04:00
*/
2020-10-05 09:12:57 -04:00
public function getChangesForCalendar ( $calendarId , $syncToken , $syncLevel , $limit = null , $calendarType = self :: CALENDAR_TYPE_CALENDAR ) {
2023-07-24 07:49:40 -04:00
$table = $calendarType === self :: CALENDAR_TYPE_CALENDAR ? 'calendars' : 'calendarsubscriptions' ;
return $this -> atomic ( function () use ( $calendarId , $syncToken , $syncLevel , $limit , $calendarType , $table ) {
2023-02-10 06:17:27 -05:00
// Current synctoken
2021-02-11 15:42:23 -05:00
$qb = $this -> db -> getQueryBuilder ();
2023-02-10 06:17:27 -05:00
$qb -> select ( 'synctoken' )
2023-07-24 07:49:40 -04:00
-> from ( $table )
2021-02-11 15:42:23 -05:00
-> where (
2023-02-10 06:17:27 -05:00
$qb -> expr () -> eq ( 'id' , $qb -> createNamedParameter ( $calendarId ))
);
2021-04-23 05:43:16 -04:00
$stmt = $qb -> executeQuery ();
2023-02-10 06:17:27 -05:00
$currentToken = $stmt -> fetchOne ();
2024-07-18 20:25:41 -04:00
$initialSync = ! is_numeric ( $syncToken );
2015-10-30 20:28:21 -04:00
2023-02-10 06:17:27 -05:00
if ( $currentToken === false ) {
return null ;
2015-10-30 20:28:21 -04:00
}
2024-07-18 20:25:41 -04:00
// evaluate if this is a initial sync and construct appropriate command
if ( $initialSync ) {
2023-02-10 06:17:27 -05:00
$qb = $this -> db -> getQueryBuilder ();
$qb -> select ( 'uri' )
-> from ( 'calendarobjects' )
2024-07-18 20:25:41 -04:00
-> where ( $qb -> expr () -> eq ( 'calendarid' , $qb -> createNamedParameter ( $calendarId )))
-> andWhere ( $qb -> expr () -> eq ( 'calendartype' , $qb -> createNamedParameter ( $calendarType )))
-> andWhere ( $qb -> expr () -> isNull ( 'deleted_at' ));
} else {
$qb = $this -> db -> getQueryBuilder ();
$qb -> select ( 'uri' , $qb -> func () -> max ( 'operation' ))
-> from ( 'calendarchanges' )
-> where ( $qb -> expr () -> eq ( 'calendarid' , $qb -> createNamedParameter ( $calendarId )))
-> andWhere ( $qb -> expr () -> eq ( 'calendartype' , $qb -> createNamedParameter ( $calendarType )))
-> andWhere ( $qb -> expr () -> gte ( 'synctoken' , $qb -> createNamedParameter ( $syncToken )))
-> andWhere ( $qb -> expr () -> lt ( 'synctoken' , $qb -> createNamedParameter ( $currentToken )))
-> groupBy ( 'uri' );
}
// evaluate if limit exists
if ( is_numeric ( $limit )) {
$qb -> setMaxResults ( $limit );
}
// execute command
$stmt = $qb -> executeQuery ();
// build results
$result = [ 'syncToken' => $currentToken , 'added' => [], 'modified' => [], 'deleted' => []];
// retrieve results
if ( $initialSync ) {
2023-02-10 06:17:27 -05:00
$result [ 'added' ] = $stmt -> fetchAll ( \PDO :: FETCH_COLUMN );
2024-07-18 20:25:41 -04:00
} else {
// \PDO::FETCH_NUM is needed due to the inconsistent field names
// produced by doctrine for MAX() with different databases
while ( $entry = $stmt -> fetch ( \PDO :: FETCH_NUM )) {
// assign uri (column 0) to appropriate mutation based on operation (column 1)
// forced (int) is needed as doctrine with OCI returns the operation field as string not integer
match (( int ) $entry [ 1 ]) {
1 => $result [ 'added' ][] = $entry [ 0 ],
2 => $result [ 'modified' ][] = $entry [ 0 ],
3 => $result [ 'deleted' ][] = $entry [ 0 ],
default => $this -> logger -> debug ( 'Unknown calendar change operation detected' )
};
}
2015-10-30 20:28:21 -04:00
}
2024-07-18 20:25:41 -04:00
$stmt -> closeCursor ();
2023-02-10 06:17:27 -05:00
return $result ;
}, $this -> db );
2015-10-30 20:28:21 -04:00
}
/**
* Returns a list of subscriptions for a principal .
*
* Every subscription is an array with the following keys :
* * id , a unique id that will be used by other functions to modify the
* subscription . This can be the same as the uri or a database key .
* * uri . This is just the 'base uri' or 'filename' of the subscription .
* * principaluri . The owner of the subscription . Almost always the same as
* principalUri passed to this method .
*
* Furthermore , all the subscription info must be returned too :
*
* 1. { DAV : } displayname
* 2. { http :// apple . com / ns / ical / } refreshrate
* 3. { http :// calendarserver . org / ns / } subscribed - strip - todos ( omit if todos
* should not be stripped ) .
* 4. { http :// calendarserver . org / ns / } subscribed - strip - alarms ( omit if alarms
* should not be stripped ) .
* 5. { http :// calendarserver . org / ns / } subscribed - strip - attachments ( omit if
* attachments should not be stripped ) .
* 6. { http :// calendarserver . org / ns / } source ( Must be a
* Sabre\DAV\Property\Href ) .
* 7. { http :// apple . com / ns / ical / } calendar - color
* 8. { http :// apple . com / ns / ical / } calendar - order
* 9. { urn : ietf : params : xml : ns : caldav } supported - calendar - component - set
* ( should just be an instance of
* Sabre\CalDAV\Property\SupportedCalendarComponentSet , with a bunch of
* default components ) .
*
* @ param string $principalUri
* @ return array
*/
2020-04-10 10:51:06 -04:00
public function getSubscriptionsForUser ( $principalUri ) {
2021-12-29 09:26:49 -05:00
$fields = array_column ( $this -> subscriptionPropertyMap , 0 );
2015-10-30 20:28:21 -04:00
$fields [] = 'id' ;
$fields [] = 'uri' ;
$fields [] = 'source' ;
$fields [] = 'principaluri' ;
$fields [] = 'lastmodified' ;
2018-06-28 07:07:33 -04:00
$fields [] = 'synctoken' ;
2015-10-30 20:28:21 -04:00
$query = $this -> db -> getQueryBuilder ();
$query -> select ( $fields )
-> from ( 'calendarsubscriptions' )
-> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )))
-> orderBy ( 'calendarorder' , 'asc' );
2021-04-23 05:43:16 -04:00
$stmt = $query -> executeQuery ();
2015-10-30 20:28:21 -04:00
$subscriptions = [];
2021-04-23 05:37:22 -04:00
while ( $row = $stmt -> fetch ()) {
2015-10-30 20:28:21 -04:00
$subscription = [
2020-10-05 09:12:57 -04:00
'id' => $row [ 'id' ],
'uri' => $row [ 'uri' ],
2015-10-30 20:28:21 -04:00
'principaluri' => $row [ 'principaluri' ],
2020-10-05 09:12:57 -04:00
'source' => $row [ 'source' ],
2015-10-30 20:28:21 -04:00
'lastmodified' => $row [ 'lastmodified' ],
'{' . Plugin :: NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet ([ 'VTODO' , 'VEVENT' ]),
2024-09-15 15:01:34 -04:00
'{http://sabredav.org/ns}sync-token' => $row [ 'synctoken' ] ? : '0' ,
2015-10-30 20:28:21 -04:00
];
2021-12-29 09:26:49 -05:00
$subscriptions [] = $this -> rowToSubscription ( $row , $subscription );
2015-10-30 20:28:21 -04:00
}
return $subscriptions ;
}
/**
* Creates a new subscription for a principal .
*
* If the creation was a success , an id must be returned that can be used to reference
* this subscription in other methods , such as updateSubscription .
*
* @ param string $principalUri
* @ param string $uri
* @ param array $properties
* @ return mixed
*/
2020-04-10 10:51:06 -04:00
public function createSubscription ( $principalUri , $uri , array $properties ) {
2015-10-30 20:28:21 -04:00
if ( ! isset ( $properties [ '{http://calendarserver.org/ns/}source' ])) {
throw new Forbidden ( 'The {http://calendarserver.org/ns/}source property is required when creating subscriptions' );
}
$values = [
'principaluri' => $principalUri ,
2020-10-05 09:12:57 -04:00
'uri' => $uri ,
'source' => $properties [ '{http://calendarserver.org/ns/}source' ] -> getHref (),
2015-10-30 20:28:21 -04:00
'lastmodified' => time (),
];
2016-07-01 07:42:35 -04:00
$propertiesBoolean = [ 'striptodos' , 'stripalarms' , 'stripattachments' ];
2015-10-30 20:28:21 -04:00
2021-12-29 09:26:49 -05:00
foreach ( $this -> subscriptionPropertyMap as $xmlName => [ $dbName , $type ]) {
2016-07-01 07:42:35 -04:00
if ( array_key_exists ( $xmlName , $properties )) {
$values [ $dbName ] = $properties [ $xmlName ];
if ( in_array ( $dbName , $propertiesBoolean )) {
$values [ $dbName ] = true ;
}
2015-10-30 20:28:21 -04:00
}
}
2023-01-23 12:23:52 -05:00
[ $subscriptionId , $subscriptionRow ] = $this -> atomic ( function () use ( $values ) {
2022-10-03 14:03:29 -04:00
$valuesToInsert = [];
$query = $this -> db -> getQueryBuilder ();
foreach ( array_keys ( $values ) as $name ) {
$valuesToInsert [ $name ] = $query -> createNamedParameter ( $values [ $name ]);
}
$query -> insert ( 'calendarsubscriptions' )
-> values ( $valuesToInsert )
-> executeStatement ();
2016-07-01 07:42:35 -04:00
2022-10-03 14:03:29 -04:00
$subscriptionId = $query -> getLastInsertId ();
2015-10-30 20:28:21 -04:00
2022-10-03 14:03:29 -04:00
$subscriptionRow = $this -> getSubscriptionById ( $subscriptionId );
return [ $subscriptionId , $subscriptionRow ];
}, $this -> db );
2018-06-28 07:07:33 -04:00
2021-02-15 14:50:28 -05:00
$this -> dispatcher -> dispatchTyped ( new SubscriptionCreatedEvent ( $subscriptionId , $subscriptionRow ));
2018-06-28 07:07:33 -04:00
return $subscriptionId ;
2015-10-30 20:28:21 -04:00
}
/**
* Updates a subscription
*
* The list of mutations is stored in a Sabre\DAV\PropPatch object .
* To do the actual updates , you must tell this object which properties
* you ' re going to process with the handle () method .
*
* Calling the handle method is like telling the PropPatch object " I
* promise I can handle updating this property " .
*
* Read the PropPatch documentation for more info and examples .
*
* @ param mixed $subscriptionId
2016-04-19 05:33:37 -04:00
* @ param PropPatch $propPatch
2015-10-30 20:28:21 -04:00
* @ return void
*/
2020-04-10 10:51:06 -04:00
public function updateSubscription ( $subscriptionId , PropPatch $propPatch ) {
2024-02-29 03:42:51 -05:00
$supportedProperties = array_keys ( $this -> subscriptionPropertyMap );
$supportedProperties [] = '{http://calendarserver.org/ns/}source' ;
$propPatch -> handle ( $supportedProperties , function ( $mutations ) use ( $subscriptionId ) {
$newValues = [];
foreach ( $mutations as $propertyName => $propertyValue ) {
if ( $propertyName === '{http://calendarserver.org/ns/}source' ) {
$newValues [ 'source' ] = $propertyValue -> getHref ();
} else {
$fieldName = $this -> subscriptionPropertyMap [ $propertyName ][ 0 ];
$newValues [ $fieldName ] = $propertyValue ;
2015-10-30 20:28:21 -04:00
}
2024-02-29 03:42:51 -05:00
}
2015-10-30 20:28:21 -04:00
2024-02-29 03:42:51 -05:00
$subscriptionRow = $this -> atomic ( function () use ( $subscriptionId , $newValues ) {
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> update ( 'calendarsubscriptions' )
-> set ( 'lastmodified' , $query -> createNamedParameter ( time ()));
foreach ( $newValues as $fieldName => $value ) {
$query -> set ( $fieldName , $query -> createNamedParameter ( $value ));
}
$query -> where ( $query -> expr () -> eq ( 'id' , $query -> createNamedParameter ( $subscriptionId )))
-> executeStatement ();
2015-10-30 20:28:21 -04:00
2024-02-29 03:42:51 -05:00
return $this -> getSubscriptionById ( $subscriptionId );
}, $this -> db );
2018-06-28 07:07:33 -04:00
2024-02-29 03:42:51 -05:00
$this -> dispatcher -> dispatchTyped ( new SubscriptionUpdatedEvent (( int ) $subscriptionId , $subscriptionRow , [], $mutations ));
return true ;
});
2015-10-30 20:28:21 -04:00
}
/**
* Deletes a subscription .
*
* @ param mixed $subscriptionId
* @ return void
*/
2020-04-10 10:51:06 -04:00
public function deleteSubscription ( $subscriptionId ) {
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $subscriptionId ) : void {
2023-02-10 06:17:27 -05:00
$subscriptionRow = $this -> getSubscriptionById ( $subscriptionId );
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> delete ( 'calendarsubscriptions' )
-> where ( $query -> expr () -> eq ( 'id' , $query -> createNamedParameter ( $subscriptionId )))
-> executeStatement ();
2018-06-28 07:07:33 -04:00
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> delete ( 'calendarobjects' )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $subscriptionId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )))
-> executeStatement ();
2018-06-28 07:07:33 -04:00
2023-02-10 06:17:27 -05:00
$query -> delete ( 'calendarchanges' )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $subscriptionId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )))
-> executeStatement ();
2018-06-28 07:07:33 -04:00
2023-02-10 06:17:27 -05:00
$query -> delete ( $this -> dbObjectPropertiesTable )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $subscriptionId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )))
-> executeStatement ();
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
if ( $subscriptionRow ) {
$this -> dispatcher -> dispatchTyped ( new SubscriptionDeletedEvent (( int ) $subscriptionId , $subscriptionRow , []));
}
}, $this -> db );
2015-10-30 20:28:21 -04:00
}
/**
* Returns a single scheduling object for the inbox collection .
*
* The returned array should contain the following elements :
* * uri - A unique basename for the object . This will be used to
* construct a full uri .
* * calendardata - The iCalendar object
* * lastmodified - The last modification date . Can be an int for a unix
* timestamp , or a PHP DateTime object .
* * etag - A unique token that must change if the object changed .
* * size - The size of the object , in bytes .
*
* @ param string $principalUri
* @ param string $objectUri
* @ return array
*/
2020-04-10 10:51:06 -04:00
public function getSchedulingObject ( $principalUri , $objectUri ) {
2015-11-16 09:49:46 -05:00
$query = $this -> db -> getQueryBuilder ();
$stmt = $query -> select ([ 'uri' , 'calendardata' , 'lastmodified' , 'etag' , 'size' ])
-> from ( 'schedulingobjects' )
-> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )))
-> andWhere ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $objectUri )))
2021-04-23 05:43:16 -04:00
-> executeQuery ();
2015-11-16 09:49:46 -05:00
2021-04-23 05:37:22 -04:00
$row = $stmt -> fetch ();
2015-11-16 09:49:46 -05:00
if ( ! $row ) {
return null ;
}
return [
2020-10-05 09:12:57 -04:00
'uri' => $row [ 'uri' ],
2020-04-09 03:22:29 -04:00
'calendardata' => $row [ 'calendardata' ],
'lastmodified' => $row [ 'lastmodified' ],
2020-10-05 09:12:57 -04:00
'etag' => '"' . $row [ 'etag' ] . '"' ,
'size' => ( int ) $row [ 'size' ],
2015-11-16 09:49:46 -05:00
];
2015-10-30 20:28:21 -04:00
}
/**
* Returns all scheduling objects for the inbox collection .
*
* These objects should be returned as an array . Every item in the array
* should follow the same structure as returned from getSchedulingObject .
*
* The main difference is that 'calendardata' is optional .
*
* @ param string $principalUri
* @ return array
*/
2020-04-10 10:51:06 -04:00
public function getSchedulingObjects ( $principalUri ) {
2015-11-16 09:49:46 -05:00
$query = $this -> db -> getQueryBuilder ();
$stmt = $query -> select ([ 'uri' , 'calendardata' , 'lastmodified' , 'etag' , 'size' ])
-> from ( 'schedulingobjects' )
-> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )))
2021-04-23 05:43:16 -04:00
-> executeQuery ();
2015-11-16 09:49:46 -05:00
2023-09-27 12:36:45 -04:00
$results = [];
while (( $row = $stmt -> fetch ()) !== false ) {
$results [] = [
2020-04-09 03:22:29 -04:00
'calendardata' => $row [ 'calendardata' ],
2020-10-05 09:12:57 -04:00
'uri' => $row [ 'uri' ],
2020-04-09 03:22:29 -04:00
'lastmodified' => $row [ 'lastmodified' ],
2020-10-05 09:12:57 -04:00
'etag' => '"' . $row [ 'etag' ] . '"' ,
'size' => ( int ) $row [ 'size' ],
2015-11-16 09:49:46 -05:00
];
}
2021-12-02 08:21:38 -05:00
$stmt -> closeCursor ();
2015-11-16 09:49:46 -05:00
2023-09-27 12:36:45 -04:00
return $results ;
2015-10-30 20:28:21 -04:00
}
/**
* Deletes a scheduling object from the inbox collection .
*
* @ param string $principalUri
* @ param string $objectUri
* @ return void
*/
2020-04-10 10:51:06 -04:00
public function deleteSchedulingObject ( $principalUri , $objectUri ) {
2023-07-25 12:09:11 -04:00
$this -> cachedObjects = [];
2015-11-16 09:49:46 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> delete ( 'schedulingobjects' )
-> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $principalUri )))
-> andWhere ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $objectUri )))
2021-05-05 04:35:25 -04:00
-> executeStatement ();
2015-10-30 20:28:21 -04:00
}
2024-05-08 14:38:51 -04:00
/**
* Deletes all scheduling objects last modified before $modifiedBefore from the inbox collection .
*
* @ param int $modifiedBefore
* @ param int $limit
* @ return void
*/
public function deleteOutdatedSchedulingObjects ( int $modifiedBefore , int $limit ) : void {
$query = $this -> db -> getQueryBuilder ();
$query -> select ( 'id' )
-> from ( 'schedulingobjects' )
-> where ( $query -> expr () -> lt ( 'lastmodified' , $query -> createNamedParameter ( $modifiedBefore )))
-> setMaxResults ( $limit );
$result = $query -> executeQuery ();
$count = $result -> rowCount ();
if ( $count === 0 ) {
return ;
}
$ids = array_map ( static function ( array $id ) {
return ( int ) $id [ 0 ];
}, $result -> fetchAll ( \PDO :: FETCH_NUM ));
$result -> closeCursor ();
$numDeleted = 0 ;
$deleteQuery = $this -> db -> getQueryBuilder ();
$deleteQuery -> delete ( 'schedulingobjects' )
-> where ( $deleteQuery -> expr () -> in ( 'id' , $deleteQuery -> createParameter ( 'ids' ), IQueryBuilder :: PARAM_INT_ARRAY ));
foreach ( array_chunk ( $ids , 1000 ) as $chunk ) {
$deleteQuery -> setParameter ( 'ids' , $chunk , IQueryBuilder :: PARAM_INT_ARRAY );
$numDeleted += $deleteQuery -> executeStatement ();
}
if ( $numDeleted === $limit ) {
$this -> logger -> info ( " Deleted $limit scheduling objects, continuing with next batch " );
$this -> deleteOutdatedSchedulingObjects ( $modifiedBefore , $limit );
}
}
2015-10-30 20:28:21 -04:00
/**
* Creates a new scheduling object . This should land in a users ' inbox .
*
* @ param string $principalUri
* @ param string $objectUri
* @ param string $objectData
* @ return void
*/
2020-04-10 10:51:06 -04:00
public function createSchedulingObject ( $principalUri , $objectUri , $objectData ) {
2023-07-25 12:09:11 -04:00
$this -> cachedObjects = [];
2015-11-16 09:49:46 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> insert ( 'schedulingobjects' )
-> values ([
'principaluri' => $query -> createNamedParameter ( $principalUri ),
2018-02-21 04:18:38 -05:00
'calendardata' => $query -> createNamedParameter ( $objectData , IQueryBuilder :: PARAM_LOB ),
2015-11-16 09:49:46 -05:00
'uri' => $query -> createNamedParameter ( $objectUri ),
'lastmodified' => $query -> createNamedParameter ( time ()),
'etag' => $query -> createNamedParameter ( md5 ( $objectData )),
'size' => $query -> createNamedParameter ( strlen ( $objectData ))
])
2021-05-05 04:35:25 -04:00
-> executeStatement ();
2015-10-30 20:28:21 -04:00
}
/**
* Adds a change record to the calendarchanges table .
*
* @ param mixed $calendarId
2024-03-11 11:27:23 -04:00
* @ param string [] $objectUris
2015-10-30 20:28:21 -04:00
* @ param int $operation 1 = add , 2 = modify , 3 = delete .
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2015-10-30 20:28:21 -04:00
* @ return void
*/
2024-03-11 11:27:23 -04:00
protected function addChanges ( int $calendarId , array $objectUris , int $operation , int $calendarType = self :: CALENDAR_TYPE_CALENDAR ) : void {
2023-07-25 12:09:11 -04:00
$this -> cachedObjects = [];
2023-03-20 12:45:50 -04:00
$table = $calendarType === self :: CALENDAR_TYPE_CALENDAR ? 'calendars' : 'calendarsubscriptions' ;
2015-10-30 20:28:21 -04:00
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $calendarId , $objectUris , $operation , $calendarType , $table ) : void {
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> select ( 'synctoken' )
-> from ( $table )
-> where ( $query -> expr () -> eq ( 'id' , $query -> createNamedParameter ( $calendarId )));
$result = $query -> executeQuery ();
$syncToken = ( int ) $result -> fetchOne ();
$result -> closeCursor ();
2018-06-28 07:07:33 -04:00
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> insert ( 'calendarchanges' )
-> values ([
2024-03-11 11:27:23 -04:00
'uri' => $query -> createParameter ( 'uri' ),
2023-02-10 06:17:27 -05:00
'synctoken' => $query -> createNamedParameter ( $syncToken ),
'calendarid' => $query -> createNamedParameter ( $calendarId ),
'operation' => $query -> createNamedParameter ( $operation ),
'calendartype' => $query -> createNamedParameter ( $calendarType ),
2025-06-20 10:24:24 -04:00
'created_at' => $query -> createNamedParameter ( time ()),
2024-03-11 11:27:23 -04:00
]);
foreach ( $objectUris as $uri ) {
$query -> setParameter ( 'uri' , $uri );
$query -> executeStatement ();
}
2018-06-28 07:07:33 -04:00
2023-03-20 12:45:50 -04:00
$query = $this -> db -> getQueryBuilder ();
$query -> update ( $table )
-> set ( 'synctoken' , $query -> createNamedParameter ( $syncToken + 1 , IQueryBuilder :: PARAM_INT ))
-> where ( $query -> expr () -> eq ( 'id' , $query -> createNamedParameter ( $calendarId )))
-> executeStatement ();
2023-02-10 06:17:27 -05:00
}, $this -> db );
2015-10-30 20:28:21 -04:00
}
2024-03-11 11:27:23 -04:00
public function restoreChanges ( int $calendarId , int $calendarType = self :: CALENDAR_TYPE_CALENDAR ) : void {
$this -> cachedObjects = [];
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $calendarId , $calendarType ) : void {
2024-03-11 11:27:23 -04:00
$qbAdded = $this -> db -> getQueryBuilder ();
$qbAdded -> select ( 'uri' )
-> from ( 'calendarobjects' )
-> where (
$qbAdded -> expr () -> andX (
$qbAdded -> expr () -> eq ( 'calendarid' , $qbAdded -> createNamedParameter ( $calendarId )),
$qbAdded -> expr () -> eq ( 'calendartype' , $qbAdded -> createNamedParameter ( $calendarType )),
$qbAdded -> expr () -> isNull ( 'deleted_at' ),
)
);
$resultAdded = $qbAdded -> executeQuery ();
$addedUris = $resultAdded -> fetchAll ( \PDO :: FETCH_COLUMN );
$resultAdded -> closeCursor ();
// Track everything as changed
// Tracking the creation is not necessary because \OCA\DAV\CalDAV\CalDavBackend::getChangesForCalendar
// only returns the last change per object.
$this -> addChanges ( $calendarId , $addedUris , 2 , $calendarType );
$qbDeleted = $this -> db -> getQueryBuilder ();
$qbDeleted -> select ( 'uri' )
-> from ( 'calendarobjects' )
-> where (
$qbDeleted -> expr () -> andX (
$qbDeleted -> expr () -> eq ( 'calendarid' , $qbDeleted -> createNamedParameter ( $calendarId )),
$qbDeleted -> expr () -> eq ( 'calendartype' , $qbDeleted -> createNamedParameter ( $calendarType )),
$qbDeleted -> expr () -> isNotNull ( 'deleted_at' ),
)
);
$resultDeleted = $qbDeleted -> executeQuery ();
$deletedUris = array_map ( function ( string $uri ) {
return str_replace ( '-deleted.ics' , '.ics' , $uri );
}, $resultDeleted -> fetchAll ( \PDO :: FETCH_COLUMN ));
$resultDeleted -> closeCursor ();
$this -> addChanges ( $calendarId , $deletedUris , 3 , $calendarType );
}, $this -> db );
}
2015-10-30 20:28:21 -04:00
/**
* Parses some information from calendar objects , used for optimized
* calendar - queries .
*
* Returns an array with the following keys :
* * etag - An md5 checksum of the object without the quotes .
* * size - Size of the object in bytes
* * componentType - VEVENT , VTODO or VJOURNAL
* * firstOccurence
* * lastOccurence
* * uid - value of the UID property
*
* @ param string $calendarData
* @ return array
*/
2023-06-23 03:09:38 -04:00
public function getDenormalizedData ( string $calendarData ) : array {
2015-10-30 20:28:21 -04:00
$vObject = Reader :: read ( $calendarData );
2020-09-27 17:05:15 -04:00
$vEvents = [];
2015-10-30 20:28:21 -04:00
$componentType = null ;
$component = null ;
2016-04-19 05:33:37 -04:00
$firstOccurrence = null ;
$lastOccurrence = null ;
2015-10-30 20:28:21 -04:00
$uid = null ;
2016-04-19 05:33:37 -04:00
$classification = self :: CLASSIFICATION_PUBLIC ;
2020-09-27 17:05:15 -04:00
$hasDTSTART = false ;
2015-10-30 20:28:21 -04:00
foreach ( $vObject -> getComponents () as $component ) {
2020-10-29 10:31:56 -04:00
if ( $component -> name !== 'VTIMEZONE' ) {
2020-09-27 17:05:15 -04:00
// Finding all VEVENTs, and track them
if ( $component -> name === 'VEVENT' ) {
2023-06-23 03:09:38 -04:00
$vEvents [] = $component ;
2020-09-27 17:05:15 -04:00
if ( $component -> DTSTART ) {
$hasDTSTART = true ;
}
}
// Track first component type and uid
if ( $uid === null ) {
$componentType = $component -> name ;
$uid = ( string ) $component -> UID ;
}
2015-10-30 20:28:21 -04:00
}
}
if ( ! $componentType ) {
2021-03-12 05:20:04 -05:00
throw new BadRequest ( 'Calendar objects must have a VJOURNAL, VEVENT or VTODO component' );
2015-10-30 20:28:21 -04:00
}
2020-09-27 17:05:15 -04:00
2020-10-29 10:31:56 -04:00
if ( $hasDTSTART ) {
2020-09-27 17:05:15 -04:00
$component = $vEvents [ 0 ];
2016-01-29 12:25:27 -05:00
// Finding the last occurrence is a bit harder
2020-09-27 17:05:15 -04:00
if ( ! isset ( $component -> RRULE ) && count ( $vEvents ) === 1 ) {
$firstOccurrence = $component -> DTSTART -> getDateTime () -> getTimeStamp ();
2015-10-30 20:28:21 -04:00
if ( isset ( $component -> DTEND )) {
2016-04-19 05:33:37 -04:00
$lastOccurrence = $component -> DTEND -> getDateTime () -> getTimeStamp ();
2015-10-30 20:28:21 -04:00
} elseif ( isset ( $component -> DURATION )) {
$endDate = clone $component -> DTSTART -> getDateTime ();
$endDate -> add ( DateTimeParser :: parse ( $component -> DURATION -> getValue ()));
2016-04-19 05:33:37 -04:00
$lastOccurrence = $endDate -> getTimeStamp ();
2015-10-30 20:28:21 -04:00
} elseif ( ! $component -> DTSTART -> hasTime ()) {
$endDate = clone $component -> DTSTART -> getDateTime ();
$endDate -> modify ( '+1 day' );
2016-04-19 05:33:37 -04:00
$lastOccurrence = $endDate -> getTimeStamp ();
2015-10-30 20:28:21 -04:00
} else {
2016-05-31 05:13:45 -04:00
$lastOccurrence = $firstOccurrence ;
2015-10-30 20:28:21 -04:00
}
} else {
2024-07-17 15:26:05 -04:00
try {
$it = new EventIterator ( $vEvents );
} catch ( NoInstancesException $e ) {
$this -> logger -> debug ( 'Caught no instance exception for calendar data. This usually indicates invalid calendar data.' , [
'app' => 'dav' ,
'exception' => $e ,
]);
throw new Forbidden ( $e -> getMessage ());
}
2020-01-05 15:32:33 -05:00
$maxDate = new DateTime ( self :: MAX_DATE );
2020-09-27 17:05:15 -04:00
$firstOccurrence = $it -> getDtStart () -> getTimestamp ();
2015-10-30 20:28:21 -04:00
if ( $it -> isInfinite ()) {
2016-10-19 04:49:43 -04:00
$lastOccurrence = $maxDate -> getTimestamp ();
2015-10-30 20:28:21 -04:00
} else {
$end = $it -> getDtEnd ();
while ( $it -> valid () && $end < $maxDate ) {
$end = $it -> getDtEnd ();
$it -> next ();
}
2016-10-19 04:49:43 -04:00
$lastOccurrence = $end -> getTimestamp ();
2015-10-30 20:28:21 -04:00
}
}
}
2016-04-19 05:33:37 -04:00
if ( $component -> CLASS ) {
$classification = CalDavBackend :: CLASSIFICATION_PRIVATE ;
switch ( $component -> CLASS -> getValue ()) {
case 'PUBLIC' :
$classification = CalDavBackend :: CLASSIFICATION_PUBLIC ;
break ;
case 'CONFIDENTIAL' :
$classification = CalDavBackend :: CLASSIFICATION_CONFIDENTIAL ;
break ;
}
}
2015-10-30 20:28:21 -04:00
return [
2016-04-19 05:33:37 -04:00
'etag' => md5 ( $calendarData ),
'size' => strlen ( $calendarData ),
'componentType' => $componentType ,
'firstOccurence' => is_null ( $firstOccurrence ) ? null : max ( 0 , $firstOccurrence ),
2022-04-01 11:31:37 -04:00
'lastOccurence' => is_null ( $lastOccurrence ) ? null : max ( 0 , $lastOccurrence ),
2016-04-19 05:33:37 -04:00
'uid' => $uid ,
'classification' => $classification
2015-10-30 20:28:21 -04:00
];
}
2018-06-28 07:07:33 -04:00
/**
* @ param $cardData
* @ return bool | string
*/
2015-10-30 20:28:21 -04:00
private function readBlob ( $cardData ) {
if ( is_resource ( $cardData )) {
return stream_get_contents ( $cardData );
}
return $cardData ;
}
2016-01-26 06:06:02 -05:00
2016-01-25 11:18:47 -05:00
/**
2022-06-14 09:26:15 -04:00
* @ param list < array { href : string , commonName : string , readOnly : bool } > $add
* @ param list < string > $remove
2016-01-25 11:18:47 -05:00
*/
2022-06-14 09:26:15 -04:00
public function updateShares ( IShareable $shareable , array $add , array $remove ) : void {
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $shareable , $add , $remove ) : void {
2023-02-10 06:17:27 -05:00
$calendarId = $shareable -> getResourceId ();
$calendarRow = $this -> getCalendarById ( $calendarId );
if ( $calendarRow === null ) {
2023-07-06 04:52:19 -04:00
throw new \RuntimeException ( 'Trying to update shares for non-existing calendar: ' . $calendarId );
2023-02-10 06:17:27 -05:00
}
$oldShares = $this -> getShares ( $calendarId );
2020-07-28 03:35:51 -04:00
2024-01-30 12:35:44 -05:00
$this -> calendarSharingBackend -> updateShares ( $shareable , $add , $remove , $oldShares );
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$this -> dispatcher -> dispatchTyped ( new CalendarShareUpdatedEvent ( $calendarId , $calendarRow , $oldShares , $add , $remove ));
}, $this -> db );
2016-01-26 06:06:02 -05:00
}
2016-01-25 11:18:47 -05:00
/**
2022-06-14 09:26:15 -04:00
* @ return list < array { href : string , commonName : string , status : int , readOnly : bool , '{http://owncloud.org/ns}principal' : string , '{http://owncloud.org/ns}group-share' : bool } >
2016-01-25 11:18:47 -05:00
*/
2022-06-14 09:26:15 -04:00
public function getShares ( int $resourceId ) : array {
2018-06-28 07:07:33 -04:00
return $this -> calendarSharingBackend -> getShares ( $resourceId );
2016-01-26 06:06:02 -05:00
}
2025-05-14 05:21:28 -04:00
public function getSharesByShareePrincipal ( string $principal ) : array {
return $this -> calendarSharingBackend -> getSharesByShareePrincipal ( $principal );
}
2023-07-25 11:08:50 -04:00
public function preloadShares ( array $resourceIds ) : void {
$this -> calendarSharingBackend -> preloadShares ( $resourceIds );
}
2016-07-06 06:19:46 -04:00
/**
2016-08-01 04:51:11 -04:00
* @ param boolean $value
2024-10-10 06:40:31 -04:00
* @ param Calendar $calendar
2016-09-03 04:52:05 -04:00
* @ return string | null
2016-08-01 04:51:11 -04:00
*/
2016-07-31 14:18:35 -04:00
public function setPublishStatus ( $value , $calendar ) {
2025-08-15 07:54:56 -04:00
$publishStatus = $this -> atomic ( function () use ( $value , $calendar ) {
2023-02-10 06:17:27 -05:00
$calendarId = $calendar -> getResourceId ();
$calendarData = $this -> getCalendarById ( $calendarId );
2017-09-20 11:33:51 -04:00
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
if ( $value ) {
$publicUri = $this -> random -> generate ( 16 , ISecureRandom :: CHAR_HUMAN_READABLE );
$query -> insert ( 'dav_shares' )
-> values ([
'principaluri' => $query -> createNamedParameter ( $calendar -> getPrincipalURI ()),
'type' => $query -> createNamedParameter ( 'calendar' ),
'access' => $query -> createNamedParameter ( self :: ACCESS_PUBLIC ),
'resourceid' => $query -> createNamedParameter ( $calendar -> getResourceId ()),
'publicuri' => $query -> createNamedParameter ( $publicUri )
]);
$query -> executeStatement ();
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$this -> dispatcher -> dispatchTyped ( new CalendarPublishedEvent ( $calendarId , $calendarData , $publicUri ));
return $publicUri ;
}
$query -> delete ( 'dav_shares' )
-> where ( $query -> expr () -> eq ( 'resourceid' , $query -> createNamedParameter ( $calendar -> getResourceId ())))
-> andWhere ( $query -> expr () -> eq ( 'access' , $query -> createNamedParameter ( self :: ACCESS_PUBLIC )));
$query -> executeStatement ();
2020-07-28 03:35:51 -04:00
2023-02-10 06:17:27 -05:00
$this -> dispatcher -> dispatchTyped ( new CalendarUnpublishedEvent ( $calendarId , $calendarData ));
return null ;
}, $this -> db );
2025-08-15 07:54:56 -04:00
$this -> publishStatusCache -> set (( string ) $calendar -> getResourceId (), $publishStatus ? ? false );
return $publishStatus ;
2016-07-31 14:18:35 -04:00
}
2016-07-06 06:19:46 -04:00
/**
2024-10-10 06:40:31 -04:00
* @ param Calendar $calendar
2025-08-15 07:54:56 -04:00
* @ return string | false
2016-07-06 06:19:46 -04:00
*/
public function getPublishStatus ( $calendar ) {
2025-08-15 07:54:56 -04:00
$cached = $this -> publishStatusCache -> get (( string ) $calendar -> getResourceId ());
if ( $cached !== null ) {
return $cached ;
}
2016-09-14 05:34:21 -04:00
$query = $this -> db -> getQueryBuilder ();
$result = $query -> select ( 'publicuri' )
-> from ( 'dav_shares' )
-> where ( $query -> expr () -> eq ( 'resourceid' , $query -> createNamedParameter ( $calendar -> getResourceId ())))
-> andWhere ( $query -> expr () -> eq ( 'access' , $query -> createNamedParameter ( self :: ACCESS_PUBLIC )))
2021-04-23 05:43:16 -04:00
-> executeQuery ();
2016-09-14 05:34:21 -04:00
2025-08-15 07:54:56 -04:00
$publishStatus = $result -> fetchOne ();
$result -> closeCursor ();
$this -> publishStatusCache -> set (( string ) $calendar -> getResourceId (), $publishStatus );
return $publishStatus ;
}
/**
* @ param int [] $resourceIds
*/
public function preloadPublishStatuses ( array $resourceIds ) : void {
$query = $this -> db -> getQueryBuilder ();
$result = $query -> select ( 'resourceid' , 'publicuri' )
-> from ( 'dav_shares' )
-> where ( $query -> expr () -> in (
'resourceid' ,
$query -> createNamedParameter ( $resourceIds , IQueryBuilder :: PARAM_INT_ARRAY ),
IQueryBuilder :: PARAM_INT_ARRAY ,
))
-> andWhere ( $query -> expr () -> eq (
'access' ,
$query -> createNamedParameter ( self :: ACCESS_PUBLIC , IQueryBuilder :: PARAM_INT ),
IQueryBuilder :: PARAM_INT ,
))
-> executeQuery ();
$hasPublishStatuses = [];
while ( $row = $result -> fetch ()) {
$this -> publishStatusCache -> set (( string ) $row [ 'resourceid' ], $row [ 'publicuri' ]);
$hasPublishStatuses [( int ) $row [ 'resourceid' ]] = true ;
}
// Also remember resources with no publish status
foreach ( $resourceIds as $resourceId ) {
if ( ! isset ( $hasPublishStatuses [ $resourceId ])) {
$this -> publishStatusCache -> set (( string ) $resourceId , false );
}
}
2016-09-14 05:34:21 -04:00
$result -> closeCursor ();
}
2016-01-25 11:18:47 -05:00
/**
* @ param int $resourceId
2022-06-14 09:26:15 -04:00
* @ param list < array { privilege : string , principal : string , protected : bool } > $acl
* @ return list < array { privilege : string , principal : string , protected : bool } >
2016-01-25 11:18:47 -05:00
*/
2022-06-14 09:26:15 -04:00
public function applyShareAcl ( int $resourceId , array $acl ) : array {
2024-01-30 12:35:44 -05:00
$shares = $this -> calendarSharingBackend -> getShares ( $resourceId );
return $this -> calendarSharingBackend -> applyShareAcl ( $shares , $acl );
2016-01-26 06:06:02 -05:00
}
2016-02-03 09:43:45 -05:00
2017-03-25 06:56:40 -04:00
/**
* update properties table
*
* @ param int $calendarId
* @ param string $objectUri
* @ param string $calendarData
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2017-03-25 06:56:40 -04:00
*/
2020-10-05 09:12:57 -04:00
public function updateProperties ( $calendarId , $objectUri , $calendarData , $calendarType = self :: CALENDAR_TYPE_CALENDAR ) {
2023-07-25 12:09:11 -04:00
$this -> cachedObjects = [];
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $calendarId , $objectUri , $calendarData , $calendarType ) : void {
2023-02-10 06:17:27 -05:00
$objectId = $this -> getCalendarObjectId ( $calendarId , $objectUri , $calendarType );
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
try {
$vCalendar = $this -> readCalendarData ( $calendarData );
} catch ( \Exception $ex ) {
return ;
2017-03-25 06:56:40 -04:00
}
2023-02-10 06:17:27 -05:00
$this -> purgeProperties ( $calendarId , $objectId );
$query = $this -> db -> getQueryBuilder ();
$query -> insert ( $this -> dbObjectPropertiesTable )
-> values (
[
'calendarid' => $query -> createNamedParameter ( $calendarId ),
'calendartype' => $query -> createNamedParameter ( $calendarType ),
'objectid' => $query -> createNamedParameter ( $objectId ),
'name' => $query -> createParameter ( 'name' ),
'parameter' => $query -> createParameter ( 'parameter' ),
'value' => $query -> createParameter ( 'value' ),
]
);
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
$indexComponents = [ 'VEVENT' , 'VJOURNAL' , 'VTODO' ];
foreach ( $vCalendar -> getComponents () as $component ) {
if ( ! in_array ( $component -> name , $indexComponents )) {
continue ;
2017-03-25 06:56:40 -04:00
}
2023-02-10 06:17:27 -05:00
foreach ( $component -> children () as $property ) {
if ( in_array ( $property -> name , self :: INDEXED_PROPERTIES , true )) {
$value = $property -> getValue ();
// is this a shitty db?
if ( ! $this -> db -> supports4ByteText ()) {
$value = preg_replace ( '/[\x{10000}-\x{10FFFF}]/u' , " \xEF \xBF \xBD " , $value );
}
$value = mb_strcut ( $value , 0 , 254 );
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
$query -> setParameter ( 'name' , $property -> name );
$query -> setParameter ( 'parameter' , null );
2024-08-20 07:20:47 -04:00
$query -> setParameter ( 'value' , mb_strcut ( $value , 0 , 254 ));
2023-02-10 06:17:27 -05:00
$query -> executeStatement ();
}
2017-03-25 06:56:40 -04:00
2023-02-10 06:17:27 -05:00
if ( array_key_exists ( $property -> name , self :: $indexParameters )) {
$parameters = $property -> parameters ();
$indexedParametersForProperty = self :: $indexParameters [ $property -> name ];
foreach ( $parameters as $key => $value ) {
if ( in_array ( $key , $indexedParametersForProperty )) {
// is this a shitty db?
if ( $this -> db -> supports4ByteText ()) {
$value = preg_replace ( '/[\x{10000}-\x{10FFFF}]/u' , " \xEF \xBF \xBD " , $value );
}
$query -> setParameter ( 'name' , $property -> name );
$query -> setParameter ( 'parameter' , mb_strcut ( $key , 0 , 254 ));
$query -> setParameter ( 'value' , mb_strcut ( $value , 0 , 254 ));
$query -> executeStatement ();
}
2017-03-25 06:56:40 -04:00
}
}
}
}
2023-02-10 06:17:27 -05:00
}, $this -> db );
2017-03-25 06:56:40 -04:00
}
2017-11-11 05:25:40 -05:00
/**
* deletes all birthday calendars
*/
public function deleteAllBirthdayCalendars () {
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () : void {
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$result = $query -> select ([ 'id' ]) -> from ( 'calendars' )
-> where ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( BirthdayService :: BIRTHDAY_CALENDAR_URI )))
-> executeQuery ();
2017-11-11 05:25:40 -05:00
2023-09-27 12:36:45 -04:00
while (( $row = $result -> fetch ()) !== false ) {
2023-02-10 06:17:27 -05:00
$this -> deleteCalendar (
2023-09-27 12:36:45 -04:00
$row [ 'id' ],
2023-02-10 06:17:27 -05:00
true // No data to keep in the trashbin, if the user re-enables then we regenerate
);
}
2023-09-27 12:36:45 -04:00
$result -> closeCursor ();
2023-02-10 06:17:27 -05:00
}, $this -> db );
2017-11-11 05:25:40 -05:00
}
2018-06-28 07:07:33 -04:00
/**
* @ param $subscriptionId
*/
public function purgeAllCachedEventsForSubscription ( $subscriptionId ) {
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $subscriptionId ) : void {
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> select ( 'uri' )
-> from ( 'calendarobjects' )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $subscriptionId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )));
$stmt = $query -> executeQuery ();
2018-06-28 07:07:33 -04:00
2023-02-10 06:17:27 -05:00
$uris = [];
2023-09-27 12:36:45 -04:00
while (( $row = $stmt -> fetch ()) !== false ) {
2023-02-10 06:17:27 -05:00
$uris [] = $row [ 'uri' ];
}
$stmt -> closeCursor ();
2018-06-28 07:07:33 -04:00
2023-02-10 06:17:27 -05:00
$query = $this -> db -> getQueryBuilder ();
$query -> delete ( 'calendarobjects' )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $subscriptionId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )))
-> executeStatement ();
2018-06-28 07:07:33 -04:00
2023-12-11 05:32:32 -05:00
$query = $this -> db -> getQueryBuilder ();
2023-02-10 06:17:27 -05:00
$query -> delete ( 'calendarchanges' )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $subscriptionId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )))
-> executeStatement ();
2018-06-28 07:07:33 -04:00
2023-12-11 05:32:32 -05:00
$query = $this -> db -> getQueryBuilder ();
2023-02-10 06:17:27 -05:00
$query -> delete ( $this -> dbObjectPropertiesTable )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $subscriptionId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )))
-> executeStatement ();
2018-06-28 07:07:33 -04:00
2024-03-11 11:27:23 -04:00
$this -> addChanges ( $subscriptionId , $uris , 3 , self :: CALENDAR_TYPE_SUBSCRIPTION );
2023-02-10 06:17:27 -05:00
}, $this -> db );
2018-06-28 07:07:33 -04:00
}
2024-07-24 10:11:47 -04:00
/**
* @ param int $subscriptionId
* @ param array < int > $calendarObjectIds
* @ param array < string > $calendarObjectUris
*/
public function purgeCachedEventsForSubscription ( int $subscriptionId , array $calendarObjectIds , array $calendarObjectUris ) : void {
if ( empty ( $calendarObjectUris )) {
return ;
}
2024-09-20 11:38:36 -04:00
$this -> atomic ( function () use ( $subscriptionId , $calendarObjectIds , $calendarObjectUris ) : void {
2024-07-24 10:11:47 -04:00
foreach ( array_chunk ( $calendarObjectIds , 1000 ) as $chunk ) {
$query = $this -> db -> getQueryBuilder ();
$query -> delete ( $this -> dbObjectPropertiesTable )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $subscriptionId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )))
-> andWhere ( $query -> expr () -> in ( 'id' , $query -> createNamedParameter ( $chunk , IQueryBuilder :: PARAM_INT_ARRAY ), IQueryBuilder :: PARAM_INT_ARRAY ))
-> executeStatement ();
$query = $this -> db -> getQueryBuilder ();
$query -> delete ( 'calendarobjects' )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $subscriptionId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )))
-> andWhere ( $query -> expr () -> in ( 'id' , $query -> createNamedParameter ( $chunk , IQueryBuilder :: PARAM_INT_ARRAY ), IQueryBuilder :: PARAM_INT_ARRAY ))
-> executeStatement ();
}
foreach ( array_chunk ( $calendarObjectUris , 1000 ) as $chunk ) {
$query = $this -> db -> getQueryBuilder ();
$query -> delete ( 'calendarchanges' )
-> where ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $subscriptionId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( self :: CALENDAR_TYPE_SUBSCRIPTION )))
-> andWhere ( $query -> expr () -> in ( 'uri' , $query -> createNamedParameter ( $chunk , IQueryBuilder :: PARAM_STR_ARRAY ), IQueryBuilder :: PARAM_STR_ARRAY ))
-> executeStatement ();
}
$this -> addChanges ( $subscriptionId , $calendarObjectUris , 3 , self :: CALENDAR_TYPE_SUBSCRIPTION );
}, $this -> db );
}
2017-07-26 06:33:32 -04:00
/**
* Move a calendar from one user to another
*
* @ param string $uriName
* @ param string $uriOrigin
* @ param string $uriDestination
2020-12-08 04:22:46 -05:00
* @ param string $newUriName ( optional ) the new uriName
2017-07-26 06:33:32 -04:00
*/
2020-12-08 04:22:46 -05:00
public function moveCalendar ( $uriName , $uriOrigin , $uriDestination , $newUriName = null ) {
2017-07-26 06:33:32 -04:00
$query = $this -> db -> getQueryBuilder ();
$query -> update ( 'calendars' )
-> set ( 'principaluri' , $query -> createNamedParameter ( $uriDestination ))
2020-12-08 04:22:46 -05:00
-> set ( 'uri' , $query -> createNamedParameter ( $newUriName ? : $uriName ))
2017-07-26 06:33:32 -04:00
-> where ( $query -> expr () -> eq ( 'principaluri' , $query -> createNamedParameter ( $uriOrigin )))
-> andWhere ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $uriName )))
2021-05-05 04:35:25 -04:00
-> executeStatement ();
2017-07-26 06:33:32 -04:00
}
2017-03-25 06:56:40 -04:00
/**
* read VCalendar data into a VCalendar object
*
* @ param string $objectData
* @ return VCalendar
*/
protected function readCalendarData ( $objectData ) {
return Reader :: read ( $objectData );
}
/**
* delete all properties from a given calendar object
*
* @ param int $calendarId
* @ param int $objectId
*/
protected function purgeProperties ( $calendarId , $objectId ) {
2023-07-25 12:09:11 -04:00
$this -> cachedObjects = [];
2017-03-25 06:56:40 -04:00
$query = $this -> db -> getQueryBuilder ();
$query -> delete ( $this -> dbObjectPropertiesTable )
-> where ( $query -> expr () -> eq ( 'objectid' , $query -> createNamedParameter ( $objectId )))
-> andWhere ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $calendarId )));
2021-05-05 04:35:25 -04:00
$query -> executeStatement ();
2017-03-25 06:56:40 -04:00
}
/**
* get ID from a given calendar object
*
* @ param int $calendarId
* @ param string $uri
2018-06-28 07:07:33 -04:00
* @ param int $calendarType
2017-03-25 06:56:40 -04:00
* @ return int
*/
2018-06-28 07:07:33 -04:00
protected function getCalendarObjectId ( $calendarId , $uri , $calendarType ) : int {
2017-03-25 06:56:40 -04:00
$query = $this -> db -> getQueryBuilder ();
2018-06-28 07:07:33 -04:00
$query -> select ( 'id' )
-> from ( 'calendarobjects' )
2017-03-25 06:56:40 -04:00
-> where ( $query -> expr () -> eq ( 'uri' , $query -> createNamedParameter ( $uri )))
2018-06-28 07:07:33 -04:00
-> andWhere ( $query -> expr () -> eq ( 'calendarid' , $query -> createNamedParameter ( $calendarId )))
-> andWhere ( $query -> expr () -> eq ( 'calendartype' , $query -> createNamedParameter ( $calendarType )));
2017-03-25 06:56:40 -04:00
2021-04-23 05:43:16 -04:00
$result = $query -> executeQuery ();
2017-03-25 06:56:40 -04:00
$objectIds = $result -> fetch ();
$result -> closeCursor ();
if ( ! isset ( $objectIds [ 'id' ])) {
throw new \InvalidArgumentException ( 'Calendarobject does not exists: ' . $uri );
}
return ( int ) $objectIds [ 'id' ];
}
2022-10-02 06:07:51 -04:00
/**
* @ throws \InvalidArgumentException
*/
2024-03-08 02:14:05 -05:00
public function pruneOutdatedSyncTokens ( int $keep , int $retention ) : int {
2022-10-02 06:07:51 -04:00
if ( $keep < 0 ) {
throw new \InvalidArgumentException ();
}
2023-06-04 17:51:11 -04:00
$query = $this -> db -> getQueryBuilder ();
$query -> select ( $query -> func () -> max ( 'id' ))
-> from ( 'calendarchanges' );
2023-07-11 01:58:16 -04:00
$result = $query -> executeQuery ();
$maxId = ( int ) $result -> fetchOne ();
$result -> closeCursor ();
2023-06-04 17:51:11 -04:00
if ( ! $maxId || $maxId < $keep ) {
2023-09-26 05:07:49 -04:00
return 0 ;
2023-06-04 17:51:11 -04:00
}
2022-10-02 06:07:51 -04:00
$query = $this -> db -> getQueryBuilder ();
$query -> delete ( 'calendarchanges' )
2024-03-08 02:14:05 -05:00
-> where (
$query -> expr () -> lte ( 'id' , $query -> createNamedParameter ( $maxId - $keep , IQueryBuilder :: PARAM_INT ), IQueryBuilder :: PARAM_INT ),
$query -> expr () -> lte ( 'created_at' , $query -> createNamedParameter ( $retention )),
);
2022-10-02 06:07:51 -04:00
return $query -> executeStatement ();
}
2018-06-28 07:07:33 -04:00
/**
* return legacy endpoint principal name to new principal name
*
* @ param $principalUri
* @ param $toV2
* @ return string
*/
2016-02-12 08:38:43 -05:00
private function convertPrincipal ( $principalUri , $toV2 ) {
if ( $this -> principalBackend -> getPrincipalPrefix () === 'principals' ) {
2021-01-12 04:15:48 -05:00
[, $name ] = Uri\split ( $principalUri );
2016-02-12 08:38:43 -05:00
if ( $toV2 === true ) {
return " principals/users/ $name " ;
}
return " principals/ $name " ;
}
return $principalUri ;
}
2017-04-19 10:18:44 -04:00
2018-06-28 07:07:33 -04:00
/**
* adds information about an owner to the calendar data
*
*/
2021-03-12 05:20:04 -05:00
private function addOwnerPrincipalToCalendar ( array $calendarInfo ) : array {
2017-04-19 10:18:44 -04:00
$ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_OWNCLOUD . '}owner-principal' ;
$displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin :: NS_NEXTCLOUD . '}owner-displayname' ;
if ( isset ( $calendarInfo [ $ownerPrincipalKey ])) {
$uri = $calendarInfo [ $ownerPrincipalKey ];
} else {
$uri = $calendarInfo [ 'principaluri' ];
}
2025-08-14 11:20:33 -04:00
$principalInformation = $this -> principalBackend -> getPrincipalPropertiesByPath ( $uri , [
'{DAV:}displayname' ,
]);
2017-04-19 10:18:44 -04:00
if ( isset ( $principalInformation [ '{DAV:}displayname' ])) {
$calendarInfo [ $displaynameKey ] = $principalInformation [ '{DAV:}displayname' ];
}
2021-03-12 05:20:04 -05:00
return $calendarInfo ;
}
private function addResourceTypeToCalendar ( array $row , array $calendar ) : array {
if ( isset ( $row [ 'deleted_at' ])) {
// Columns is set and not null -> this is a deleted calendar
// we send a custom resourcetype to hide the deleted calendar
// from ordinary DAV clients, but the Calendar app will know
// how to handle this special resource.
$calendar [ '{DAV:}resourcetype' ] = new DAV\Xml\Property\ResourceType ([
'{DAV:}collection' ,
sprintf ( '{%s}deleted-calendar' , \OCA\DAV\DAV\Sharing\Plugin :: NS_NEXTCLOUD ),
]);
}
return $calendar ;
2017-04-19 10:18:44 -04:00
}
2021-12-29 08:18:45 -05:00
/**
* Amend the calendar info with database row data
*
* @ param array $row
* @ param array $calendar
*
* @ return array
*/
private function rowToCalendar ( $row , array $calendar ) : array {
foreach ( $this -> propertyMap as $xmlName => [ $dbName , $type ]) {
$value = $row [ $dbName ];
if ( $value !== null ) {
settype ( $value , $type );
}
$calendar [ $xmlName ] = $value ;
}
return $calendar ;
}
2021-12-29 09:26:49 -05:00
/**
* Amend the subscription info with database row data
*
* @ param array $row
* @ param array $subscription
*
* @ return array
*/
private function rowToSubscription ( $row , array $subscription ) : array {
foreach ( $this -> subscriptionPropertyMap as $xmlName => [ $dbName , $type ]) {
$value = $row [ $dbName ];
if ( $value !== null ) {
settype ( $value , $type );
}
$subscription [ $xmlName ] = $value ;
}
return $subscription ;
}
2024-09-08 19:33:16 -04:00
/**
* delete all invitations from a given calendar
*
* @ since 31.0 . 0
*
* @ param int $calendarId
*
* @ return void
*/
protected function purgeCalendarInvitations ( int $calendarId ) : void {
// select all calendar object uid's
$cmd = $this -> db -> getQueryBuilder ();
$cmd -> select ( 'uid' )
-> from ( $this -> dbObjectsTable )
-> where ( $cmd -> expr () -> eq ( 'calendarid' , $cmd -> createNamedParameter ( $calendarId )));
$allIds = $cmd -> executeQuery () -> fetchAll ( \PDO :: FETCH_COLUMN );
// delete all links that match object uid's
$cmd = $this -> db -> getQueryBuilder ();
$cmd -> delete ( $this -> dbObjectInvitationsTable )
2024-11-23 10:41:59 -05:00
-> where ( $cmd -> expr () -> in ( 'uid' , $cmd -> createParameter ( 'uids' ), IQueryBuilder :: PARAM_STR_ARRAY ));
foreach ( array_chunk ( $allIds , 1000 ) as $chunkIds ) {
2024-11-23 10:52:46 -05:00
$cmd -> setParameter ( 'uids' , $chunkIds , IQueryBuilder :: PARAM_STR_ARRAY );
2024-09-08 19:33:16 -04:00
$cmd -> executeStatement ();
}
}
/**
* Delete all invitations from a given calendar event
*
* @ since 31.0 . 0
*
* @ param string $eventId UID of the event
*
* @ return void
*/
protected function purgeObjectInvitations ( string $eventId ) : void {
$cmd = $this -> db -> getQueryBuilder ();
$cmd -> delete ( $this -> dbObjectInvitationsTable )
2024-11-23 10:52:46 -05:00
-> where ( $cmd -> expr () -> eq ( 'uid' , $cmd -> createNamedParameter ( $eventId , IQueryBuilder :: PARAM_STR ), IQueryBuilder :: PARAM_STR ));
2024-09-08 19:33:16 -04:00
$cmd -> executeStatement ();
}
2025-04-07 08:59:20 -04:00
public function unshare ( IShareable $shareable , string $principal ) : void {
$this -> atomic ( function () use ( $shareable , $principal ) : void {
$calendarData = $this -> getCalendarById ( $shareable -> getResourceId ());
if ( $calendarData === null ) {
throw new \RuntimeException ( 'Trying to update shares for non-existing calendar: ' . $shareable -> getResourceId ());
}
$oldShares = $this -> getShares ( $shareable -> getResourceId ());
$unshare = $this -> calendarSharingBackend -> unshare ( $shareable , $principal );
if ( $unshare ) {
$this -> dispatcher -> dispatchTyped ( new CalendarShareUpdatedEvent (
$shareable -> getResourceId (),
$calendarData ,
$oldShares ,
[],
[ $principal ]
));
}
}, $this -> db );
}
2025-05-14 05:21:28 -04:00
/**
* @ return array < string , mixed > []
*/
public function getFederatedCalendarsForUser ( string $principalUri ) : array {
$federatedCalendars = $this -> federatedCalendarMapper -> findByPrincipalUri ( $principalUri );
return array_map (
static fn ( FederatedCalendarEntity $entity ) => $entity -> toCalendarInfo (),
$federatedCalendars ,
);
}
public function getFederatedCalendarByUri ( string $principalUri , string $uri ) : ? array {
$federatedCalendar = $this -> federatedCalendarMapper -> findByUri ( $principalUri , $uri );
return $federatedCalendar ? -> toCalendarInfo ();
}
2015-10-30 20:28:21 -04:00
}