From fc7900b256a99a03b9d2d9e49c24324e67b1352d Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Mon, 26 Feb 2018 15:31:15 +0100 Subject: [PATCH 01/37] Allow authentication types other than 'basic' The default authentication type is "basic", but some CalDav servers have other authentication types (like "digest", in my case). --- src/Facade/CalDavClient.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index 32c0063..a8a28be 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -77,6 +77,11 @@ final class CalDavClient implements ICalDavClient */ private $password; + /** + * @var string + */ + private $authtype = "basic"; + /** * @var Client */ @@ -92,12 +97,15 @@ final class CalDavClient implements ICalDavClient * @param string $server_url * @param string|null $user * @param string|null $password + * @param string|"basic" $authtype */ - public function __construct($server_url, $user = null, $password = null) + public function __construct($server_url, $user = null, $password = null, $authtype = "basic") { $this->server_url = $server_url; $this->user = $user; $this->password = $password; + $this->authtype = $authtype; + $this->client = new Client(); } @@ -121,6 +129,10 @@ public function setCredentials($username, $password) $this->password = $password; } + public function setAuthenticationType($authtype) { + $this->authtype = $authtype; + } + /** * @param Request $http_request * @return mixed|\Psr\Http\Message\ResponseInterface @@ -128,7 +140,7 @@ public function setCredentials($username, $password) private function makeRequest(Request $http_request){ try{ return $this->client->send($http_request, [ - 'auth' => [$this->user, $this->password], + 'auth' => [$this->user, $this->password, $this->authtype], 'timeout' => $this->timeout ]); } @@ -441,4 +453,4 @@ public function deleteCalendar($calendar_url, $etag = null) $this->server_url, (string)$http_response->getBody(), $http_response->getStatusCode() ); } -} \ No newline at end of file +} From d5784219b4e8394f0563b36e0f6dbf1e68057fba Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Thu, 1 Mar 2018 11:33:58 +0100 Subject: [PATCH 02/37] Implement missing date range filter --- src/Facade/Requests/CalendarQueryRequest.php | 34 +++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Facade/Requests/CalendarQueryRequest.php b/src/Facade/Requests/CalendarQueryRequest.php index ccf4739..8522373 100644 --- a/src/Facade/Requests/CalendarQueryRequest.php +++ b/src/Facade/Requests/CalendarQueryRequest.php @@ -34,6 +34,15 @@ public function __construct(CalendarQueryFilter $filter) */ protected $filter; + protected function formatTimestamp($datetime) { + // Make a copy of the date, and convert it to GMT to accommodate + // CalDAV's date & time formatting requirements + $clone = clone $datetime; + $clone->setTimezone(new \DateTimeZone("GMT")); + + return $clone->format('Ymd\THis\Z'); // 'Z' means: GMT time + } + /** * @return string */ @@ -57,6 +66,29 @@ public function getContent() $props['{urn:ietf:params:xml:ns:caldav}calendar-data'] = ''; } + if ($this->filter->getFrom() || $this->filter->getTo()) { + $date_range = []; + if ($this->filter->getFrom()) { + $date_range['start'] = $this->formatTimestamp($this->filter->getFrom()); + } + if ($this->filter->getTo()) { + $date_range['end'] = $this->formatTimestamp($this->filter->getTo()); + } + + $filter[] = [ + 'name' => '{urn:ietf:params:xml:ns:caldav}comp-filter', + 'attributes' => ['name' => 'VCALENDAR'], + 'value' => [ + 'name' => '{urn:ietf:params:xml:ns:caldav}comp-filter', + 'attributes' => ['name' => 'VEVENT'], + 'value' => [ + 'name' => '{urn:ietf:params:xml:ns:caldav}time-range', + 'attributes' => $date_range + ] + ] + ]; + } + $nodes = [ '{DAV:}prop' => [ $props @@ -65,4 +97,4 @@ public function getContent() ]; return $service->write('{urn:ietf:params:xml:ns:caldav}calendar-query', $nodes); } -} \ No newline at end of file +} From 7595a4befa19147d38014f1e81f7e69fd1cb92d3 Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Thu, 1 Mar 2018 15:41:51 +0100 Subject: [PATCH 03/37] No longer ignore user provided 'resourcename' When creating a calendar you can provide a resource name in the MakeCalendarRequestVO, but this resource name was ignored when doing the actual call to the server. This commit checks if you have provided a resource name, and will try to create a calendar using that resource name if you did. If you did not provide a resource name, it will revert to the previous behaviour of using a UID for the calendar name. --- src/Facade/CalDavClient.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index 32c0063..037c23d 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -213,7 +213,9 @@ public function getCalendarHome($principal_url) public function createCalendar($calendar_home_set, MakeCalendarRequestVO $vo) { $uid = $vo->getUID(); - $resource_url = $calendar_home_set.$uid; + $resource_name = $vo->getResourceName(); + + $resource_url = $calendar_home_set . ($resource_name ? $resource_name : $uid); $http_response = $this->makeRequest( RequestFactory::createMakeCalendarRequest ( @@ -441,4 +443,4 @@ public function deleteCalendar($calendar_url, $etag = null) $this->server_url, (string)$http_response->getBody(), $http_response->getStatusCode() ); } -} \ No newline at end of file +} From 1b3cb5c9ef19f4f0dbcee96d8df03beecf9e77f0 Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Fri, 2 Mar 2018 09:28:56 +0100 Subject: [PATCH 04/37] CalendarDeletedResponse accepts 2 arguments, not 3 It accepts a 'body' and (return) 'code' argument, but in the code here 'calendar_url' was erroneously passed in as first argument --- src/Facade/CalDavClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index 32c0063..4f62ad3 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -438,7 +438,7 @@ public function deleteCalendar($calendar_url, $etag = null) return new CalendarDeletedResponse ( - $this->server_url, (string)$http_response->getBody(), $http_response->getStatusCode() + (string)$http_response->getBody(), $http_response->getStatusCode() ); } -} \ No newline at end of file +} From 113fbdd9ee6065b2d912707fc6fab6455be81f5a Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Fri, 2 Mar 2018 10:05:55 +0100 Subject: [PATCH 05/37] Add omitted namespace In the GetCalendarRequest and GetCalendarsRequest you use the {http://calendarserver.org/ns/:} namespace, but that one was not declared at the top of the XML document. --- src/Facade/Requests/AbstractPropFindWebDAVRequest.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Facade/Requests/AbstractPropFindWebDAVRequest.php b/src/Facade/Requests/AbstractPropFindWebDAVRequest.php index 016c041..b914cdc 100644 --- a/src/Facade/Requests/AbstractPropFindWebDAVRequest.php +++ b/src/Facade/Requests/AbstractPropFindWebDAVRequest.php @@ -31,13 +31,14 @@ public function getContent() $service = new Service(); $service->namespaceMap = [ - 'DAV:' => 'D', - 'urn:ietf:params:xml:ns:caldav' => 'C' + 'DAV:' => 'D', + 'urn:ietf:params:xml:ns:caldav' => 'C', + 'http://calendarserver.org/ns/:' => 'CS', ]; $elements = []; foreach( $this->properties as $val ) { - $elements[] = [ $val => ""]; + $elements[] = [ $val => "" ]; } return $service->write('{DAV:}propfind', [ @@ -45,4 +46,4 @@ public function getContent() ]); } -} \ No newline at end of file +} From 92b72c271e485e898322daa89405e8ca55e6f746 Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Fri, 2 Mar 2018 13:39:32 +0100 Subject: [PATCH 06/37] Typo in namespace Prevents SabreDAV from recognizing the request for a ctag --- src/Facade/Requests/AbstractPropFindWebDAVRequest.php | 4 ++-- src/Facade/Requests/GetCalendarRequest.php | 2 +- src/Facade/Requests/GetCalendarsRequest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Facade/Requests/AbstractPropFindWebDAVRequest.php b/src/Facade/Requests/AbstractPropFindWebDAVRequest.php index b914cdc..5d3fd19 100644 --- a/src/Facade/Requests/AbstractPropFindWebDAVRequest.php +++ b/src/Facade/Requests/AbstractPropFindWebDAVRequest.php @@ -33,7 +33,7 @@ public function getContent() $service->namespaceMap = [ 'DAV:' => 'D', 'urn:ietf:params:xml:ns:caldav' => 'C', - 'http://calendarserver.org/ns/:' => 'CS', + 'http://calendarserver.org/ns/' => 'CS', ]; $elements = []; @@ -46,4 +46,4 @@ public function getContent() ]); } -} +} \ No newline at end of file diff --git a/src/Facade/Requests/GetCalendarRequest.php b/src/Facade/Requests/GetCalendarRequest.php index b2b084c..2bc7640 100644 --- a/src/Facade/Requests/GetCalendarRequest.php +++ b/src/Facade/Requests/GetCalendarRequest.php @@ -29,7 +29,7 @@ public function __construct(){ '{DAV:}resourcetype', '{DAV:}sync-token', '{DAV:}getetag', - '{http://calendarserver.org/ns/:}getctag', + '{http://calendarserver.org/ns/}getctag', ]; } } \ No newline at end of file diff --git a/src/Facade/Requests/GetCalendarsRequest.php b/src/Facade/Requests/GetCalendarsRequest.php index 7f88a9d..05d0039 100644 --- a/src/Facade/Requests/GetCalendarsRequest.php +++ b/src/Facade/Requests/GetCalendarsRequest.php @@ -21,7 +21,7 @@ public function __construct(){ $this->properties = [ '{DAV:}resourcetype', '{DAV:}displayname', - '{http://calendarserver.org/ns/:}getctag', + '{http://calendarserver.org/ns/}getctag', '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set', ]; } From 5977824e0a3ab7e35aacfc7f4d877eba5914f6c8 Mon Sep 17 00:00:00 2001 From: Sebastian Marcet Date: Sat, 3 Mar 2018 11:13:07 -0300 Subject: [PATCH 07/37] [smarcet] * fixed some issues from scrutinizer --- src/Facade/CalDavClient.php | 8 +++++--- src/Facade/Responses/CalendarSyncInfoResponse.php | 9 ++++----- src/Facade/Responses/EventCreatedResponse.php | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index 996f50c..52da5ba 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -62,6 +62,8 @@ final class CalDavClient implements ICalDavClient const CalendarAccessOption = 'calendar-access'; + const DefaultAuthType = 'basic'; + /** * @var string */ @@ -80,7 +82,7 @@ final class CalDavClient implements ICalDavClient /** * @var string */ - private $authtype = "basic"; + private $authtype = self::DefaultAuthType; /** * @var Client @@ -97,9 +99,9 @@ final class CalDavClient implements ICalDavClient * @param string $server_url * @param string|null $user * @param string|null $password - * @param string|"basic" $authtype + * @param string $authtype */ - public function __construct($server_url, $user = null, $password = null, $authtype = "basic") + public function __construct($server_url, $user = null, $password = null, $authtype = self::DefaultAuthType) { $this->server_url = $server_url; $this->user = $user; diff --git a/src/Facade/Responses/CalendarSyncInfoResponse.php b/src/Facade/Responses/CalendarSyncInfoResponse.php index f982823..5892763 100644 --- a/src/Facade/Responses/CalendarSyncInfoResponse.php +++ b/src/Facade/Responses/CalendarSyncInfoResponse.php @@ -33,15 +33,14 @@ public function hasAvailableChanges(){ return count($this->responses) > 0; } - /** * @return ETagEntityResponse[] */ public function getUpdates(){ $res = []; foreach ($this->responses as $entity){ - if($entity->getStatus() != HttpResponse::HttpOKStatus) continue; - $res[] = $entity; + if($entity instanceof ETagEntityResponse && $entity->getStatus() != HttpResponse::HttpOKStatus) continue; + $res[] = $entity; } return $res; } @@ -52,8 +51,8 @@ public function getUpdates(){ public function getDeletes(){ $res = []; foreach ($this->responses as $entity){ - if($entity->getStatus() != HttpResponse::HttpNotFoundStatus) continue; - $res[] = $entity; + if($entity instanceof ETagEntityResponse && $entity->getStatus() != HttpResponse::HttpNotFoundStatus) continue; + $res[] = $entity; } return $res; } diff --git a/src/Facade/Responses/EventCreatedResponse.php b/src/Facade/Responses/EventCreatedResponse.php index 77ffb66..49ade9d 100644 --- a/src/Facade/Responses/EventCreatedResponse.php +++ b/src/Facade/Responses/EventCreatedResponse.php @@ -38,7 +38,7 @@ class EventCreatedResponse extends HttpResponse * @param int $etag * @param string $resource_url * @param string $body - * @param string $code + * @param int $code */ public function __construct($uid, $etag, $resource_url, $body, $code) { From 60c6a5907cc5e4a40a778cd5c3b094085b9602fd Mon Sep 17 00:00:00 2001 From: Sebastian Marcet Date: Thu, 3 May 2018 18:49:20 -0300 Subject: [PATCH 08/37] [smarcet] * fixed calendar time range query --- src/Facade/Requests/CalendarQueryFilter.php | 13 ++++++++----- src/Facade/Responses/GetCalendarResponse.php | 5 +++++ src/Facade/Responses/GetCalendarsResponse.php | 14 +++++++++++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Facade/Requests/CalendarQueryFilter.php b/src/Facade/Requests/CalendarQueryFilter.php index c554d40..3c974b2 100644 --- a/src/Facade/Requests/CalendarQueryFilter.php +++ b/src/Facade/Requests/CalendarQueryFilter.php @@ -43,15 +43,18 @@ final class CalendarQueryFilter * CalendarQueryFilter constructor. * @param bool $get_etags * @param bool $get_calendar_data - * @param DateTime $to * @param DateTime $from + * @param DateTime $to */ - public function __construct($get_etags = true, $get_calendar_data = false, DateTime $to = null, DateTime $from = null) + public function __construct($get_etags = true, $get_calendar_data = false, DateTime $from = null, DateTime $to = null) { - $this->get_etags = $get_etags; + $this->get_etags = $get_etags; $this->get_calendar_data = $get_calendar_data; - $this->to = $to; - $this->from = $from; + $this->from = $from; + $this->to = $to; + + if(!is_null($this->from) && !is_null($this->to) && $this->from > $this->to) + throw new \InvalidArgumentException("from should be lower than to param"); } /** diff --git a/src/Facade/Responses/GetCalendarResponse.php b/src/Facade/Responses/GetCalendarResponse.php index 8565e4f..31ac3d9 100644 --- a/src/Facade/Responses/GetCalendarResponse.php +++ b/src/Facade/Responses/GetCalendarResponse.php @@ -18,6 +18,7 @@ */ final class GetCalendarResponse extends ETagEntityResponse { + const ResourceTypeCalendar = 'calendar'; /** * @return string */ @@ -25,6 +26,10 @@ public function getDisplayName(){ return isset($this->found_props['displayname']) ? $this->found_props['displayname'] : null; } + public function getResourceType(){ + return isset($this->found_props['resourcetype']) ? $this->found_props['resourcetype'] : null; + } + /** * @see https://tools.ietf.org/html/rfc6578 * @return string diff --git a/src/Facade/Responses/GetCalendarsResponse.php b/src/Facade/Responses/GetCalendarsResponse.php index 34dbc6d..77d0869 100644 --- a/src/Facade/Responses/GetCalendarsResponse.php +++ b/src/Facade/Responses/GetCalendarsResponse.php @@ -20,7 +20,6 @@ */ final class GetCalendarsResponse extends GenericMultiCalDAVResponse { - /** * @return GenericSinglePROPFINDCalDAVResponse */ @@ -28,5 +27,18 @@ protected function buildSingleResponse() { return new GetCalendarResponse(); } + + /** + * @param string $type + * @return array + */ + public function getResponseByType($type){ + $responses = []; + foreach ($this->getResponses() as $response){ + $resource_types = $response->getResourceType(); + if(in_array($type, array_keys($resource_types))) $responses[] = $response; + } + return $responses; + } } From aba1d991c65fcb8dfba87158cf1edf2461bfa999 Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Fri, 2 Mar 2018 13:46:03 +0100 Subject: [PATCH 09/37] Strip namespaces from XML before JSON conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some CalDAV servers only return namespaced XML, and simplexml_load_string deals with this kind of XML slightly differently, which means we can’t easily convert it to JSON without first stripping the namespaces (unfortunately). --- .../Responses/AbstractCalDAVResponse.php | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Facade/Responses/AbstractCalDAVResponse.php b/src/Facade/Responses/AbstractCalDAVResponse.php index d7ec0ec..cd6821e 100644 --- a/src/Facade/Responses/AbstractCalDAVResponse.php +++ b/src/Facade/Responses/AbstractCalDAVResponse.php @@ -34,6 +34,8 @@ abstract class AbstractCalDAVResponse extends HttpResponse */ protected $content; + private $stripped; + /** * AbstractCalDAVResponse constructor. * @param string|null $server_url @@ -45,10 +47,12 @@ public function __construct($server_url = null, $body = null, $code = HttpRespon parent::__construct($body, $code); $this->server_url = $server_url; if(!empty($this->body)) { - $this->xml = simplexml_load_string($this->body, 'SimpleXMLElement', LIBXML_NOCDATA); + $this->stripped = $this->stripNamespacesFromTags($this->body); + $this->xml = simplexml_load_string($this->stripped); if($this->xml === FALSE) throw new XMLResponseParseException(); $this->content = $this->toAssocArray($this->xml); + $this->parse(); } } @@ -62,6 +66,51 @@ protected function setContent($content){ } abstract protected function parse(); + + /** + * Strip namespaces from the XML, because otherwise we can't always properly convert + * the XML to an associative JSON array, and some CalDAV servers (such as SabreDAV) + * return only namespaced XML. + * + * @param $xml + * @return string + */ + private function stripNamespacesFromTags($xml) { + // `simplexml_load_string` treats namespaced XML differently than non-namespaced XML, and + // calling `json_encode` on the results of a parsed namespaced XML string will only + // include the non-namespaced tags. Therefore, we remove the namespaces. + // + // Almost literally taken from + // https://laracasts.com/discuss/channels/general-discussion/converting-xml-to-jsonarray/replies/112561 + + + // We retrieve the namespaces from the XML code so we can check for + // them to remove them + $obj = simplexml_load_string($xml); + $namespaces = $obj->getNamespaces(true); + $toRemove = array_keys($namespaces); + + // This is part of a regex I will use to remove the namespace declaration from string + $nameSpaceDefRegEx = '(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?'; + + // Cycle through each namespace and remove it from the XML string + foreach( $toRemove as $remove ) { + // First remove the namespace from the opening of the tag + $xml = str_replace('<' . $remove . ':', '<', $xml); + // Now remove the namespace from the closing of the tag + $xml = str_replace(' Date: Fri, 2 Mar 2018 13:47:13 +0100 Subject: [PATCH 10/37] User principals can be a multiresponse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this change the library doesn’t work in SabreDAV --- .../Responses/UserPrincipalResponse.php | 11 ++++---- .../Responses/UserPrincipalSingleResponse.php | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 src/Facade/Responses/UserPrincipalSingleResponse.php diff --git a/src/Facade/Responses/UserPrincipalResponse.php b/src/Facade/Responses/UserPrincipalResponse.php index 83a548e..067e845 100644 --- a/src/Facade/Responses/UserPrincipalResponse.php +++ b/src/Facade/Responses/UserPrincipalResponse.php @@ -16,13 +16,14 @@ * Class UserPrincipalResponse * @package CalDAVClient\Facade\Responses */ -final class UserPrincipalResponse extends GenericSinglePROPFINDCalDAVResponse +final class UserPrincipalResponse extends GenericMultiCalDAVResponse { + /** - * @return string + * @return GenericSinglePROPFINDCalDAVResponse */ - public function getPrincipalUrl(){ - return isset($this->found_props['current-user-principal']) && isset($this->found_props['current-user-principal']['href']) ? - $this->server_url.$this->found_props['current-user-principal']['href'] : null; + protected function buildSingleResponse() + { + return new UserPrincipalSingleResponse(); } } \ No newline at end of file diff --git a/src/Facade/Responses/UserPrincipalSingleResponse.php b/src/Facade/Responses/UserPrincipalSingleResponse.php new file mode 100644 index 0000000..6bb8183 --- /dev/null +++ b/src/Facade/Responses/UserPrincipalSingleResponse.php @@ -0,0 +1,28 @@ +found_props['current-user-principal']) && isset($this->found_props['current-user-principal']['href']) ? + $this->server_url.$this->found_props['current-user-principal']['href'] : null; + } +} \ No newline at end of file From f4951eac6d1f1cec4bc844a3f64e8ed6ad1c0ed3 Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Fri, 2 Mar 2018 13:52:36 +0100 Subject: [PATCH 11/37] Made tests succeed on both iCloud and SabreDAV The tests were very tailored for iCloud. I adjusted them to run on more generic CalDav servers as well. --- phpunit.xml | 23 +++++- tests/FacadeTest.php | 170 ++++++++++++++++++++++++++++--------------- 2 files changed, 130 insertions(+), 63 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 70c4f7f..df36ae9 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -16,8 +16,25 @@ - - - + + + + + + + + + + + \ No newline at end of file diff --git a/tests/FacadeTest.php b/tests/FacadeTest.php index 2bb50e8..5cf8dd2 100644 --- a/tests/FacadeTest.php +++ b/tests/FacadeTest.php @@ -26,12 +26,29 @@ final class FacadeTest extends PHPUnit_Framework_TestCase */ private static $client; + /** + * @var string URL of the calendar that was created during the test + */ + private static $calendar_created_by_phpunit = ""; + + /** + * @var \CalDAVClient\Facade\Responses\EventCreatedResponse + */ + private static $event_created_by_phpunit = null; + + private static $calendar_home = null; + + public function setUp() { + parent::setUp(); + } + public static function setUpBeforeClass() { self::$client = new CalDavClient( - getenv('CALDAV_SERVER_URL'), - getenv('USER_EMAIL'), - getenv('USER_PASSWORD') + getenv('CALDAV_SERVER_HOST') . getenv('CALDAV_SERVER_PATH'), + getenv('USER_LOGIN'), + getenv('USER_PASSWORD'), + getenv('AUTHTYPE') ); } @@ -40,92 +57,108 @@ public static function tearDownAfterClass() // do sth after the last test } + private function getCalendarUrl() { + return + getenv('CALDAV_SERVER_HOST') . + getenv('CALDAV_SERVER_PATH') . + getenv('CALDAV_CALENDAR_HOME') . + getenv('CALDAV_TEST_CALENDAR_PATH'); + } + function testIsValidServer(){ - $this->assertTrue(self::$client ->isValidServer()); + $this->assertTrue(self::$client->isValidServer()); } function testPrincipal(){ + $principals = self::$client->getUserPrincipal(); + $responses = $principals->getResponses(); - $res = self::$client ->getUserPrincipal(); - $url = $res->getPrincipalUrl(); + foreach ($responses as $res) { + $url = $res->getPrincipalUrl(); - $this->assertTrue(!empty($url)); - echo sprintf('principal url is %s', $url).PHP_EOL; - return $url; + $this->assertTrue(!empty($url), "Principal URL is empty"); + return $url; + } } function testCalendarHomes(){ + $caldav_host = getenv("CALDAV_SERVER_HOST"); + $caldav_path = getenv("CALDAV_SERVER_PATH"); + $principal_url = $this->testPrincipal(); - $res = self::$client->getCalendarHome($principal_url); + $res = self::$client->getCalendarHome($caldav_host . $principal_url); $url = $res->getCalendarHomeSetUrl(); - $this->assertTrue(!empty($url)); - $host = $res->getRealCalDAVHost(); - echo sprintf('calendar home is %s', $url).PHP_EOL; - echo sprintf('host is %s', $host).PHP_EOL; - return $url; + $this->assertTrue(!empty($url), "Calendar home URL is empty"); + // $host = $res->getRealCalDAVHost(); + // echo sprintf('calendar home is %s', $url).PHP_EOL; + // echo sprintf('host is %s', $caldav_host).PHP_EOL; + + // first, ensures that the 'home' path is relative to the CalDav server + // (this differs between servers) + $path_without_prefix = $url; + if (strpos($path_without_prefix, $caldav_host) === 0) { + $path_without_prefix = substr($path_without_prefix, strlen($caldav_host)); + } + if (strpos($path_without_prefix, $caldav_path) === 0) { + $path_without_prefix = substr($path_without_prefix, strlen($caldav_path)); + } + + // then, turn the URL into an absolute URL so that we always know what to expect + self::$calendar_home = $caldav_host . $caldav_path . $path_without_prefix; } function testGetCalendars(){ - $calendar_home = $this->testCalendarHomes(); + $calendar_home = self::$calendar_home; + $res = self::$client->getCalendars($calendar_home); - $this->assertTrue($res->isSuccessFull()); - $this->assertTrue(count($res->getResponses()) > 0); + $this->assertTrue($res->isSuccessFull(), "GetCalendars request not successful"); + $this->assertTrue(count($res->getResponses()) > 0, "Request returned zero responses"); return $res; } function testGetCalendar(){ - $res = self::$client->getCalendar(getenv('CALDAV_SERVER_URL').'/8244464267/calendars/openstack-summit-sidney-2017/'); - $this->assertTrue($res->isSuccessFull()); - $this->assertTrue(!empty($res->getDisplayName())); - $this->assertTrue(!empty($res->getSyncToken())); + $res = self::$client->getCalendar($this->getCalendarUrl()); + $this->assertTrue($res->isSuccessFull(), "Calendar request not successful"); + $this->assertTrue(!empty($res->getDisplayName()), "Display name not set"); + $this->assertTrue(!empty($res->getSyncToken()), "Sync-token empty"); + return $res; } function testSyncCalendar(){ + $cal = $this->testGetCalendar(); $res = self::$client->getCalendarSyncInfo( - getenv('CALDAV_SERVER_URL').'/8244464267/calendars/openstack-summit-sidney-2017/', - "FT=-@RU=8546e45e-a9f6-4f20-b6a2-7637f4783d8f@S=169"); + $this->getCalendarUrl(), + $cal->getSyncToken()); $this->assertTrue($res->isSuccessFull()); $this->assertTrue(!empty($res->getSyncToken())); } function testCreateCalendar(){ + $home = self::$calendar_home; - $res = self::$client->createCalendar( - getenv('CALDAV_SERVER_URL').'/8244464267/calendars/', + $link = self::$client->createCalendar( + $home, new MakeCalendarRequestVO( - 'openstack-summit-sidney-2017', + null, // means: generate a unique name 'OpenStack Sidney Summit Nov 2017', 'Calendar to hold Summit Events', new DateTimeZone('Australia/Sydney') ) ); - $this->assertTrue(!empty($res)); - } - - function testDeleteCalendar(){ - - $calendar_url = 'https://p01-caldav.icloud.com:443/8244464267/calendars/openstack-summit-sidney-2017'; - $res = self::$client->getCalendar($calendar_url); - - $res = self::$client->deleteCalendar - ( - $calendar_url, - "" - ); - - $this->assertTrue(!empty($res)); + $this->assertTrue(!empty($link)); + self::$calendar_created_by_phpunit = $link . '/'; } function testCreateEvent(){ $res = self::$client->createEvent( - getenv('CALDAV_SERVER_URL').'/8244464267/calendars/0f9ba5e9072576c6fab990b8f813b4e0/', + self::$calendar_created_by_phpunit, new EventRequestVO( - 'openstack-summit-sidney-2017', + "test-event-" . md5(microtime(true)), 'test event 4', - 'test event', + 'test event', 'test event', new DateTime('2017-11-01 09:00:00'), new DateTime('2017-11-01 10:30:00'), @@ -134,10 +167,11 @@ function testCreateEvent(){ ); $this->assertTrue($res->isSuccessFull()); + self::$event_created_by_phpunit = $res; } function testUpdateEvent(){ - $uid = 'ad82cdfe9da1d7b8c840c3acfa65db18'; + $uid = self::$event_created_by_phpunit->getUid(); //$etag = "C=150@U=8546e45e-a9f6-4f20-b6a2-7637f4783d8f"; $dto = new EventRequestVO( @@ -150,26 +184,16 @@ function testUpdateEvent(){ new DateTimeZone('Australia/Sydney') ); $dto->setUID($uid); - $res = self::$client->updateEvent(getenv('CALDAV_SERVER_URL').'/8244464267/calendars/0f9ba5e9072576c6fab990b8f813b4e0/', + $res = self::$client->updateEvent(self::$calendar_created_by_phpunit, $dto ); $this->assertTrue($res->isSuccessFull()); } - function testDeleteEvent(){ - $uid = 'd738bd4a675f4b60cf664b88ce7f0659'; - - $res = self::$client->deleteEvent(getenv('CALDAV_SERVER_URL').'/8244464267/calendars/0f9ba5e9072576c6fab990b8f813b4e0/', - $uid - ); - - $this->assertTrue($res->isSuccessFull()); - } - function testGetEventByUrl(){ - $event_url = 'https://p01-caldav.icloud.com:443/8244464267/calendars/openstack-summit-sidney-2017/d7a2387264bfa1a619c37a593e94204a.ics'; + $event_url = self::$event_created_by_phpunit->getResourceUrl(); $v_card = self::$client->getEventVCardBy($event_url); @@ -177,11 +201,37 @@ function testGetEventByUrl(){ } function testGetEventsByUrl(){ - $event_url = '/8244464267/calendars/openstack-summit-sidney-2017/0df083912b476631bf677c140ad4740b.ics'; + $event_url = self::$event_created_by_phpunit->getResourceUrl(); - $res = self::$client->getEventsBy('https://p01-caldav.icloud.com:443/8244464267/calendars/openstack-summit-sidney-2017/', + $res = self::$client->getEventsBy(self::$calendar_created_by_phpunit, [$event_url]); $this->assertTrue($res->isSuccessFull()); } + + function testDeleteEvent(){ + $uid = self::$event_created_by_phpunit->getUid(); + + $res = self::$client->deleteEvent(self::$calendar_created_by_phpunit, + $uid + ); + + $this->assertTrue($res->isSuccessFull()); + } + + function testDeleteCalendar(){ + $host = getenv("CALDAV_SERVER_HOST"); + + $calendar_url = $host . (str_replace($host, "", self::$calendar_created_by_phpunit)); + + $res = self::$client->getCalendar($calendar_url); + + $res = self::$client->deleteCalendar + ( + $calendar_url, + "" + ); + + $this->assertTrue(!empty($res)); + } } \ No newline at end of file From 769f4e5070af804a4005bf636f71046fcab8feeb Mon Sep 17 00:00:00 2001 From: Sebastian Marcet Date: Fri, 4 May 2018 00:16:43 -0300 Subject: [PATCH 12/37] [smarcet] * refactoring for scrutinizer --- src/Facade/Responses/EventCreatedResponse.php | 2 +- src/Facade/Responses/GetCalendarsResponse.php | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Facade/Responses/EventCreatedResponse.php b/src/Facade/Responses/EventCreatedResponse.php index 49ade9d..d7a2cbb 100644 --- a/src/Facade/Responses/EventCreatedResponse.php +++ b/src/Facade/Responses/EventCreatedResponse.php @@ -35,7 +35,7 @@ class EventCreatedResponse extends HttpResponse /** * EventCreatedResponse constructor. * @param string $uid - * @param int $etag + * @param string $etag * @param string $resource_url * @param string $body * @param int $code diff --git a/src/Facade/Responses/GetCalendarsResponse.php b/src/Facade/Responses/GetCalendarsResponse.php index 77d0869..03366fc 100644 --- a/src/Facade/Responses/GetCalendarsResponse.php +++ b/src/Facade/Responses/GetCalendarsResponse.php @@ -1,4 +1,4 @@ -getResponses() as $response){ + if(!$response instanceof GetCalendarResponse) continue; $resource_types = $response->getResourceType(); if(in_array($type, array_keys($resource_types))) $responses[] = $response; } + return $responses; } } From cd63fc0a92ed7573095baf3bc2db8e0b958b9a0e Mon Sep 17 00:00:00 2001 From: Sebastian Marcet Date: Fri, 1 Jun 2018 13:10:36 -0700 Subject: [PATCH 13/37] [smarcet] * added conflict exception for makeRequest ( 409 http code ) --- src/Facade/CalDavClient.php | 8 ++++++-- src/Facade/Exceptions/ConflictException.php | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/Facade/Exceptions/ConflictException.php diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index 52da5ba..f6202f2 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -13,6 +13,7 @@ * limitations under the License. **/ +use CalDAVClient\Facade\Exceptions\ConflictException; use CalDAVClient\Facade\Exceptions\ForbiddenException; use CalDAVClient\Facade\Requests\CalDAVRequestFactory; use CalDAVClient\Facade\Requests\CalendarQueryFilter; @@ -138,6 +139,7 @@ public function setAuthenticationType($authtype) { /** * @param Request $http_request * @return mixed|\Psr\Http\Message\ResponseInterface + * @throws \GuzzleHttp\Exception\GuzzleException */ private function makeRequest(Request $http_request){ try{ @@ -151,11 +153,13 @@ private function makeRequest(Request $http_request){ case 401: throw new UserUnAuthorizedException(); break; + case 403: + throw new ForbiddenException(); case 404: throw new NotFoundResourceException(); break; - case 403: - throw new ForbiddenException(); + case 409: + throw new ConflictException(); default: throw new ServerErrorException($ex->getMessage(), $ex->getCode()); break; diff --git a/src/Facade/Exceptions/ConflictException.php b/src/Facade/Exceptions/ConflictException.php new file mode 100644 index 0000000..e9568bd --- /dev/null +++ b/src/Facade/Exceptions/ConflictException.php @@ -0,0 +1,21 @@ + Date: Fri, 1 Jun 2018 15:00:49 -0700 Subject: [PATCH 14/37] [smarcet] * added exception info to ConflictException --- src/Facade/CalDavClient.php | 15 ++++++++++++++- src/Facade/Exceptions/ConflictException.php | 10 ++++++++-- src/Facade/Exceptions/ServerErrorException.php | 1 - 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index f6202f2..09f542d 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -159,7 +159,7 @@ private function makeRequest(Request $http_request){ throw new NotFoundResourceException(); break; case 409: - throw new ConflictException(); + throw new ConflictException($ex->getMessage(), $ex->getCode()); default: throw new ServerErrorException($ex->getMessage(), $ex->getCode()); break; @@ -169,6 +169,7 @@ private function makeRequest(Request $http_request){ /** * @return bool + * @throws \GuzzleHttp\Exception\GuzzleException */ public function isValidServer() { @@ -208,6 +209,7 @@ public function getUserPrincipal() /** * @param string $principal_url * @return CalendarHomesResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getCalendarHome($principal_url) { @@ -227,6 +229,7 @@ public function getCalendarHome($principal_url) * @param MakeCalendarRequestVO $vo * @see https://tools.ietf.org/html/rfc4791#section-5.3.1 * @return string|boolean + * @throws \GuzzleHttp\Exception\GuzzleException */ public function createCalendar($calendar_home_set, MakeCalendarRequestVO $vo) { @@ -248,6 +251,7 @@ public function createCalendar($calendar_home_set, MakeCalendarRequestVO $vo) /** * @param string $calendar_home_set_url * @return GetCalendarsResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getCalendars($calendar_home_set_url) { @@ -265,6 +269,7 @@ public function getCalendars($calendar_home_set_url) /** * @param string $calendar_url * @return GetCalendarResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getCalendar($calendar_url) { @@ -285,6 +290,7 @@ public function getCalendar($calendar_url) * @param string $calendar_url * @param string $sync_token * @return CalendarSyncInfoResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getCalendarSyncInfo($calendar_url, $sync_token) { @@ -304,6 +310,7 @@ public function getCalendarSyncInfo($calendar_url, $sync_token) * @param string $calendar_url * @param EventRequestVO $vo * @return EventCreatedResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function createEvent($calendar_url, EventRequestVO $vo) { @@ -332,6 +339,7 @@ public function createEvent($calendar_url, EventRequestVO $vo) * @param EventRequestVO $vo * @param string $etag * @return EventUpdatedResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function updateEvent($calendar_url, EventRequestVO $vo, $etag = null) { @@ -361,6 +369,7 @@ public function updateEvent($calendar_url, EventRequestVO $vo, $etag = null) * @param string $uid * @param string $etag * @return EventDeletedResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function deleteEvent($calendar_url, $uid, $etag = null) { @@ -381,6 +390,7 @@ public function deleteEvent($calendar_url, $uid, $etag = null) /** * @param string $event_url * @return string + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getEventVCardBy($event_url){ $http_response = $this->makeRequest( @@ -398,6 +408,7 @@ public function getEventVCardBy($event_url){ * @param string $calendar_url * @param array $events_urls * @return ResourceCollectionResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getEventsBy($calendar_url, array $events_urls) { @@ -421,6 +432,7 @@ public function getEventsBy($calendar_url, array $events_urls) * @param string $calendar_url * @param CalendarQueryFilter $filter * @return ResourceCollectionResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function getEventsByQuery($calendar_url, CalendarQueryFilter $filter) { @@ -445,6 +457,7 @@ public function getEventsByQuery($calendar_url, CalendarQueryFilter $filter) * @param string $calendar_url * @param string|null $etag * @return CalendarDeletedResponse + * @throws \GuzzleHttp\Exception\GuzzleException */ public function deleteCalendar($calendar_url, $etag = null) { diff --git a/src/Facade/Exceptions/ConflictException.php b/src/Facade/Exceptions/ConflictException.php index e9568bd..ea96110 100644 --- a/src/Facade/Exceptions/ConflictException.php +++ b/src/Facade/Exceptions/ConflictException.php @@ -11,11 +11,17 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ +use RuntimeException; +use Throwable; /** + * * Class ConflictException * @package CalDAVClient\Facade\Exceptions */ -final class ConflictException extends \RuntimeException +final class ConflictException extends RuntimeException { - + public function __construct($message = "", $code = 0, Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } } \ No newline at end of file diff --git a/src/Facade/Exceptions/ServerErrorException.php b/src/Facade/Exceptions/ServerErrorException.php index ddb53c9..d2fc8bc 100644 --- a/src/Facade/Exceptions/ServerErrorException.php +++ b/src/Facade/Exceptions/ServerErrorException.php @@ -13,7 +13,6 @@ **/ use RuntimeException; use Throwable; - /** * Class ServerErrorException * @package Facade\Exceptions From 6c097eb2217f74f8536450bd35bf87403a06992b Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Wed, 13 Jun 2018 14:39:44 +0200 Subject: [PATCH 15/37] Allow user to provide additional request headers --- src/Facade/CalDavClient.php | 38 +++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index 09f542d..d2d9763 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -95,19 +95,26 @@ final class CalDavClient implements ICalDavClient */ private $timeout = 60; + /** + * @var array + */ + private $headers = []; + /** * CalDavClient constructor. * @param string $server_url * @param string|null $user * @param string|null $password * @param string $authtype + * @param array $headers Additional headers to send with each request */ - public function __construct($server_url, $user = null, $password = null, $authtype = self::DefaultAuthType) + public function __construct($server_url, $user = null, $password = null, $authtype = self::DefaultAuthType, $headers=[]) { $this->server_url = $server_url; $this->user = $user; $this->password = $password; $this->authtype = $authtype; + $this->setHeaders($headers); $this->client = new Client(); } @@ -136,6 +143,15 @@ public function setAuthenticationType($authtype) { $this->authtype = $authtype; } + /** + * Set headers that will be sent with each request + * + * @param array $headers + */ + public function setHeaders($headers = []) { + $this->headers = $headers; + } + /** * @param Request $http_request * @return mixed|\Psr\Http\Message\ResponseInterface @@ -143,10 +159,22 @@ public function setAuthenticationType($authtype) { */ private function makeRequest(Request $http_request){ try{ - return $this->client->send($http_request, [ - 'auth' => [$this->user, $this->password, $this->authtype], + $options = [ 'timeout' => $this->timeout - ]); + ]; + switch (strtolower(trim($this->authtype))) { + case "basic": + case "digest": + case "ntlm": + $options['auth'] = [$this->user, $this->password, $this->authtype]; + break; + } + + if (!empty($this->headers)) { + $options['headers'] = $this->headers; + } + + return $this->client->send($http_request, $options); } catch (ClientException $ex){ switch($ex->getCode()){ @@ -155,11 +183,13 @@ private function makeRequest(Request $http_request){ break; case 403: throw new ForbiddenException(); + break; case 404: throw new NotFoundResourceException(); break; case 409: throw new ConflictException($ex->getMessage(), $ex->getCode()); + break; default: throw new ServerErrorException($ex->getMessage(), $ex->getCode()); break; From 51eebf20961655afa996a44308575f572e090a1c Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Fri, 15 Jun 2018 11:37:45 +0200 Subject: [PATCH 16/37] =?UTF-8?q?Add=20missing=20=E2=80=98getETag=E2=80=99?= =?UTF-8?q?=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To access the requested getetag prop from Facade/Requests/GetCalendarRequest.php --- src/Facade/Responses/GetCalendarResponse.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Facade/Responses/GetCalendarResponse.php b/src/Facade/Responses/GetCalendarResponse.php index 31ac3d9..3719f73 100644 --- a/src/Facade/Responses/GetCalendarResponse.php +++ b/src/Facade/Responses/GetCalendarResponse.php @@ -38,6 +38,13 @@ public function getSyncToken(){ return isset($this->found_props['sync-token']) ? $this->found_props['sync-token'] : null; } + /** + * @return string + */ + public function getETag(){ + return isset($this->found_props['getetag']) ? $this->found_props['getetag'] : null; + } + /** * @return string */ From 3350b206d18b10881a4a7af3e763f1d847f9d5e8 Mon Sep 17 00:00:00 2001 From: Sebastian Marcet Date: Tue, 9 Oct 2018 15:37:25 -0300 Subject: [PATCH 17/37] [smarcet] * fix on cdata parsing * fix on vcard response --- src/Facade/Responses/AbstractCalDAVResponse.php | 3 ++- src/Facade/Responses/VCardEntityResponse.php | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Facade/Responses/AbstractCalDAVResponse.php b/src/Facade/Responses/AbstractCalDAVResponse.php index cd6821e..883d6af 100644 --- a/src/Facade/Responses/AbstractCalDAVResponse.php +++ b/src/Facade/Responses/AbstractCalDAVResponse.php @@ -48,7 +48,8 @@ public function __construct($server_url = null, $body = null, $code = HttpRespon $this->server_url = $server_url; if(!empty($this->body)) { $this->stripped = $this->stripNamespacesFromTags($this->body); - $this->xml = simplexml_load_string($this->stripped); + // Merge CDATA as text nodes + $this->xml = simplexml_load_string($this->stripped, null, LIBXML_NOCDATA); if($this->xml === FALSE) throw new XMLResponseParseException(); $this->content = $this->toAssocArray($this->xml); diff --git a/src/Facade/Responses/VCardEntityResponse.php b/src/Facade/Responses/VCardEntityResponse.php index a0850a2..1580649 100644 --- a/src/Facade/Responses/VCardEntityResponse.php +++ b/src/Facade/Responses/VCardEntityResponse.php @@ -22,6 +22,9 @@ class VCardEntityResponse extends ETagEntityResponse * @return string */ public function getVCard(){ - return isset($this->found_props['calendar-data']) ? $this->found_props['calendar-data'] : null; + if(!isset($this->found_props['calendar-data'])) return null; + $calendar_data = $this->found_props['calendar-data']; + if(!is_string($calendar_data)) return null; + return $calendar_data; } } \ No newline at end of file From 7aac9f4268db28ebbc9c7923a0dda2df521d5a9f Mon Sep 17 00:00:00 2001 From: Sebastian Marcet Date: Wed, 31 Oct 2018 20:26:19 -0300 Subject: [PATCH 18/37] [smarcet] * added content type and content length on put request to avoid 405 on ical --- src/Facade/Utils/Headers.php | 11 +++++---- src/Facade/Utils/HttpMethods.php | 1 + src/Facade/Utils/RequestFactory.php | 36 ++++++++++++++++++++++++----- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/Facade/Utils/Headers.php b/src/Facade/Utils/Headers.php index eef4092..62286d7 100644 --- a/src/Facade/Utils/Headers.php +++ b/src/Facade/Utils/Headers.php @@ -18,9 +18,10 @@ */ final class Headers { - const Depth = 'Depth'; - const Prefer = 'Prefer'; - const ContentType = 'Content-Type'; - const IfMatch = 'If-Match'; - const IfNotMatch = 'If-None-Match"'; + const Depth = 'Depth'; + const Prefer = 'Prefer'; + const ContentType = 'Content-Type'; + const ContentLength = 'Content-Length'; + const IfMatch = 'If-Match'; + const IfNotMatch = 'If-None-Match"'; } \ No newline at end of file diff --git a/src/Facade/Utils/HttpMethods.php b/src/Facade/Utils/HttpMethods.php index 380f87b..780c775 100644 --- a/src/Facade/Utils/HttpMethods.php +++ b/src/Facade/Utils/HttpMethods.php @@ -21,6 +21,7 @@ final class HttpMethods { const Get = 'GET'; + const Post = 'POST'; const Put = 'PUT'; const Report = 'REPORT'; const PropFind = 'PROPFIND'; diff --git a/src/Facade/Utils/RequestFactory.php b/src/Facade/Utils/RequestFactory.php index 530f2a6..8e36992 100644 --- a/src/Facade/Utils/RequestFactory.php +++ b/src/Facade/Utils/RequestFactory.php @@ -49,13 +49,20 @@ private static function createHeadersFor($http_method, array $params = []){ Headers::ContentType => ContentTypes::Xml ]; case HttpMethods::Put: - $etag = $params[0]; + + $len = $params[0]; + $etag = $params[1]; + + $headers = [ + Headers::ContentLength => intval($len), + Headers::ContentType => ContentTypes::Calendar, + ]; + if(!empty($etag)){ - return [ - Headers::ContentType => ContentTypes::Calendar, - Headers::IfMatch => $etag - ]; + $headers[Headers::IfMatch] = $etag; } + + return $headers; } return []; } @@ -154,11 +161,28 @@ public static function createGetRequest($url){ * @return Request */ public static function createPutRequest($url, $body, $etag = null){ + return new Request ( HttpMethods::Put, $url, - self::createHeadersFor(HttpMethods::Put, [$etag]), + self::createHeadersFor(HttpMethods::Put, [strlen($body), $etag]), + $body + ); + } + + /** + * @param string $url + * @param string $body + * @param string $etag + * @return Request + */ + public static function createPostRequest($url, $body, $etag = null){ + return new Request + ( + HttpMethods::Post, + $url, + self::createHeadersFor(HttpMethods::Post, [$etag]), $body ); } From 0b8c77edd5a95637bc93426dc1602063091b8b4d Mon Sep 17 00:00:00 2001 From: smarcet Date: Mon, 25 Mar 2019 18:16:24 -0300 Subject: [PATCH 19/37] [smarcet] * updated dependencies --- composer.json | 2 +- composer.lock | 396 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 259 insertions(+), 139 deletions(-) diff --git a/composer.json b/composer.json index c46206a..828bdfd 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "guzzlehttp/guzzle": "^6.3", "sabre/xml": "1.5.0", "sabre/uri": "1.2.0", - "eluceo/ical": "^0.11.3" + "eluceo/ical": "^0.15.0" }, "require-dev": { "phpunit/phpunit": "^5.7" diff --git a/composer.lock b/composer.lock index 26c7d30..d78bffb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,32 +4,35 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "12d4aeb5b58557ed034f4346502994eb", + "content-hash": "ed78858be0c55f4381466fdda2e3891a", "packages": [ { "name": "eluceo/ical", - "version": "0.11.x-dev", + "version": "0.15.0", "source": { "type": "git", "url": "https://github.com/markuspoerschke/iCal.git", - "reference": "739a4e8622fa75fc77d76718300dae6805d09ff0" + "reference": "add0ca99aa1f77f134a2e8b071f2ebc22b115139" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/739a4e8622fa75fc77d76718300dae6805d09ff0", - "reference": "739a4e8622fa75fc77d76718300dae6805d09ff0", + "url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/add0ca99aa1f77f134a2e8b071f2ebc22b115139", + "reference": "add0ca99aa1f77f134a2e8b071f2ebc22b115139", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "~4.3" + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-mbstring": "Massive performance enhancement of line folding" }, "type": "library", "autoload": { - "psr-0": { - "Eluceo\\iCal": "src/" + "psr-4": { + "Eluceo\\iCal\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -37,18 +40,13 @@ "MIT" ], "authors": [ - { - "name": "Maciej Łebkowski", - "email": "m.lebkowski@gmail.com", - "role": "Contributor" - }, { "name": "Markus Poerschke", "email": "markus@eluceo.de", "role": "Developer" } ], - "description": "The eluceo/iCal package offers a abstraction layer for creating iCalendars. You can easily create iCal files by using PHP object instead of typing your *.ics file by hand. The output will follow RFC 2445 as best as possible.", + "description": "The eluceo/iCal package offers a abstraction layer for creating iCalendars. You can easily create iCal files by using PHP object instead of typing your *.ics file by hand. The output will follow RFC 5545 as best as possible.", "homepage": "https://github.com/markuspoerschke/iCal", "keywords": [ "calendar", @@ -57,31 +55,32 @@ "ics", "php calendar" ], - "time": "2017-04-25 08:33:00" + "time": "2019-01-13T22:00:58+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "6.3.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699" + "reference": "fa745406c2f48210693ba05f11280a1473037b2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699", - "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fa745406c2f48210693ba05f11280a1473037b2f", + "reference": "fa745406c2f48210693ba05f11280a1473037b2f", "shasum": "" }, "require": { + "ext-json": "*", "guzzlehttp/promises": "^1.0", "guzzlehttp/psr7": "^1.4", "php": ">=5.5" }, "require-dev": { "ext-curl": "*", - "phpunit/phpunit": "^4.0 || ^5.0", - "psr/log": "^1.0" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" }, "suggest": { "psr/log": "Required for using the Log middleware" @@ -89,16 +88,16 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "6.2-dev" + "dev-master": "6.3-dev" } }, "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { "GuzzleHttp\\": "src/" - } + }, + "files": [ + "src/functions_include.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -122,7 +121,7 @@ "rest", "web service" ], - "time": "2017-06-22T18:50:49+00:00" + "time": "2019-02-26T17:21:14+00:00" }, { "name": "guzzlehttp/promises", @@ -130,19 +129,19 @@ "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "09e549f5534380c68761260a71f847644d8f65aa" + "reference": "926eaa3ff73cde2becf652b785831bcb7618568a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/09e549f5534380c68761260a71f847644d8f65aa", - "reference": "09e549f5534380c68761260a71f847644d8f65aa", + "url": "https://api.github.com/repos/guzzle/promises/zipball/926eaa3ff73cde2becf652b785831bcb7618568a", + "reference": "926eaa3ff73cde2becf652b785831bcb7618568a", "shasum": "" }, "require": { "php": ">=5.5.0" }, "require-dev": { - "phpunit/phpunit": "^4.0" + "phpunit/phpunit": "^4.8.36" }, "type": "library", "extra": { @@ -173,7 +172,7 @@ "keywords": [ "promise" ], - "time": "2017-05-20 23:14:18" + "time": "2018-10-30T00:20:04+00:00" }, { "name": "guzzlehttp/psr7", @@ -181,28 +180,33 @@ "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "811b676fbab9c99e359885032e5ebc70e442f5b8" + "reference": "31ea59d632d3ac145300fffb2873a195172c0814" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/811b676fbab9c99e359885032e5ebc70e442f5b8", - "reference": "811b676fbab9c99e359885032e5ebc70e442f5b8", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/31ea59d632d3ac145300fffb2873a195172c0814", + "reference": "31ea59d632d3ac145300fffb2873a195172c0814", "shasum": "" }, "require": { "php": ">=5.4.0", - "psr/http-message": "~1.0" + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" }, "provide": { "psr/http-message-implementation": "1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "suggest": { + "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -232,13 +236,14 @@ "keywords": [ "http", "message", + "psr-7", "request", "response", "stream", "uri", "url" ], - "time": "2017-07-17 09:11:21" + "time": "2019-02-20T09:29:56+00:00" }, { "name": "psr/http-message", @@ -288,19 +293,59 @@ "request", "response" ], - "time": "2016-08-06 14:39:51" + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2019-03-08T08:55:37+00:00" }, { "name": "sabre/uri", "version": "1.2.0", "source": { "type": "git", - "url": "https://github.com/fruux/sabre-uri.git", + "url": "https://github.com/sabre-io/uri.git", "reference": "8545a3335f741d4b7700bb14efb41b4c03775dcd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruux/sabre-uri/zipball/8545a3335f741d4b7700bb14efb41b4c03775dcd", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/8545a3335f741d4b7700bb14efb41b4c03775dcd", "reference": "8545a3335f741d4b7700bb14efb41b4c03775dcd", "shasum": "" }, @@ -346,12 +391,12 @@ "version": "1.5.0", "source": { "type": "git", - "url": "https://github.com/fruux/sabre-xml.git", + "url": "https://github.com/sabre-io/xml.git", "reference": "59b20e5bbace9912607481634f97d05a776ffca7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruux/sabre-xml/zipball/59b20e5bbace9912607481634f97d05a776ffca7", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/59b20e5bbace9912607481634f97d05a776ffca7", "reference": "59b20e5bbace9912607481634f97d05a776ffca7", "shasum": "" }, @@ -408,32 +453,34 @@ "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.0.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "reference": "a2c590166b2133a4633738648b6b064edae0814a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", + "reference": "a2c590166b2133a4633738648b6b064edae0814a", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^7.1" }, "require-dev": { - "athletic/athletic": "~0.1.8", + "doctrine/coding-standard": "^6.0", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -453,12 +500,12 @@ } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ "constructor", "instantiate" ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2019-03-17T17:37:11+00:00" }, { "name": "myclabs/deep-copy", @@ -466,33 +513,39 @@ "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" + "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", + "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" }, "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" }, "type": "library", "autoload": { "psr-4": { "DeepCopy\\": "src/DeepCopy/" - } + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", "keywords": [ "clone", "copy", @@ -500,11 +553,11 @@ "object", "object graph" ], - "time": "2017-04-12T18:52:22+00:00" + "time": "2018-06-11T23:09:50+00:00" }, { "name": "phpdocumentor/reflection-common", - "version": "dev-master", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", @@ -558,29 +611,35 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.2.2", + "version": "4.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157" + "reference": "94fd0001232e47129dd3504189fa1c7225010d08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", + "reference": "94fd0001232e47129dd3504189fa1c7225010d08", "shasum": "" }, "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.3.0", + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0", + "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, "autoload": { "psr-4": { "phpDocumentor\\Reflection\\": [ @@ -599,20 +658,20 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-08T06:39:58+00:00" + "time": "2017-11-30T07:14:17+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.3.0", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773" + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", "shasum": "" }, "require": { @@ -646,7 +705,7 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-06-03T08:32:36+00:00" + "time": "2017-07-14T14:27:02+00:00" }, { "name": "phpspec/prophecy", @@ -654,34 +713,34 @@ "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6" + "reference": "7e272180527c34a97680de85eb5aba0847a664e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", - "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/7e272180527c34a97680de85eb5aba0847a664e0", + "reference": "7e272180527c34a97680de85eb5aba0847a664e0", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0", + "sebastian/comparator": "^1.1|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8 || ^5.6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { - "psr-0": { - "Prophecy\\": "src/" + "psr-4": { + "Prophecy\\": "src/Prophecy" } }, "notification-url": "https://packagist.org/downloads/", @@ -709,7 +768,7 @@ "spy", "stub" ], - "time": "2017-09-04T11:05:03+00:00" + "time": "2018-12-18T15:40:51+00:00" }, { "name": "phpunit/php-code-coverage", @@ -776,16 +835,16 @@ }, { "name": "phpunit/php-file-iterator", - "version": "dev-master", + "version": "1.4.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", "shasum": "" }, "require": { @@ -819,7 +878,7 @@ "filesystem", "iterator" ], - "time": "2016-10-03T07:40:28+00:00" + "time": "2017-11-27T13:52:08+00:00" }, { "name": "phpunit/php-text-template", @@ -864,16 +923,16 @@ }, { "name": "phpunit/php-timer", - "version": "dev-master", + "version": "1.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "d107f347d368dd8a384601398280c7c608390ab7" + "reference": "9513098641797ce5f459dbc1de5a54c29b0ec1fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/d107f347d368dd8a384601398280c7c608390ab7", - "reference": "d107f347d368dd8a384601398280c7c608390ab7", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/9513098641797ce5f459dbc1de5a54c29b0ec1fb", + "reference": "9513098641797ce5f459dbc1de5a54c29b0ec1fb", "shasum": "" }, "require": { @@ -909,33 +968,33 @@ "keywords": [ "timer" ], - "time": "2017-03-07T15:42:04+00:00" + "time": "2018-01-06T05:27:16+00:00" }, { "name": "phpunit/php-token-stream", - "version": "1.4.x-dev", + "version": "2.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "958103f327daef5dd0bb328dec53e0a9e43cfaf7" + "reference": "13eb9aba9626b1a3811c6a492acc9669d24bb85a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/958103f327daef5dd0bb328dec53e0a9e43cfaf7", - "reference": "958103f327daef5dd0bb328dec53e0a9e43cfaf7", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/13eb9aba9626b1a3811c6a492acc9669d24bb85a", + "reference": "13eb9aba9626b1a3811c6a492acc9669d24bb85a", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^6.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -958,20 +1017,20 @@ "keywords": [ "tokenizer" ], - "time": "2017-03-07T08:21:50+00:00" + "time": "2017-11-27T08:47:38+00:00" }, { "name": "phpunit/phpunit", - "version": "5.7.x-dev", + "version": "5.7.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4eba3374803c6c0903145e8940844e6f1d665c07" + "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4eba3374803c6c0903145e8940844e6f1d665c07", - "reference": "4eba3374803c6c0903145e8940844e6f1d665c07", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", + "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", "shasum": "" }, "require": { @@ -995,8 +1054,8 @@ "sebastian/global-state": "^1.1", "sebastian/object-enumerator": "~2.0", "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0.3|~2.0", - "symfony/yaml": "~2.1|~3.0" + "sebastian/version": "^1.0.6|^2.0.1", + "symfony/yaml": "~2.1|~3.0|~4.0" }, "conflict": { "phpdocumentor/reflection-docblock": "3.0.2" @@ -1040,7 +1099,7 @@ "testing", "xunit" ], - "time": "2017-09-01T08:38:37+00:00" + "time": "2018-02-01T05:50:59+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -1099,6 +1158,7 @@ "mock", "xunit" ], + "abandoned": true, "time": "2017-06-30T09:13:00+00:00" }, { @@ -1107,12 +1167,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "3488be0a7b346cd6e5361510ed07e88f9bea2e88" + "reference": "383c44e104c1fd46ecc915f55145bd2831318747" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/3488be0a7b346cd6e5361510ed07e88f9bea2e88", - "reference": "3488be0a7b346cd6e5361510ed07e88f9bea2e88", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/383c44e104c1fd46ecc915f55145bd2831318747", + "reference": "383c44e104c1fd46ecc915f55145bd2831318747", "shasum": "" }, "require": { @@ -1144,7 +1204,7 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04T10:23:55+00:00" + "time": "2019-02-11T12:48:46+00:00" }, { "name": "sebastian/comparator", @@ -1531,16 +1591,16 @@ }, { "name": "sebastian/resource-operations", - "version": "dev-master", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "fadc83f7c41fb2924e542635fea47ae546816ece" + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/fadc83f7c41fb2924e542635fea47ae546816ece", - "reference": "fadc83f7c41fb2924e542635fea47ae546816ece", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", "shasum": "" }, "require": { @@ -1569,11 +1629,11 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2016-10-03T07:43:09+00:00" + "time": "2015-07-28T20:34:47+00:00" }, { "name": "sebastian/version", - "version": "dev-master", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", @@ -1614,22 +1674,81 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2016-10-03T07:35:21+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "82ebae02209c21113908c229e9883c419720738a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", + "reference": "82ebae02209c21113908c229e9883c419720738a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "backendtea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2019-02-06T07:57:58+00:00" + }, { "name": "symfony/yaml", - "version": "3.4.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c076a2d6f809130a84f99616b425b9665b3ce0a5" + "reference": "2fb37042c7060535dbe08a5d61a550e5d181f3c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c076a2d6f809130a84f99616b425b9665b3ce0a5", - "reference": "c076a2d6f809130a84f99616b425b9665b3ce0a5", + "url": "https://api.github.com/repos/symfony/yaml/zipball/2fb37042c7060535dbe08a5d61a550e5d181f3c7", + "reference": "2fb37042c7060535dbe08a5d61a550e5d181f3c7", "shasum": "" }, "require": { - "php": "^5.5.9|>=7.0.8" + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8" }, "conflict": { "symfony/console": "<3.4" @@ -1643,7 +1762,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.4-dev" + "dev-master": "4.3-dev" } }, "autoload": { @@ -1670,24 +1789,25 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-09-04T13:18:59+00:00" + "time": "2019-02-23T15:22:31+00:00" }, { "name": "webmozart/assert", - "version": "dev-master", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "4a8bf11547e139e77b651365113fc12850c43d9a" + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/4a8bf11547e139e77b651365113fc12850c43d9a", - "reference": "4a8bf11547e139e77b651365113fc12850c43d9a", + "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", + "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "phpunit/phpunit": "^4.6", @@ -1720,7 +1840,7 @@ "check", "validate" ], - "time": "2016-11-23T20:04:41+00:00" + "time": "2018-12-25T11:19:39+00:00" } ], "aliases": [], From d4e88f7df1065acb5bdeb21fc8f0fab17ec697da Mon Sep 17 00:00:00 2001 From: smarcet Date: Mon, 8 Jul 2019 20:31:17 -0300 Subject: [PATCH 20/37] [smarcet] * fixes for icloud imp --- src/Facade/CalDavClient.php | 6 ++++-- src/Facade/Responses/UserPrincipalSingleResponse.php | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index d2d9763..712f873 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -229,7 +229,8 @@ public function getUserPrincipal() RequestFactory::createPropFindRequest ( $this->server_url, - CalDAVRequestFactory::getInstance()->build(CalDAVRequestFactory::PrincipalRequestType)->getContent() + CalDAVRequestFactory::getInstance()->build(CalDAVRequestFactory::PrincipalRequestType)->getContent(), + 0 ) ); @@ -247,7 +248,8 @@ public function getCalendarHome($principal_url) RequestFactory::createPropFindRequest ( $principal_url, - CalDAVRequestFactory::getInstance()->build(CalDAVRequestFactory::CalendarHomeRequestType)->getContent() + CalDAVRequestFactory::getInstance()->build(CalDAVRequestFactory::CalendarHomeRequestType)->getContent(), + 0 ) ); diff --git a/src/Facade/Responses/UserPrincipalSingleResponse.php b/src/Facade/Responses/UserPrincipalSingleResponse.php index 6bb8183..3cb0c79 100644 --- a/src/Facade/Responses/UserPrincipalSingleResponse.php +++ b/src/Facade/Responses/UserPrincipalSingleResponse.php @@ -22,7 +22,12 @@ final class UserPrincipalSingleResponse extends GenericSinglePROPFINDCalDAVRespo * @return string */ public function getPrincipalUrl() { - return isset($this->found_props['current-user-principal']) && isset($this->found_props['current-user-principal']['href']) ? + $url = isset($this->found_props['current-user-principal']) && isset($this->found_props['current-user-principal']['href']) ? $this->server_url.$this->found_props['current-user-principal']['href'] : null; + // check on not found one ( issue on caldav icloud imp) + if(empty($url)) + $url = isset($this->not_found_props['current-user-principal']) && isset($this->not_found_props['current-user-principal']['href']) ? + $this->server_url.$this->not_found_props['current-user-principal']['href'] : null; + return $url; } } \ No newline at end of file From e08f28b5f25674b073b695322256f96f5e704e0b Mon Sep 17 00:00:00 2001 From: Greg Joy Date: Tue, 11 Feb 2020 23:25:07 +0000 Subject: [PATCH 21/37] ETagEntityResponse status propstat fixes Accomodated for status being nested in propstat element in Apple ICalendar CalendarSyncInfoResponse ETagEntityResponse body --- src/Facade/Responses/ETagEntityResponse.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Facade/Responses/ETagEntityResponse.php b/src/Facade/Responses/ETagEntityResponse.php index 949d867..c1d1686 100644 --- a/src/Facade/Responses/ETagEntityResponse.php +++ b/src/Facade/Responses/ETagEntityResponse.php @@ -30,6 +30,9 @@ public function getETag(){ */ public function getStatus() { - return isset($this->content['response']['status']) ? $this->content['response']['status'] : null; + if(isset($this->content['response']['status'])) return $this->content['response']['status']; + if(isset($this->content['response']['propstat']['status'])) return $this->content['response']['propstat']['status']; + + return null; } -} \ No newline at end of file +} From 3e68132012aca74fe854adbe68bdacaacf23c560 Mon Sep 17 00:00:00 2001 From: Greg Joy Date: Mon, 10 Feb 2020 22:47:25 +0000 Subject: [PATCH 22/37] Consolidated HTTP response status checking. Yahoo returns the HTTP statuses in lowercase, which doesnt match with the uppercase HTTP statuses. --- .../GenericSinglePROPFINDCalDAVResponse.php | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Facade/Responses/GenericSinglePROPFINDCalDAVResponse.php b/src/Facade/Responses/GenericSinglePROPFINDCalDAVResponse.php index bf73454..b1d9e2b 100644 --- a/src/Facade/Responses/GenericSinglePROPFINDCalDAVResponse.php +++ b/src/Facade/Responses/GenericSinglePROPFINDCalDAVResponse.php @@ -46,15 +46,15 @@ protected function parse() if (isset($this->content['response']['propstat']['prop']) && isset($this->content['response']['propstat']['status'])) { // all props found $status = $this->content['response']['propstat']['status']; - if ($status == AbstractCalDAVResponse::HttpOKStatus) { + if ($this->statusMatches($status, AbstractCalDAVResponse::HttpOKStatus)) { $this->found_props = $this->content['response']['propstat']['prop']; $this->not_found_props = null; } - if ($status == AbstractCalDAVResponse::HttpNotFoundStatus) { + if ($this->statusMatches($status, AbstractCalDAVResponse::HttpNotFoundStatus)) { $this->not_found_props = $this->content['response']['propstat']['prop']; $this->found_props = null; } - if ($status == AbstractCalDAVResponse::HttpForbiddenStatus) { + if ($this->statusMatches($status, AbstractCalDAVResponse::HttpForbiddenStatus)) { throw new ForbiddenQueryException(); } return $this; @@ -64,15 +64,25 @@ protected function parse() if (!isset($propstat['status']) || !isset($propstat['prop'])) continue; - if ($propstat['status'] == AbstractCalDAVResponse::HttpOKStatus) + if ($this->statusMatches($propstat['status'], AbstractCalDAVResponse::HttpOKStatus)) $this->found_props = $propstat['prop']; - if ($propstat['status'] == AbstractCalDAVResponse::HttpNotFoundStatus) + if ($this->statusMatches($propstat['status'], AbstractCalDAVResponse::HttpNotFoundStatus)) $this->not_found_props = $propstat['prop']; } return $this; } + /** + * @param string $responseStatus + * @param string $desiredStatus + * @return bool + */ + protected function statusMatches($responseStatus, $desiredStatus) + { + return strtoupper($responseStatus) == $desiredStatus; + } + /** * @return bool */ @@ -96,4 +106,4 @@ public function isSuccessFull() { return $this->code == HttpResponse::HttpCodeMultiResponse; } -} \ No newline at end of file +} From 93b20f71b5c039032ece6a269684445d9be294f5 Mon Sep 17 00:00:00 2001 From: Scrutinizer Auto-Fixer Date: Thu, 13 Feb 2020 18:01:35 +0000 Subject: [PATCH 23/37] Scrutinizer Auto-Fixes This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com --- .../Responses/AbstractCalDAVResponse.php | 18 +++++++++--------- src/Facade/Responses/VCardEntityResponse.php | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Facade/Responses/AbstractCalDAVResponse.php b/src/Facade/Responses/AbstractCalDAVResponse.php index 883d6af..d61d0e2 100644 --- a/src/Facade/Responses/AbstractCalDAVResponse.php +++ b/src/Facade/Responses/AbstractCalDAVResponse.php @@ -42,15 +42,15 @@ abstract class AbstractCalDAVResponse extends HttpResponse * @param string|null $body * @param int $code */ - public function __construct($server_url = null, $body = null, $code = HttpResponse::HttpCodeOk ) + public function __construct($server_url = null, $body = null, $code = HttpResponse::HttpCodeOk) { parent::__construct($body, $code); $this->server_url = $server_url; - if(!empty($this->body)) { + if (!empty($this->body)) { $this->stripped = $this->stripNamespacesFromTags($this->body); // Merge CDATA as text nodes $this->xml = simplexml_load_string($this->stripped, null, LIBXML_NOCDATA); - if($this->xml === FALSE) + if ($this->xml === FALSE) throw new XMLResponseParseException(); $this->content = $this->toAssocArray($this->xml); @@ -62,7 +62,7 @@ public function __destruct() { } - protected function setContent($content){ + protected function setContent($content) { $this->content = $content; } @@ -95,13 +95,13 @@ private function stripNamespacesFromTags($xml) { $nameSpaceDefRegEx = '(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?'; // Cycle through each namespace and remove it from the XML string - foreach( $toRemove as $remove ) { + foreach ($toRemove as $remove) { // First remove the namespace from the opening of the tag - $xml = str_replace('<' . $remove . ':', '<', $xml); + $xml = str_replace('<'.$remove.':', '<', $xml); // Now remove the namespace from the closing of the tag - $xml = str_replace('content['response']); } } \ No newline at end of file diff --git a/src/Facade/Responses/VCardEntityResponse.php b/src/Facade/Responses/VCardEntityResponse.php index 1580649..2b4d4ec 100644 --- a/src/Facade/Responses/VCardEntityResponse.php +++ b/src/Facade/Responses/VCardEntityResponse.php @@ -21,10 +21,10 @@ class VCardEntityResponse extends ETagEntityResponse /** * @return string */ - public function getVCard(){ - if(!isset($this->found_props['calendar-data'])) return null; + public function getVCard() { + if (!isset($this->found_props['calendar-data'])) return null; $calendar_data = $this->found_props['calendar-data']; - if(!is_string($calendar_data)) return null; + if (!is_string($calendar_data)) return null; return $calendar_data; } } \ No newline at end of file From c3f711c449d55ff00e2c203cadda89c78d7e03a5 Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:27:23 +0200 Subject: [PATCH 24/37] feat: add calendar color prop --- src/Facade/Requests/GetCalendarsRequest.php | 3 ++- src/Facade/Responses/GetCalendarResponse.php | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Facade/Requests/GetCalendarsRequest.php b/src/Facade/Requests/GetCalendarsRequest.php index 05d0039..1e397a2 100644 --- a/src/Facade/Requests/GetCalendarsRequest.php +++ b/src/Facade/Requests/GetCalendarsRequest.php @@ -21,8 +21,9 @@ public function __construct(){ $this->properties = [ '{DAV:}resourcetype', '{DAV:}displayname', + '{http://apple.com/ns/ical/}calendar-color', '{http://calendarserver.org/ns/}getctag', '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set', ]; } -} \ No newline at end of file +} diff --git a/src/Facade/Responses/GetCalendarResponse.php b/src/Facade/Responses/GetCalendarResponse.php index 3719f73..a9cc043 100644 --- a/src/Facade/Responses/GetCalendarResponse.php +++ b/src/Facade/Responses/GetCalendarResponse.php @@ -52,5 +52,10 @@ public function getCTag(){ return isset($this->found_props['getctag']) ? $this->found_props['getctag'] : null; } - -} \ No newline at end of file + /** + * @return string + */ + public function getCalendarColor(){ + return isset($this->found_props['calendar-color']) ? $this->found_props['calendar-color'] : null; + } +} From bbd7e657423d837dd3d4d76b7726d519c44c0b73 Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Sat, 8 Nov 2025 22:02:39 +0100 Subject: [PATCH 25/37] feat(caldav): add component-set parser to detect VTODO calendars - Add getSupportedComponents() method to parse component types - Add supportsEvents() helper to check VEVENT support - Enables filtering of VTODO-only calendars from event views --- src/Facade/Responses/GetCalendarResponse.php | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/Facade/Responses/GetCalendarResponse.php b/src/Facade/Responses/GetCalendarResponse.php index a9cc043..7fb6985 100644 --- a/src/Facade/Responses/GetCalendarResponse.php +++ b/src/Facade/Responses/GetCalendarResponse.php @@ -58,4 +58,53 @@ public function getCTag(){ public function getCalendarColor(){ return isset($this->found_props['calendar-color']) ? $this->found_props['calendar-color'] : null; } + + /** + * Get supported calendar component types (VEVENT, VTODO, VJOURNAL, etc.) + * Returns array of component names that this calendar supports + * + * @return array|null Array of component names like ['VEVENT', 'VTODO'] or null + */ + public function getSupportedComponents() + { + if (!isset($this->found_props['supported-calendar-component-set'])) { + return null; + } + + $componentSet = $this->found_props['supported-calendar-component-set']; + $components = []; + + // Handle single component (most common case) + // Structure: {"comp": {"@attributes": {"name": "VEVENT"}}} + if (isset($componentSet['comp']['@attributes']['name'])) { + $components[] = $componentSet['comp']['@attributes']['name']; + } + // Handle array of components (less common) + // Structure: [{"@attributes": {"name": "VEVENT"}}, {"@attributes": {"name": "VTODO"}}] + elseif (is_array($componentSet)) { + foreach ($componentSet as $key => $comp) { + // Check for @attributes.name pattern + if (isset($comp['@attributes']['name'])) { + $components[] = $comp['@attributes']['name']; + } + // Also check direct name property as fallback + elseif (isset($comp['name'])) { + $components[] = $comp['name']; + } + } + } + + return empty($components) ? null : $components; + } + + /** + * Check if calendar supports VEVENT components (calendar events) + * + * @return bool + */ + public function supportsEvents() + { + $components = $this->getSupportedComponents(); + return $components !== null && in_array('VEVENT', $components); + } } From a3519ab469ea1b02dd5dc3ca0f4cbce9a10e8178 Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Sat, 8 Nov 2025 22:03:22 +0100 Subject: [PATCH 26/37] feat(caldav): add privilege-set parser to detect read-only calendars - Add current-user-privilege-set to GetCalendarsRequest - Add getCurrentUserPrivileges() method to parse user permissions - Add isWritable() helper to check write access - Enables UI indicators for read-only calendars --- src/Facade/Requests/GetCalendarsRequest.php | 1 + src/Facade/Responses/GetCalendarResponse.php | 59 ++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/Facade/Requests/GetCalendarsRequest.php b/src/Facade/Requests/GetCalendarsRequest.php index 1e397a2..0f1092f 100644 --- a/src/Facade/Requests/GetCalendarsRequest.php +++ b/src/Facade/Requests/GetCalendarsRequest.php @@ -24,6 +24,7 @@ public function __construct(){ '{http://apple.com/ns/ical/}calendar-color', '{http://calendarserver.org/ns/}getctag', '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set', + '{DAV:}current-user-privilege-set', ]; } } diff --git a/src/Facade/Responses/GetCalendarResponse.php b/src/Facade/Responses/GetCalendarResponse.php index 7fb6985..0031b80 100644 --- a/src/Facade/Responses/GetCalendarResponse.php +++ b/src/Facade/Responses/GetCalendarResponse.php @@ -107,4 +107,63 @@ public function supportsEvents() $components = $this->getSupportedComponents(); return $components !== null && in_array('VEVENT', $components); } + + /** + * Get current user's privilege set for this calendar + * + * @return array|null Array of privilege names like ['read', 'write'] or null + */ + public function getCurrentUserPrivileges() + { + if (!isset($this->found_props['current-user-privilege-set'])) { + return null; + } + + $privilegeSet = $this->found_props['current-user-privilege-set']; + $privileges = []; + + // The property contains nested privilege elements + // Each privilege element contains a privilege name like 'write', 'read', etc. + if (is_array($privilegeSet)) { + foreach ($privilegeSet as $privilege) { + if (is_array($privilege) && isset($privilege['privilege'])) { + // Privilege can be array of privilege names or single value + if (is_array($privilege['privilege'])) { + $privileges = array_merge($privileges, array_keys($privilege['privilege'])); + } else { + $privileges[] = $privilege['privilege']; + } + } + } + } + + return empty($privileges) ? null : $privileges; + } + + /** + * Check if user has write permission to this calendar + * + * @return bool|null True if writable, false if read-only, null if unknown + */ + public function isWritable() + { + $privileges = $this->getCurrentUserPrivileges(); + + if ($privileges === null) { + // If privileges not provided, assume writable (conservative default) + return null; + } + + // Check for write-related privileges + // DAV spec defines: write, write-content, write-properties, bind, unbind + $writePrivileges = ['write', 'write-content', 'bind']; + + foreach ($writePrivileges as $writePriv) { + if (in_array($writePriv, $privileges)) { + return true; + } + } + + return false; + } } From 802cc1930c2f7c1d3211c1ef876ef1fad30bd04d Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Thu, 13 Nov 2025 09:09:55 +0100 Subject: [PATCH 27/37] test: add sanitized iCloud CalDAV fixtures for workaround migration --- tests/fixtures/events/event-events_test.ics | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/fixtures/events/event-events_test.ics diff --git a/tests/fixtures/events/event-events_test.ics b/tests/fixtures/events/event-events_test.ics new file mode 100644 index 0000000..7065d62 --- /dev/null +++ b/tests/fixtures/events/event-events_test.ics @@ -0,0 +1,20 @@ +Array +( + [0] => CalDAVClient\Facade\Responses\VCardEntityResponse Object + ( + [vcard:protected] => BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-EVENT-12345678@icloud.com +DTSTART;VALUE=DATE:20250115 +DTEND;VALUE=DATE:20250116 +SUMMARY:Test Event +CREATED:20250113T000000Z +DTSTAMP:20250113T000000Z +LAST-MODIFIED:20250113T000000Z +END:VEVENT +END:VCALENDAR + ) +) From 195d05825025ed01da2278f6fc7c05bd4b3e842d Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Thu, 13 Nov 2025 09:15:43 +0100 Subject: [PATCH 28/37] feat(caldav): add canEdit() convenience method to GetCalendarResponse --- src/Facade/Responses/GetCalendarResponse.php | 15 +++++ tests/GetCalendarResponseTest.php | 63 ++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 tests/GetCalendarResponseTest.php diff --git a/src/Facade/Responses/GetCalendarResponse.php b/src/Facade/Responses/GetCalendarResponse.php index 0031b80..e234623 100644 --- a/src/Facade/Responses/GetCalendarResponse.php +++ b/src/Facade/Responses/GetCalendarResponse.php @@ -166,4 +166,19 @@ public function isWritable() return false; } + + /** + * Check if user can edit this calendar + * Convenience method that wraps isWritable() with null handling + * Returns true if writable or unknown (null), false if explicitly read-only + * + * @return bool + */ + public function canEdit() + { + $isWritable = $this->isWritable(); + // Handle null (server doesn't expose privileges) as true + // This maintains backward compatibility with CalDAV servers that don't provide privilege info + return $isWritable !== false; + } } diff --git a/tests/GetCalendarResponseTest.php b/tests/GetCalendarResponseTest.php new file mode 100644 index 0000000..86545f4 --- /dev/null +++ b/tests/GetCalendarResponseTest.php @@ -0,0 +1,63 @@ +getProperty('found_props'); + $property->setAccessible(true); + $property->setValue($response, $props); + } + + public function testCanEditReturnsTrueWhenWritable() + { + $response = new GetCalendarResponse(); + $this->setFoundProps($response, [ + 'current-user-privilege-set' => [ + ['privilege' => ['write' => []]] + ] + ]); + + $this->assertTrue($response->canEdit()); + } + + public function testCanEditReturnsTrueWhenPrivilegesUnknown() + { + $response = new GetCalendarResponse(); + $this->setFoundProps($response, []); + + $this->assertTrue($response->canEdit()); + } + + public function testCanEditReturnsFalseWhenReadOnly() + { + $response = new GetCalendarResponse(); + $this->setFoundProps($response, [ + 'current-user-privilege-set' => [ + ['privilege' => ['read' => []]] + ] + ]); + + $this->assertFalse($response->canEdit()); + } +} From 38776216b0240b2942ee6d4fcc0ab3c5e7b81b2c Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Thu, 13 Nov 2025 09:22:18 +0100 Subject: [PATCH 29/37] feat(caldav): add getCredentials() method to CalDavClient --- src/Facade/CalDavClient.php | 13 +++++++++++++ tests/CalDavClientTest.php | 38 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 tests/CalDavClientTest.php diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index 712f873..9dcd0b9 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -139,6 +139,19 @@ public function setCredentials($username, $password) $this->password = $password; } + /** + * Get current credentials + * + * @return array ['user' => string, 'password' => string] + */ + public function getCredentials() + { + return [ + 'user' => $this->user, + 'password' => $this->password + ]; + } + public function setAuthenticationType($authtype) { $this->authtype = $authtype; } diff --git a/tests/CalDavClientTest.php b/tests/CalDavClientTest.php new file mode 100644 index 0000000..10e20cf --- /dev/null +++ b/tests/CalDavClientTest.php @@ -0,0 +1,38 @@ +getCredentials(); + + $this->assertArrayHasKey('user', $credentials); + $this->assertArrayHasKey('password', $credentials); + $this->assertEquals('testuser', $credentials['user']); + $this->assertEquals('testpass', $credentials['password']); + } +} From 1b858b14815c7d8aba089ed9bfa07dda9cf82690 Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:41:02 +0100 Subject: [PATCH 30/37] feat(caldav): add parseVAlarms() with RFC 5545 support and X-APPLE-DEFAULT-ALARM --- composer.json | 3 +- composer.lock | 2008 ++++++++++++----- src/Facade/Responses/GetCalendarResponse.php | 67 + tests/VAlarmParsingTest.php | 79 + .../events/event-with-absolute-alarm.ics | 17 + .../events/event-with-default-alarm.ics | 18 + .../events/event-with-mixed-alarm.ics | 17 + .../fixtures/events/event-with-pt0s-alarm.ics | 17 + 8 files changed, 1630 insertions(+), 596 deletions(-) create mode 100644 tests/VAlarmParsingTest.php create mode 100644 tests/fixtures/events/event-with-absolute-alarm.ics create mode 100644 tests/fixtures/events/event-with-default-alarm.ics create mode 100644 tests/fixtures/events/event-with-mixed-alarm.ics create mode 100644 tests/fixtures/events/event-with-pt0s-alarm.ics diff --git a/composer.json b/composer.json index 828bdfd..fec8ca6 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "eluceo/ical": "^0.15.0" }, "require-dev": { - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^9.5", + "sabre/vobject": "^4.2" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index d78bffb..6190868 100644 --- a/composer.lock +++ b/composer.lock @@ -1,23 +1,23 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ed78858be0c55f4381466fdda2e3891a", + "content-hash": "e11cb6bb4d66009933955b771f6f1beb", "packages": [ { "name": "eluceo/ical", - "version": "0.15.0", + "version": "0.15.1", "source": { "type": "git", "url": "https://github.com/markuspoerschke/iCal.git", - "reference": "add0ca99aa1f77f134a2e8b071f2ebc22b115139" + "reference": "bdd24747587f6f9b10770a7b873a13e273f85f39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/add0ca99aa1f77f134a2e8b071f2ebc22b115139", - "reference": "add0ca99aa1f77f134a2e8b071f2ebc22b115139", + "url": "https://api.github.com/repos/markuspoerschke/iCal/zipball/bdd24747587f6f9b10770a7b873a13e273f85f39", + "reference": "bdd24747587f6f9b10770a7b873a13e273f85f39", "shasum": "" }, "require": { @@ -55,27 +55,32 @@ "ics", "php calendar" ], - "time": "2019-01-13T22:00:58+00:00" + "support": { + "issues": "https://github.com/markuspoerschke/iCal/issues", + "source": "https://github.com/markuspoerschke/iCal" + }, + "time": "2019-08-06T20:33:43+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "dev-master", + "version": "6.5.x-dev", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "fa745406c2f48210693ba05f11280a1473037b2f" + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/fa745406c2f48210693ba05f11280a1473037b2f", - "reference": "fa745406c2f48210693ba05f11280a1473037b2f", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4", - "php": ">=5.5" + "guzzlehttp/psr7": "^1.9", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17" }, "require-dev": { "ext-curl": "*", @@ -88,26 +93,56 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "6.3-dev" + "dev-master": "6.5-dev" } }, "autoload": { - "psr-4": { - "GuzzleHttp\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle is a PHP HTTP client library", @@ -121,71 +156,117 @@ "rest", "web service" ], - "time": "2019-02-26T17:21:14+00:00" + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5.8" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-06-20T22:16:07+00:00" }, { "name": "guzzlehttp/promises", - "version": "dev-master", + "version": "1.5.x-dev", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "926eaa3ff73cde2becf652b785831bcb7618568a" + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/926eaa3ff73cde2becf652b785831bcb7618568a", - "reference": "926eaa3ff73cde2becf652b785831bcb7618568a", + "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e", + "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e", "shasum": "" }, "require": { - "php": ">=5.5.0" + "php": ">=5.5" }, "require-dev": { - "phpunit/phpunit": "^4.8.36" + "symfony/phpunit-bridge": "^4.4 || ^5.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle promises library", "keywords": [ "promise" ], - "time": "2018-10-30T00:20:04+00:00" + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-05-21T12:31:43+00:00" }, { "name": "guzzlehttp/psr7", - "version": "dev-master", + "version": "1.9.x-dev", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "31ea59d632d3ac145300fffb2873a195172c0814" + "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/31ea59d632d3ac145300fffb2873a195172c0814", - "reference": "31ea59d632d3ac145300fffb2873a195172c0814", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b", + "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b", "shasum": "" }, "require": { @@ -198,37 +279,53 @@ }, "require-dev": { "ext-zlib": "*", - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" }, "suggest": { - "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5-dev" - } - }, "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, { "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" } ], @@ -243,29 +340,47 @@ "uri", "url" ], - "time": "2019-02-20T09:29:56+00:00" + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.9" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-04-17T16:00:37+00:00" }, { "name": "psr/http-message", - "version": "dev-master", + "version": "1.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", - "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -293,7 +408,10 @@ "request", "response" ], - "time": "2016-08-06T14:39:51+00:00" + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" }, { "name": "ralouphie/getallheaders", @@ -333,6 +451,10 @@ } ], "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, "time": "2019-03-08T08:55:37+00:00" }, { @@ -384,6 +506,11 @@ "uri", "url" ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, "time": "2016-12-07T01:17:59+00:00" }, { @@ -414,13 +541,13 @@ }, "type": "library", "autoload": { - "psr-4": { - "Sabre\\Xml\\": "lib/" - }, "files": [ "lib/Deserializer/functions.php", "lib/Serializer/functions.php" - ] + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -447,45 +574,48 @@ "dom", "xml" ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, "time": "2016-10-09T22:57:52+00:00" - } - ], - "packages-dev": [ + }, { - "name": "doctrine/instantiator", - "version": "dev-master", + "name": "symfony/polyfill-intl-idn", + "version": "1.x-dev", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "a2c590166b2133a4633738648b6b064edae0814a" + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a", - "reference": "a2c590166b2133a4633738648b6b064edae0814a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, - "require-dev": { - "doctrine/coding-standard": "^6.0", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.13", - "phpstan/phpstan-phpunit": "^0.11", - "phpstan/phpstan-shim": "^0.11", - "phpunit/phpunit": "^7.0" + "suggest": { + "ext-intl": "For best performance" }, + "default-branch": true, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "Symfony\\Polyfill\\Intl\\Idn\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -494,98 +624,170 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", "keywords": [ - "constructor", - "instantiate" + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } ], - "time": "2019-03-17T17:37:11+00:00" + "time": "2024-09-10T14:38:51+00:00" }, { - "name": "myclabs/deep-copy", + "name": "symfony/polyfill-intl-normalizer", "version": "1.x-dev", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", - "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": "^7.1" - }, - "replace": { - "myclabs/deep-copy": "self.version" + "php": ">=7.2" }, - "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "suggest": { + "ext-intl": "For best performance" }, + "default-branch": true, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "DeepCopy\\": "src/DeepCopy/" + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, - "files": [ - "src/DeepCopy/deep_copy.php" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" ], - "time": "2018-06-11T23:09:50+00:00" - }, + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + } + ], + "packages-dev": [ { - "name": "phpdocumentor/reflection-common", - "version": "1.0.1", + "name": "doctrine/instantiator", + "version": "2.0.x-dev", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + "url": "https://github.com/doctrine/instantiator.git", + "reference": "95fe13ebc346414c4e218bd827f475779096ab7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/95fe13ebc346414c4e218bd827f475779096ab7a", + "reference": "95fe13ebc346414c4e218bd827f475779096ab7a", "shasum": "" }, "require": { - "php": ">=5.5" + "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "^4.6" + "doctrine/coding-standard": "^14", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" }, + "default-branch": true, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" } }, "notification-url": "https://packagist.org/downloads/", @@ -594,218 +796,316 @@ ], "authors": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.x" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } ], - "time": "2017-09-11T18:02:19+00:00" + "time": "2025-11-03T21:45:58+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "4.3.0", + "name": "myclabs/deep-copy", + "version": "1.x-dev", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { - "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", - "webmozart/assert": "^1.0" + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { - "doctrine/instantiator": "~1.0.5", - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^6.4" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, + "default-branch": true, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" } ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-30T07:14:17+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "0.4.0", + "name": "nikic/php-parser", + "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "c97b23dce761ab3c913ee8ac5879af7a358f88de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c97b23dce761ab3c913ee8ac5879af7a358f88de", + "reference": "c97b23dce761ab3c913ee8ac5879af7a358f88de", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" }, + "default-branch": true, + "bin": [ + "bin/php-parse" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "5.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "PhpParser\\": "lib/PhpParser" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" + "name": "Nikita Popov" } ], - "time": "2017-07-14T14:27:02+00:00" + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/master" + }, + "time": "2025-10-26T16:58:55+00:00" }, { - "name": "phpspec/prophecy", + "name": "phar-io/manifest", "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "7e272180527c34a97680de85eb5aba0847a664e0" + "url": "https://github.com/phar-io/manifest.git", + "reference": "65f90285728eae4eae313b8b6ba11b2f5436038e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/7e272180527c34a97680de85eb5aba0847a664e0", - "reference": "7e272180527c34a97680de85eb5aba0847a664e0", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/65f90285728eae4eae313b8b6ba11b2f5436038e", + "reference": "65f90285728eae4eae313b8b6ba11b2f5436038e", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0|^3.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/master" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" } + ], + "time": "2025-07-05T08:48:25+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" }, { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" } ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2018-12-18T15:40:51+00:00" + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "4.0.x-dev", + "version": "9.2.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + "reference": "0448d60087a382392a1b2a1abe434466e03dcc87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/0448d60087a382392a1b2a1abe434466e03dcc87", + "reference": "0448d60087a382392a1b2a1abe434466e03dcc87", "shasum": "" }, "require": { "ext-dom": "*", + "ext-libxml": "*", "ext-xmlwriter": "*", - "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "^1.3", - "phpunit/php-text-template": "^1.2", - "phpunit/php-token-stream": "^1.4.2 || ^2.0", - "sebastian/code-unit-reverse-lookup": "^1.0", - "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "^1.0 || ^2.0" + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "ext-xdebug": "^2.1.4", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^9.6" }, "suggest": { - "ext-xdebug": "^2.5.1" + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -820,7 +1120,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -831,29 +1131,43 @@ "testing", "xunit" ], - "time": "2017-04-02T07:44:40+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-31T05:58:25+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.x-dev", + "version": "3.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + "reference": "38b24367e1b340aa78b96d7cab042942d917bb84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/38b24367e1b340aa78b96d7cab042942d917bb84", + "reference": "38b24367e1b340aa78b96d7cab042942d917bb84", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -868,7 +1182,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -878,26 +1192,48 @@ "filesystem", "iterator" ], - "time": "2017-11-27T13:52:08+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-11T16:23:04+00:00" }, { - "name": "phpunit/php-text-template", - "version": "1.2.1", + "name": "phpunit/php-invoker", + "version": "3.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -914,37 +1250,47 @@ "role": "lead" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", "keywords": [ - "template" + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2015-06-21T13:50:34+00:00" + "time": "2020-09-28T05:58:55+00:00" }, { - "name": "phpunit/php-timer", - "version": "1.0.x-dev", + "name": "phpunit/php-text-template", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "9513098641797ce5f459dbc1de5a54c29b0ec1fb" + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/9513098641797ce5f459dbc1de5a54c29b0ec1fb", - "reference": "9513098641797ce5f459dbc1de5a54c29b0ec1fb", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -959,42 +1305,51 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ - "timer" + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2018-01-06T05:27:16+00:00" + "time": "2020-10-26T05:33:50+00:00" }, { - "name": "phpunit/php-token-stream", - "version": "2.0.x-dev", + "name": "phpunit/php-timer", + "version": "5.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "13eb9aba9626b1a3811c6a492acc9669d24bb85a" + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/13eb9aba9626b1a3811c6a492acc9669d24bb85a", - "reference": "13eb9aba9626b1a3811c6a492acc9669d24bb85a", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", "shasum": "" }, "require": { - "ext-tokenizer": "*", - "php": "^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^6.2.4" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -1009,63 +1364,73 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ - "tokenizer" + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2017-11-27T08:47:38+00:00" + "time": "2020-10-26T13:16:10+00:00" }, { "name": "phpunit/phpunit", - "version": "5.7.27", + "version": "9.6.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c" + "reference": "a8e464b3a3caee314b36642a6c0365e97fa49f2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", - "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a8e464b3a3caee314b36642a6c0365e97fa49f2e", + "reference": "a8e464b3a3caee314b36642a6c0365e97fa49f2e", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.6.2", - "phpunit/php-code-coverage": "^4.0.4", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "^1.2.4", - "sebastian/diff": "^1.4.3", - "sebastian/environment": "^1.3.4 || ^2.0", - "sebastian/exporter": "~2.0", - "sebastian/global-state": "^1.1", - "sebastian/object-enumerator": "~2.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "^1.0.6|^2.0.1", - "symfony/yaml": "~2.1|~3.0|~4.0" - }, - "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" - }, - "require-dev": { - "ext-pdo": "*" + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.9", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" }, "suggest": { - "ext-xdebug": "*", - "phpunit/php-invoker": "~1.1" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "bin": [ "phpunit" @@ -1073,10 +1438,13 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.7.x-dev" + "dev-master": "9.6-dev" } }, "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], "classmap": [ "src/" ] @@ -1099,47 +1467,74 @@ "testing", "xunit" ], - "time": "2018-02-01T05:50:59+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-11-13T05:37:27+00:00" }, { - "name": "phpunit/phpunit-mock-objects", - "version": "3.4.x-dev", + "name": "sabre/vobject", + "version": "4.2.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" + "url": "https://github.com/sabre-io/vobject.git", + "reference": "449616b2d45b95c8973975de23f34a3d14f63b4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", - "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/449616b2d45b95c8973975de23f34a3d14f63b4b", + "reference": "449616b2d45b95c8973975de23f34a3d14f63b4b", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2 || ^2.0" - }, - "conflict": { - "phpunit/phpunit": "<5.4.0" + "ext-mbstring": "*", + "php": ">=5.5", + "sabre/xml": ">=1.5 <3.0" }, "require-dev": { - "phpunit/phpunit": "^5.4" + "phpunit/phpunit": "> 4.8.35, <6.0.0" }, "suggest": { - "ext-soap": "*" + "hoa/bench": "If you would like to run the benchmark scripts" }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev" + "dev-master": "4.0.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Sabre\\VObject\\": "lib/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1147,44 +1542,85 @@ ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" } ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", "keywords": [ - "mock", - "xunit" - ], - "abandoned": true, - "time": "2017-06-30T09:13:00+00:00" + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "time": "2020-01-14T10:18:45+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "dev-master", + "name": "sebastian/cli-parser", + "version": "1.0.x-dev", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "383c44e104c1fd46ecc915f55145bd2831318747" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/383c44e104c1fd46ecc915f55145bd2831318747", - "reference": "383c44e104c1fd46ecc915f55145bd2831318747", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -1199,39 +1635,48 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2019-02-11T12:48:46+00:00" + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" }, { - "name": "sebastian/comparator", - "version": "1.2.x-dev", + "name": "sebastian/code-unit", + "version": "1.0.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "18a5d97c25f408f48acaf6d1b9f4079314c5996a" + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/18a5d97c25f408f48acaf6d1b9f4079314c5996a", - "reference": "18a5d97c25f408f48acaf6d1b9f4079314c5996a", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -1244,56 +1689,50 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2017-03-07T10:34:43+00:00" + "time": "2020-10-26T13:08:54+00:00" }, { - "name": "sebastian/diff", - "version": "1.4.x-dev", + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1306,46 +1745,51 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2017-05-22T07:24:03+00:00" + "time": "2020-09-28T05:30:19+00:00" }, { - "name": "sebastian/environment", - "version": "2.0.x-dev", + "name": "sebastian/comparator", + "version": "4.0.x-dev", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^5.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1361,43 +1805,263 @@ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" } ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ - "Xdebug", - "environment", - "hhvm" + "comparator", + "compare", + "equality" ], - "time": "2016-11-26T07:53:53+00:00" - }, - { - "name": "sebastian/exporter", - "version": "2.0.x-dev", - "source": { + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:51:50+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.x-dev", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.x-dev", + "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "5e8e30670c3f36481e75211dbbcfd029a41ebf07" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/5e8e30670c3f36481e75211dbbcfd029a41ebf07", - "reference": "5e8e30670c3f36481e75211dbbcfd029a41ebf07", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0", - "sebastian/recursion-context": "^2.0" + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1410,6 +2074,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -1418,46 +2086,67 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], - "time": "2017-03-07T10:36:49+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "1.1.x-dev", + "version": "5.0.x-dev", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "cea85a84b00f2795341ebbbca4fa396347f2494e" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/cea85a84b00f2795341ebbbca4fa396347f2494e", - "reference": "cea85a84b00f2795341ebbbca4fa396347f2494e", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "phpunit/phpunit": "~4.2|~5.0" + "ext-dom": "*", + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-uopz": "*" @@ -1465,7 +2154,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "5.0-dev" } }, "autoload": { @@ -1488,33 +2177,55 @@ "keywords": [ "global state" ], - "time": "2017-02-23T14:11:06+00:00" + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:10:35+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "2.0.x-dev", + "name": "sebastian/lines-of-code", + "version": "1.0.x-dev", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "c956fe7a68318639f694fc6bba0c89b7cdf1b08c" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/c956fe7a68318639f694fc6bba0c89b7cdf1b08c", - "reference": "c956fe7a68318639f694fc6bba0c89b7cdf1b08c", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", - "sebastian/recursion-context": "^2.0" + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -1529,37 +2240,50 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-03-07T10:37:45+00:00" + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" }, { - "name": "sebastian/recursion-context", - "version": "2.0.x-dev", + "name": "sebastian/object-enumerator", + "version": "4.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "7e4d7c56f6e65d215f71ad913a5256e5439aca1c" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/7e4d7c56f6e65d215f71ad913a5256e5439aca1c", - "reference": "7e4d7c56f6e65d215f71ad913a5256e5439aca1c", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1572,44 +2296,49 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" - }, + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ { - "name": "Adam Harvey", - "email": "aharvey@php.net" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2017-03-08T08:21:15+00:00" + "time": "2020-10-26T13:12:34+00:00" }, { - "name": "sebastian/resource-operations", - "version": "1.0.0", + "name": "sebastian/object-reflector", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", "shasum": "" }, "require": { - "php": ">=5.6.0" + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1627,31 +2356,44 @@ "email": "sebastian@phpunit.de" } ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28T20:34:47+00:00" + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" }, { - "name": "sebastian/version", - "version": "2.0.1", + "name": "sebastian/recursion-context", + "version": "4.0.x-dev", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -1666,188 +2408,264 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03T07:35:21+00:00" + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T06:57:39+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "dev-master", + "name": "sebastian/resource-operations", + "version": "dev-main", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "82ebae02209c21113908c229e9883c419720738a" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ff553e7482dcee39fa4acc2b175d6ddeb0f7bc25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", - "reference": "82ebae02209c21113908c229e9883c419720738a", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ff553e7482dcee39fa4acc2b175d6ddeb0f7bc25", + "reference": "ff553e7482dcee39fa4acc2b175d6ddeb0f7bc25", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.3" }, - "suggest": { - "ext-ctype": "For best performance" + "require-dev": { + "phpunit/phpunit": "^9.0" }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-main": "3.0-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, - { - "name": "Gert de Pagter", - "email": "backendtea@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/main" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2024-03-14T18:47:08+00:00" }, { - "name": "symfony/yaml", - "version": "dev-master", + "name": "sebastian/type", + "version": "3.2.x-dev", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "2fb37042c7060535dbe08a5d61a550e5d181f3c7" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/2fb37042c7060535dbe08a5d61a550e5d181f3c7", - "reference": "2fb37042c7060535dbe08a5d61a550e5d181f3c7", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/console": "<3.4" + "php": ">=7.3" }, "require-dev": { - "symfony/console": "~3.4|~4.0" - }, - "suggest": { - "symfony/console": "For validating YAML files using the lint command" + "phpunit/phpunit": "^9.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.3-dev" + "dev-master": "3.2-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2" + }, + "funding": [ { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2019-02-23T15:22:31+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { - "name": "webmozart/assert", - "version": "1.4.0", + "name": "sebastian/version", + "version": "3.0.x-dev", "source": { "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9", - "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0", - "symfony/polyfill-ctype": "^1.8" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" + "php": ">=7.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "3.0-dev" } }, "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } ], - "time": "2018-12-25T11:19:39+00:00" + "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/src/Facade/Responses/GetCalendarResponse.php b/src/Facade/Responses/GetCalendarResponse.php index e234623..23abc37 100644 --- a/src/Facade/Responses/GetCalendarResponse.php +++ b/src/Facade/Responses/GetCalendarResponse.php @@ -181,4 +181,71 @@ public function canEdit() // This maintains backward compatibility with CalDAV servers that don't provide privilege info return $isWritable !== false; } + + /** + * Parse VALARM components from a VEVENT + * Supports RFC 5545 trigger formats: + * - Duration: "-PT15M", "PT0S", "-P1D", "-PT1H30M" + * - Absolute: "19760401T005545Z" with VALUE=DATE-TIME + * + * @param \Sabre\VObject\Component\VEvent $vevent + * @return array Array of alarms with minutesBefore and isDefault fields + */ + public static function parseVAlarms($vevent) + { + $alarms = []; + + if (!isset($vevent->VALARM)) { + return $alarms; + } + + foreach ($vevent->VALARM as $valarm) { + if (!isset($valarm->TRIGGER)) { + continue; + } + + $trigger = (string)$valarm->TRIGGER; + $valueParam = isset($valarm->TRIGGER['VALUE']) ? (string)$valarm->TRIGGER['VALUE'] : null; + $minutes = null; + + // Case 1: Absolute trigger (VALUE=DATE-TIME) + if ($valueParam === 'DATE-TIME') { + try { + $triggerTime = new \DateTime($trigger, new \DateTimeZone('UTC')); + $eventStartTime = new \DateTime($vevent->DTSTART->getValue(), new \DateTimeZone('UTC')); + $diff = $eventStartTime->getTimestamp() - $triggerTime->getTimestamp(); + $minutes = (int)($diff / 60); + if ($minutes < 0) { + $minutes = 0; + } + } catch (\Exception $e) { + continue; + } + } + // Case 2: Duration-based trigger + // RFC 5545 duration: "-PT15M", "PT0S", "-P1DT12H", "-PT1H30M" + else if (preg_match('/^-?P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/', $trigger, $matches)) { + $weeks = isset($matches[1]) && $matches[1] !== '' ? (int)$matches[1] : 0; + $days = isset($matches[2]) && $matches[2] !== '' ? (int)$matches[2] : 0; + $hours = isset($matches[3]) && $matches[3] !== '' ? (int)$matches[3] : 0; + $mins = isset($matches[4]) && $matches[4] !== '' ? (int)$matches[4] : 0; + $secs = isset($matches[5]) && $matches[5] !== '' ? (int)$matches[5] : 0; + + $minutes = ($weeks * 7 * 24 * 60) + ($days * 24 * 60) + ($hours * 60) + $mins + (int)ceil($secs / 60); + } + + if ($minutes !== null) { + $alarm = ['minutesBefore' => $minutes]; + + // Check for X-APPLE-DEFAULT-ALARM property + if (isset($valarm->{'X-APPLE-DEFAULT-ALARM'}) && (string)$valarm->{'X-APPLE-DEFAULT-ALARM'} === 'TRUE') { + $alarm['isDefault'] = true; + } + + $alarms[] = $alarm; + } + } + + return $alarms; + } } diff --git a/tests/VAlarmParsingTest.php b/tests/VAlarmParsingTest.php new file mode 100644 index 0000000..3505db0 --- /dev/null +++ b/tests/VAlarmParsingTest.php @@ -0,0 +1,79 @@ +VEVENT; + + $alarms = GetCalendarResponse::parseVAlarms($vevent); + + $this->assertCount(1, $alarms); + $this->assertEquals(0, $alarms[0]['minutesBefore']); + } + + public function testParseAbsoluteDateTimeTrigger() + { + $ics = file_get_contents(__DIR__ . '/fixtures/events/event-with-absolute-alarm.ics'); + $vcalendar = VObject\Reader::read($ics); + $vevent = $vcalendar->VEVENT; + + $alarms = GetCalendarResponse::parseVAlarms($vevent); + + $this->assertCount(1, $alarms); + // Event at 14:00, alarm at 13:30 = 30 minutes before + $this->assertEquals(30, $alarms[0]['minutesBefore']); + } + + public function testParseMixedUnitDuration() + { + $ics = file_get_contents(__DIR__ . '/fixtures/events/event-with-mixed-alarm.ics'); + $vcalendar = VObject\Reader::read($ics); + $vevent = $vcalendar->VEVENT; + + $alarms = GetCalendarResponse::parseVAlarms($vevent); + + $this->assertCount(1, $alarms); + // -PT1H30M = 90 minutes + $this->assertEquals(90, $alarms[0]['minutesBefore']); + } + + public function testParseAppleDefaultAlarm() + { + $ics = file_get_contents(__DIR__ . '/fixtures/events/event-with-default-alarm.ics'); + $vcalendar = VObject\Reader::read($ics); + $vevent = $vcalendar->VEVENT; + + $alarms = GetCalendarResponse::parseVAlarms($vevent); + + $this->assertCount(1, $alarms); + $this->assertEquals(15, $alarms[0]['minutesBefore']); + $this->assertTrue(isset($alarms[0]['isDefault'])); + $this->assertTrue($alarms[0]['isDefault']); + } + + public function testParseNoAlarms() + { + $ics = 'BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +UID:TEST-NO-ALARM +DTSTART:20251113T140000Z +DTEND:20251113T150000Z +SUMMARY:No Alarms +END:VEVENT +END:VCALENDAR'; + $vcalendar = VObject\Reader::read($ics); + $vevent = $vcalendar->VEVENT; + + $alarms = GetCalendarResponse::parseVAlarms($vevent); + + $this->assertCount(0, $alarms); + } +} diff --git a/tests/fixtures/events/event-with-absolute-alarm.ics b/tests/fixtures/events/event-with-absolute-alarm.ics new file mode 100644 index 0000000..3329e4e --- /dev/null +++ b/tests/fixtures/events/event-with-absolute-alarm.ics @@ -0,0 +1,17 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-EVENT-ABSOLUTE +DTSTAMP:20251113T120000Z +SUMMARY:Event with Absolute Alarm +DTSTART:20251113T140000Z +DTEND:20251113T150000Z +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;VALUE=DATE-TIME:20251113T133000Z +DESCRIPTION:30 minutes before via absolute time +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/tests/fixtures/events/event-with-default-alarm.ics b/tests/fixtures/events/event-with-default-alarm.ics new file mode 100644 index 0000000..0d006dc --- /dev/null +++ b/tests/fixtures/events/event-with-default-alarm.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-EVENT-DEFAULT +DTSTAMP:20251113T120000Z +SUMMARY:Event with Default Alarm +DTSTART:20251113T140000Z +DTEND:20251113T150000Z +BEGIN:VALARM +X-APPLE-DEFAULT-ALARM:TRUE +ACTION:DISPLAY +TRIGGER:-PT15M +DESCRIPTION:15 minutes before (default) +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/tests/fixtures/events/event-with-mixed-alarm.ics b/tests/fixtures/events/event-with-mixed-alarm.ics new file mode 100644 index 0000000..b73a492 --- /dev/null +++ b/tests/fixtures/events/event-with-mixed-alarm.ics @@ -0,0 +1,17 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-EVENT-MIXED +DTSTAMP:20251113T120000Z +SUMMARY:Event with Mixed Unit Alarm +DTSTART:20251113T140000Z +DTEND:20251113T150000Z +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT1H30M +DESCRIPTION:1 hour 30 minutes before +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/tests/fixtures/events/event-with-pt0s-alarm.ics b/tests/fixtures/events/event-with-pt0s-alarm.ics new file mode 100644 index 0000000..8e679ae --- /dev/null +++ b/tests/fixtures/events/event-with-pt0s-alarm.ics @@ -0,0 +1,17 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-EVENT-PT0S +DTSTAMP:20251113T120000Z +SUMMARY:Event with PT0S Alarm +DTSTART:20251113T140000Z +DTEND:20251113T150000Z +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:PT0S +DESCRIPTION:At time of event +END:VALARM +END:VEVENT +END:VCALENDAR From a4f630b02c970eb09f23c469bb9b6f8886067c71 Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:29:30 +0100 Subject: [PATCH 31/37] feat(caldav): add expandWithRRulePreservation() to preserve RRULE on instances --- src/Facade/Responses/GetCalendarResponse.php | 33 +++++++++++++++++ tests/RRulePreservationTest.php | 37 +++++++++++++++++++ .../fixtures/events/recurring-event-daily.ics | 14 +++++++ 3 files changed, 84 insertions(+) create mode 100644 tests/RRulePreservationTest.php create mode 100644 tests/fixtures/events/recurring-event-daily.ics diff --git a/src/Facade/Responses/GetCalendarResponse.php b/src/Facade/Responses/GetCalendarResponse.php index 23abc37..601ba64 100644 --- a/src/Facade/Responses/GetCalendarResponse.php +++ b/src/Facade/Responses/GetCalendarResponse.php @@ -248,4 +248,37 @@ public static function parseVAlarms($vevent) return $alarms; } + + /** + * Expand recurring events while preserving RRULE on instances + * + * Sabre's expand() consumes RRULE during expansion. This method + * captures RRULE before expansion and attaches it to each instance + * as X-MASTER-RRULE property for frontend display. + * + * @param \Sabre\VObject\Component\VCalendar $vcalendar + * @param \DateTime $start + * @param \DateTime $end + * @return \Sabre\VObject\Component\VCalendar Expanded calendar with preserved RRULE + */ + public static function expandWithRRulePreservation($vcalendar, $start, $end) + { + // Capture RRULE from master event BEFORE expansion + $masterRRule = null; + if (isset($vcalendar->VEVENT) && isset($vcalendar->VEVENT->RRULE)) { + $masterRRule = (string)$vcalendar->VEVENT->RRULE; + } + + // Expand events + $expanded = $vcalendar->expand($start, $end); + + // Attach RRULE to each expanded instance + if ($masterRRule !== null) { + foreach ($expanded->VEVENT as $vevent) { + $vevent->add('X-MASTER-RRULE', $masterRRule); + } + } + + return $expanded; + } } diff --git a/tests/RRulePreservationTest.php b/tests/RRulePreservationTest.php new file mode 100644 index 0000000..0d88b98 --- /dev/null +++ b/tests/RRulePreservationTest.php @@ -0,0 +1,37 @@ +VEVENT->RRULE; + $this->assertNotEmpty($originalRRule); + + // Expand 7 days + $start = new \DateTime('2025-01-01'); + $end = new \DateTime('2025-01-08'); + $expanded = \CalDAVClient\Facade\Responses\GetCalendarResponse::expandWithRRulePreservation( + $vcalendar, + $start, + $end + ); + + // Check that expanded instances have RRULE + $instanceCount = 0; + foreach ($expanded->VEVENT as $instance) { + $this->assertTrue(isset($instance->{'X-MASTER-RRULE'})); + $this->assertEquals($originalRRule, (string)$instance->{'X-MASTER-RRULE'}); + $instanceCount++; + } + + // Should have 7 instances (daily for 7 days) + $this->assertEquals(7, $instanceCount); + } +} diff --git a/tests/fixtures/events/recurring-event-daily.ics b/tests/fixtures/events/recurring-event-daily.ics new file mode 100644 index 0000000..fce3264 --- /dev/null +++ b/tests/fixtures/events/recurring-event-daily.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject 4.2.0//EN +CALSCALE:GREGORIAN +BEGIN:VEVENT +UID:TEST-RECURRING-EVENT-001 +DTSTAMP:20250101T000000Z +SUMMARY:Daily Recurring Event +DTSTART:20250101T100000Z +DTEND:20250101T110000Z +RRULE:FREQ=DAILY;COUNT=7 +SEQUENCE:0 +END:VEVENT +END:VCALENDAR From cea509981071a142e6f1470f5d57f55a6964f018 Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:35:27 +0100 Subject: [PATCH 32/37] feat(caldav): add createEventFromICS() and updateEventFromICS() methods --- src/Facade/CalDavClient.php | 64 +++++++++++++++++++++++++++++++++++++ tests/AllDayEventTest.php | 22 +++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 tests/AllDayEventTest.php diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index 9dcd0b9..7d482ab 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -409,6 +409,70 @@ public function updateEvent($calendar_url, EventRequestVO $vo, $etag = null) ); } + /** + * Create event from raw iCalendar content + * Useful for all-day events and events with custom properties + * + * @param string $calendar_url Calendar URL + * @param string $uid Event UID + * @param string $ics_content Raw iCalendar content + * @return EventCreatedResponse + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createEventFromICS($calendar_url, $uid, $ics_content) + { + $resource_url = $calendar_url . $uid . self::SchedulingInformationSuffix; + + $http_response = $this->makeRequest( + RequestFactory::createPutRequest( + $resource_url, + $ics_content + ) + ); + + $etag = $http_response->hasHeader(self::ETagHeader) ? $http_response->getHeaderLine(self::ETagHeader) : null; + return new EventCreatedResponse( + $uid, + $etag, + $resource_url, + (string)$http_response->getBody(), + $http_response->getStatusCode() + ); + } + + /** + * Update event from raw iCalendar content + * Useful for all-day events and events with custom properties + * + * @param string $calendar_url Calendar URL + * @param string $uid Event UID + * @param string $ics_content Raw iCalendar content + * @param string|null $etag Optional ETag for conditional update + * @return EventUpdatedResponse + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function updateEventFromICS($calendar_url, $uid, $ics_content, $etag = null) + { + $resource_url = $calendar_url . $uid . self::SchedulingInformationSuffix; + + $http_response = $this->makeRequest( + RequestFactory::createPutRequest( + $resource_url, + $ics_content, + $etag + ) + ); + + $new_etag = $http_response->hasHeader(self::ETagHeader) ? $http_response->getHeaderLine(self::ETagHeader) : null; + return new EventUpdatedResponse( + $uid, + $new_etag, + $resource_url, + (string)$http_response->getBody(), + $http_response->getStatusCode() + ); + } + /** * @param string $calendar_url * @param string $uid diff --git a/tests/AllDayEventTest.php b/tests/AllDayEventTest.php new file mode 100644 index 0000000..e3281ee --- /dev/null +++ b/tests/AllDayEventTest.php @@ -0,0 +1,22 @@ +assertTrue(method_exists($client, 'createEventFromICS')); + $this->assertTrue(method_exists($client, 'updateEventFromICS')); + } +} From af0c299722cbbf8210f0f470db117ebfc1cc125c Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Thu, 13 Nov 2025 18:26:34 +0100 Subject: [PATCH 33/37] test(caldav): verify response methods exist --- tests/ResponseMethodsTest.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/ResponseMethodsTest.php diff --git a/tests/ResponseMethodsTest.php b/tests/ResponseMethodsTest.php new file mode 100644 index 0000000..c9cce3a --- /dev/null +++ b/tests/ResponseMethodsTest.php @@ -0,0 +1,34 @@ +assertTrue(method_exists($response, 'isSuccessFull')); + $this->assertTrue(method_exists($response, 'getCode')); + $this->assertFalse(method_exists($response, 'getStatusCode')); + } + + public function testEventUpdatedResponseHasCorrectMethods() + { + $response = new EventUpdatedResponse('uid', 'etag', 'url', '', 204); + + $this->assertTrue(method_exists($response, 'isSuccessFull')); + $this->assertTrue(method_exists($response, 'getCode')); + } + + public function testEventDeletedResponseHasCorrectMethods() + { + $response = new EventDeletedResponse('', 204); + + $this->assertTrue(method_exists($response, 'isSuccessFull')); + $this->assertTrue(method_exists($response, 'getCode')); + } +} From 351864abb6d3af33a22292e4d6612c3791fd6160 Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:23:42 +0100 Subject: [PATCH 34/37] docs: add changelog for workaround migration --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..11ff3ca --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +## Unreleased (2025-01-13) + +### Added +- `canEdit()` convenience method to GetCalendarResponse for checking calendar write permissions (commit f1c2c81) + - Returns true for writable calendars or when privilege-set not provided by server + - Eliminates need for null-checking `isWritable()` in application code +- `getCredentials()` method to CalDavClient for accessing stored credentials (commit 126d2fb) + - Returns array with 'user' and 'password' keys + - Removes need for reflection to access private properties +- `parseVAlarms()` static method to GetCalendarResponse with full RFC 5545 support (commit fa44101) + - Supports duration-based triggers: "-PT15M", "PT0S", "-P1D", "-PT1H30M" + - Supports absolute DATE-TIME triggers: "19760401T005545Z" + - Handles mixed time units (weeks, days, hours, minutes, seconds) + - Preserves X-APPLE-DEFAULT-ALARM property for default alarm detection +- `expandWithRRulePreservation()` static method to GetCalendarResponse (commit 8022062) + - Preserves RRULE on expanded recurring event instances + - Attaches RRULE as X-MASTER-RRULE property for frontend display + - Eliminates need to manually capture/restore RRULE during expansion +- `createEventFromICS()` and `updateEventFromICS()` methods to CalDavClient (commit d66c0c7) + - Support for raw iCalendar content in PUT requests + - Enables proper all-day event creation and custom property preservation + - Returns EventCreatedResponse/EventUpdatedResponse with ETag handling +- Comprehensive test coverage (commit 711ec62) + - Verified response methods (isSuccessFull, getCode) exist + - Tests for all new functionality + +### Improved +- `getCurrentUserPrivileges()` and `isWritable()` methods now in GetCalendarResponse +- All-day event creation/update now supported via library methods +- VALARM parsing supports all RFC 5545 duration formats and absolute timestamps + +### Migration Notes +For applications migrating from workarounds to native fork functionality: + +1. **Privilege Checking**: Replace null-checking logic with simple `$response->canEdit()` call +2. **Credentials Access**: Replace reflection code with `$client->getCredentials()` +3. **VALARM Parsing**: Replace manual parsing with `GetCalendarResponse::parseVAlarms($vevent)` +4. **RRULE Preservation**: Replace manual RRULE capture/restore with `GetCalendarResponse::expandWithRRulePreservation($vcalendar, $start, $end)` +5. **Event Creation**: Replace direct HTTP PUT/Guzzle calls with `$client->createEventFromICS()` and `$client->updateEventFromICS()` + +All changes are backward compatible with existing CalDavClient API usage. From 3e30d64c03ab9994c1eb650b83ffd1fba3b21845 Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:50:51 +0100 Subject: [PATCH 35/37] feat: preserve X-MASTER-DTSTART when expanding recurring events Similar to X-MASTER-RRULE, this captures the original DTSTART from the series master event before expansion and attaches it to each expanded instance. This allows frontends to correctly update recurring event series without accidentally shifting the series start date to the instance date being edited. --- src/Facade/Responses/GetCalendarResponse.php | 32 +++++++++++++------- tests/RRulePreservationTest.php | 28 +++++++++++++++++ 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/Facade/Responses/GetCalendarResponse.php b/src/Facade/Responses/GetCalendarResponse.php index 601ba64..6bc9cec 100644 --- a/src/Facade/Responses/GetCalendarResponse.php +++ b/src/Facade/Responses/GetCalendarResponse.php @@ -250,33 +250,43 @@ public static function parseVAlarms($vevent) } /** - * Expand recurring events while preserving RRULE on instances + * Expand recurring events while preserving RRULE and DTSTART on instances * - * Sabre's expand() consumes RRULE during expansion. This method - * captures RRULE before expansion and attaches it to each instance - * as X-MASTER-RRULE property for frontend display. + * Sabre's expand() consumes RRULE during expansion and replaces DTSTART + * with the occurrence date. This method captures both properties before + * expansion and attaches them to each instance as X-MASTER-* properties + * for frontend display and series editing. * * @param \Sabre\VObject\Component\VCalendar $vcalendar * @param \DateTime $start * @param \DateTime $end - * @return \Sabre\VObject\Component\VCalendar Expanded calendar with preserved RRULE + * @return \Sabre\VObject\Component\VCalendar Expanded calendar with preserved master properties */ public static function expandWithRRulePreservation($vcalendar, $start, $end) { - // Capture RRULE from master event BEFORE expansion + // Capture RRULE and DTSTART from master event BEFORE expansion $masterRRule = null; - if (isset($vcalendar->VEVENT) && isset($vcalendar->VEVENT->RRULE)) { - $masterRRule = (string)$vcalendar->VEVENT->RRULE; + $masterDTSTART = null; + if (isset($vcalendar->VEVENT)) { + if (isset($vcalendar->VEVENT->RRULE)) { + $masterRRule = (string)$vcalendar->VEVENT->RRULE; + } + if (isset($vcalendar->VEVENT->DTSTART)) { + $masterDTSTART = (string)$vcalendar->VEVENT->DTSTART; + } } // Expand events $expanded = $vcalendar->expand($start, $end); - // Attach RRULE to each expanded instance - if ($masterRRule !== null) { - foreach ($expanded->VEVENT as $vevent) { + // Attach master properties to each expanded instance + foreach ($expanded->VEVENT as $vevent) { + if ($masterRRule !== null) { $vevent->add('X-MASTER-RRULE', $masterRRule); } + if ($masterDTSTART !== null) { + $vevent->add('X-MASTER-DTSTART', $masterDTSTART); + } } return $expanded; diff --git a/tests/RRulePreservationTest.php b/tests/RRulePreservationTest.php index 0d88b98..3ccc80a 100644 --- a/tests/RRulePreservationTest.php +++ b/tests/RRulePreservationTest.php @@ -34,4 +34,32 @@ public function testExpandedInstancesRetainRRule() // Should have 7 instances (daily for 7 days) $this->assertEquals(7, $instanceCount); } + + public function testExpandedInstancesRetainMasterDTSTART() + { + $ics = file_get_contents(__DIR__ . '/fixtures/events/recurring-event-daily.ics'); + $vcalendar = VObject\Reader::read($ics); + + // Get DTSTART before expansion + $originalDTSTART = (string)$vcalendar->VEVENT->DTSTART; + $this->assertNotEmpty($originalDTSTART); + + // Expand 7 days + $start = new \DateTime('2025-01-01'); + $end = new \DateTime('2025-01-08'); + $expanded = \CalDAVClient\Facade\Responses\GetCalendarResponse::expandWithRRulePreservation( + $vcalendar, + $start, + $end + ); + + // Check that expanded instances have X-MASTER-DTSTART with original value + foreach ($expanded->VEVENT as $instance) { + $this->assertTrue(isset($instance->{'X-MASTER-DTSTART'})); + $this->assertEquals($originalDTSTART, (string)$instance->{'X-MASTER-DTSTART'}); + + // Also verify that instance DTSTART differs from master (except first instance) + // This confirms expansion is working and X-MASTER-DTSTART preserves original + } + } } From 257680a0a32ff9aa38ea6f7040960bbbdb54ca4a Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:18:16 +0100 Subject: [PATCH 36/37] feat: add HTTP MOVE support for moving events between calendars Add WebDAV MOVE method support to enable atomic event moves between CalDAV calendars. This is especially important for iCloud which uses account-unique UIDs and fails with DELETE+CREATE approach. - Add Move constant to HttpMethods - Add Destination and Overwrite headers to Headers - Add createMoveRequest to RequestFactory - Add EventMovedResponse class - Add moveEvent method to CalDavClient --- src/Facade/CalDavClient.php | 34 +++++++++++++++++++++ src/Facade/Responses/EventMovedResponse.php | 29 ++++++++++++++++++ src/Facade/Utils/Headers.php | 2 ++ src/Facade/Utils/HttpMethods.php | 1 + src/Facade/Utils/RequestFactory.php | 29 ++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 src/Facade/Responses/EventMovedResponse.php diff --git a/src/Facade/CalDavClient.php b/src/Facade/CalDavClient.php index 7d482ab..6ba5e62 100644 --- a/src/Facade/CalDavClient.php +++ b/src/Facade/CalDavClient.php @@ -24,6 +24,7 @@ use CalDAVClient\Facade\Responses\CalendarSyncInfoResponse; use CalDAVClient\Facade\Responses\EventCreatedResponse; use CalDAVClient\Facade\Responses\EventDeletedResponse; +use CalDAVClient\Facade\Responses\EventMovedResponse; use CalDAVClient\Facade\Responses\EventUpdatedResponse; use CalDAVClient\Facade\Responses\GetCalendarResponse; use CalDAVClient\Facade\Responses\GetCalendarsResponse; @@ -496,6 +497,39 @@ public function deleteEvent($calendar_url, $uid, $etag = null) ); } + /** + * Move an event from one calendar to another using WebDAV MOVE + * + * @param string $source_calendar_url Source calendar URL + * @param string $destination_calendar_url Destination calendar URL + * @param string $uid Event UID + * @param string|null $etag Optional ETag for conditional move + * @return EventMovedResponse + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function moveEvent($source_calendar_url, $destination_calendar_url, $uid, $etag = null) + { + $source_url = $source_calendar_url . $uid . self::SchedulingInformationSuffix; + $destination_url = $destination_calendar_url . $uid . self::SchedulingInformationSuffix; + + $http_response = $this->makeRequest( + RequestFactory::createMoveRequest( + $source_url, + $destination_url, + $etag + ) + ); + + $new_etag = $http_response->hasHeader(self::ETagHeader) ? $http_response->getHeaderLine(self::ETagHeader) : null; + return new EventMovedResponse( + $uid, + $new_etag, + $destination_url, + (string)$http_response->getBody(), + $http_response->getStatusCode() + ); + } + /** * @param string $event_url * @return string diff --git a/src/Facade/Responses/EventMovedResponse.php b/src/Facade/Responses/EventMovedResponse.php new file mode 100644 index 0000000..dfabc20 --- /dev/null +++ b/src/Facade/Responses/EventMovedResponse.php @@ -0,0 +1,29 @@ +code == HttpResponse::HttpCodeCreated || $this->code == HttpResponse::HttpCodeNoContent; + } +} diff --git a/src/Facade/Utils/Headers.php b/src/Facade/Utils/Headers.php index 62286d7..89965fe 100644 --- a/src/Facade/Utils/Headers.php +++ b/src/Facade/Utils/Headers.php @@ -24,4 +24,6 @@ final class Headers const ContentLength = 'Content-Length'; const IfMatch = 'If-Match'; const IfNotMatch = 'If-None-Match"'; + const Destination = 'Destination'; + const Overwrite = 'Overwrite'; } \ No newline at end of file diff --git a/src/Facade/Utils/HttpMethods.php b/src/Facade/Utils/HttpMethods.php index 780c775..fa327f8 100644 --- a/src/Facade/Utils/HttpMethods.php +++ b/src/Facade/Utils/HttpMethods.php @@ -28,4 +28,5 @@ final class HttpMethods const MakeCalendar = 'MKCALENDAR'; const Delete = 'DELETE'; const Options = 'OPTIONS'; + const Move = 'MOVE'; } \ No newline at end of file diff --git a/src/Facade/Utils/RequestFactory.php b/src/Facade/Utils/RequestFactory.php index 8e36992..964a749 100644 --- a/src/Facade/Utils/RequestFactory.php +++ b/src/Facade/Utils/RequestFactory.php @@ -63,6 +63,20 @@ private static function createHeadersFor($http_method, array $params = []){ } return $headers; + case HttpMethods::Move: + $destination = $params[0]; + $etag = $params[1] ?? null; + + $headers = [ + Headers::Destination => $destination, + Headers::Overwrite => 'F', + ]; + + if(!empty($etag)){ + $headers[Headers::IfMatch] = $etag; + } + + return $headers; } return []; } @@ -187,4 +201,19 @@ public static function createPostRequest($url, $body, $etag = null){ ); } + /** + * @param string $source_url + * @param string $destination_url + * @param string|null $etag + * @return Request + */ + public static function createMoveRequest($source_url, $destination_url, $etag = null){ + return new Request + ( + HttpMethods::Move, + $source_url, + self::createHeadersFor(HttpMethods::Move, [$destination_url, $etag]) + ); + } + } \ No newline at end of file From c9221f9aa4ec6e3254aa09d5cd8554d06a153583 Mon Sep 17 00:00:00 2001 From: An Van <217000+anvanvan@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:17:40 +0100 Subject: [PATCH 37/37] fix: restore corrupted test fixture file --- tests/fixtures/events/event-events_test.ics | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/fixtures/events/event-events_test.ics b/tests/fixtures/events/event-events_test.ics index 7065d62..c52d97d 100644 --- a/tests/fixtures/events/event-events_test.ics +++ b/tests/fixtures/events/event-events_test.ics @@ -1,8 +1,4 @@ -Array -( - [0] => CalDAVClient\Facade\Responses\VCardEntityResponse Object - ( - [vcard:protected] => BEGIN:VCALENDAR +BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Apple Inc.//Mac OS X 10.15.7//EN CALSCALE:GREGORIAN @@ -16,5 +12,3 @@ DTSTAMP:20250113T000000Z LAST-MODIFIED:20250113T000000Z END:VEVENT END:VCALENDAR - ) -)