3333
3434/**
3535 * Service to make requests to Google v3 (JSON) API
36+ *
37+ * @phpstan-type Event array{id: string, iCalUID: string, start?: array{date?: string, dateTime?: string, timeZone?: string}, end?: array{date?: string, dateTime?: string, timeZone?: string}, originalStartTime?: array{date?: string, dateTime?: string, timeZone?: string}, recurringEventId?: string, colorId?: string, summary?: string, visibility?: string, sequence?: string, location?: string, description?: string, status?: string, created?: string, updated?: string, reminders?: array{useDefault?: bool, overrides?: list{array{minutes?: string, hours?: string, days?: string, weeks?: string}}}, recurrence?: list<string>}
3638 */
3739class GoogleCalendarAPIService {
40+ private DateTimeZone $ utcTimezone ;
3841
3942 public function __construct (
4043 string $ appName ,
@@ -44,6 +47,7 @@ public function __construct(
4447 private GoogleAPIService $ googleApiService ,
4548 private IConfig $ config ,
4649 ) {
50+ $ this ->utcTimezone = new DateTimeZone ('-0000 ' );
4751 }
4852
4953 /**
@@ -61,15 +65,152 @@ public function getCalendarList(string $userId): array {
6165 /**
6266 * @param string $userId
6367 * @param string $uri
64- * @return ?int the calendar ID
68+ * @return ?string the calendar ID
6569 */
66- private function calendarExists (string $ userId , string $ uri ): ?int {
70+ private function calendarExists (string $ userId , string $ uri ): ?string {
6771 $ res = $ this ->caldavBackend ->getCalendarByUri ('principals/users/ ' . $ userId , $ uri );
6872 return is_null ($ res )
6973 ? null
7074 : $ res ['id ' ];
7175 }
7276
77+ /**
78+ * @param array{date?: string, dateTime?: string, timeZone?: string} $obj The datetime object to map.
79+ * @return string The date time mapped to the best representation from the available data.
80+ */
81+ private function mapTime (array $ obj ): string {
82+ if (isset ($ obj ['dateTime ' ])) {
83+ $ dateTime = new DateTime ($ obj ['dateTime ' ]);
84+
85+ if (isset ($ obj ['timeZone ' ])) {
86+ $ timezone = $ obj ['timeZone ' ];
87+ $ dateTime ->setTimezone (new DateTimeZone ($ timezone ));
88+ return "TZID= $ timezone: " . $ dateTime ->format ('Ymd\THis ' );
89+ } else {
90+ $ dateTime ->setTimezone ($ this ->utcTimezone );
91+ return 'VALUE=DATE-TIME: ' . $ dateTime ->format ('Ymd\THis\Z ' );
92+ }
93+ } elseif (isset ($ obj ['date ' ])) {
94+ // whole days
95+ $ date = new DateTime ($ obj ['date ' ]);
96+ return 'VALUE=DATE: ' . $ date ->format ('Ymd ' );
97+ } else {
98+ // skip entries without any date
99+ return '' ;
100+ }
101+ }
102+
103+ /**
104+ * @param Event $e The event from which to generate the data.
105+ * @param array<Event> $events The collection of all events.
106+ * @param string $ncCalId The id of the event's calendar.
107+ * @param array $eventColors The event colors mapping.
108+ */
109+ private function generateEventData (array $ e , array $ events , string $ ncCalId , array $ eventColors ): string {
110+ $ objectUri = $ e ['id ' ];
111+
112+ $ eventData = 'BEGIN:VEVENT ' . "\n" ;
113+
114+ $ eventData .= 'UID: ' . $ ncCalId . '- ' . $ e ['iCalUID ' ] . "\n" ;
115+ /* $eventData .= 'UID:' . $ncCalId . '-' . $objectUri . "\n"; */
116+ if (isset ($ e ['colorId ' ], $ eventColors [$ e ['colorId ' ]], $ eventColors [$ e ['colorId ' ]]['background ' ])) {
117+ $ closestCssColor = $ this ->getClosestCssColor ($ eventColors [$ e ['colorId ' ]]['background ' ]);
118+ $ eventData .= 'COLOR: ' . $ closestCssColor . "\n" ;
119+ }
120+ $ eventData .= isset ($ e ['summary ' ])
121+ ? ('SUMMARY: ' . substr (str_replace ("\n" , '\n ' , $ e ['summary ' ]), 0 , 250 ) . "\n" )
122+ : (($ e ['visibility ' ] ?? '' ) === 'private '
123+ ? ('SUMMARY: ' . $ this ->l10n ->t ('Private event ' ) . "\n" )
124+ : '' );
125+ $ eventData .= isset ($ e ['sequence ' ]) ? ('SEQUENCE: ' . $ e ['sequence ' ] . "\n" ) : '' ;
126+ $ eventData .= isset ($ e ['location ' ])
127+ ? ('LOCATION: ' . substr (str_replace ("\n" , '\n ' , $ e ['location ' ]), 0 , 250 ) . "\n" )
128+ : '' ;
129+ $ eventData .= isset ($ e ['description ' ])
130+ ? ('DESCRIPTION: ' . substr (str_replace ("\n" , '\n ' , $ e ['description ' ]), 0 , 250 ) . "\n" )
131+ : '' ;
132+ $ eventData .= isset ($ e ['status ' ]) ? ('STATUS: ' . strtoupper (str_replace ("\n" , '\n ' , $ e ['status ' ])) . "\n" ) : '' ;
133+
134+ if (isset ($ e ['created ' ])) {
135+ $ created = new DateTime ($ e ['created ' ]);
136+ $ created ->setTimezone ($ this ->utcTimezone );
137+ $ eventData .= 'CREATED: ' . $ created ->format ('Ymd\THis\Z ' ) . "\n" ;
138+ }
139+
140+ if (isset ($ e ['updated ' ])) {
141+ $ updated = new DateTime ($ e ['updated ' ]);
142+ $ updated ->setTimezone ($ this ->utcTimezone );
143+ $ eventData .= 'LAST-MODIFIED: ' . $ updated ->format ('Ymd\THis\Z ' ) . "\n" ;
144+ }
145+
146+ if (isset ($ e ['reminders ' ], $ e ['reminders ' ]['useDefault ' ]) && $ e ['reminders ' ]['useDefault ' ]) {
147+ // 15 min before, default alarm
148+ $ eventData .= 'BEGIN:VALARM ' . "\n"
149+ . 'ACTION:DISPLAY ' . "\n"
150+ . 'TRIGGER;RELATED=START:-PT15M ' . "\n"
151+ . 'END:VALARM ' . "\n" ;
152+ }
153+ if (isset ($ e ['reminders ' ], $ e ['reminders ' ]['overrides ' ])) {
154+ foreach ($ e ['reminders ' ]['overrides ' ] as $ o ) {
155+ $ nbMin = 0 ;
156+ if (isset ($ o ['minutes ' ])) {
157+ $ nbMin += (int )$ o ['minutes ' ];
158+ }
159+ if (isset ($ o ['hours ' ])) {
160+ $ nbMin += ((int )$ o ['hours ' ]) * 60 ;
161+ }
162+ if (isset ($ o ['days ' ])) {
163+ $ nbMin += ((int )$ o ['days ' ]) * 60 * 24 ;
164+ }
165+ if (isset ($ o ['weeks ' ])) {
166+ $ nbMin += ((int )$ o ['weeks ' ]) * 60 * 24 * 7 ;
167+ }
168+ $ eventData .= 'BEGIN:VALARM ' . "\n"
169+ . 'ACTION:DISPLAY ' . "\n"
170+ . 'TRIGGER;RELATED=START:-PT ' . $ nbMin . 'M ' . "\n"
171+ . 'END:VALARM ' . "\n" ;
172+ }
173+ }
174+
175+ if (isset ($ e ['recurrence ' ]) && is_array ($ e ['recurrence ' ])) {
176+ foreach ($ e ['recurrence ' ] as $ r ) {
177+ $ eventData .= $ r . "\n" ;
178+ }
179+ }
180+
181+ // skip entries without any date
182+ if (!isset ($ e ['start ' ]) || !isset ($ e ['end ' ])) {
183+ return '' ;
184+ }
185+
186+ $ start = $ this ->mapTime ($ e ['start ' ]);
187+ $ end = $ this ->mapTime ($ e ['end ' ]);
188+
189+ // skip entries without any date
190+ if ($ start == '' || $ end == '' ) {
191+ return '' ;
192+ }
193+
194+ $ eventData .= "DTSTART; $ start \n" ;
195+ $ eventData .= "DTEND; $ end \n" ;
196+
197+ if (isset ($ e ['recurringEventId ' ], $ e ['originalStartTime ' ])) {
198+ $ recurrenceId = $ this ->mapTime ($ e ['originalStartTime ' ]);
199+ $ eventData .= "RECURRENCE-ID; $ recurrenceId \n" ;
200+ }
201+
202+ $ eventData .= 'CLASS:PUBLIC ' . "\n"
203+ . 'END:VEVENT ' . "\n" ;
204+
205+ foreach ($ events as $ candidateEvent ) {
206+ if (($ candidateEvent ['recurringEventId ' ] == $ e ['id ' ]) && ($ candidateEvent ['id ' ] != $ e ['id ' ])) {
207+ $ eventData .= $ this ->generateEventData ($ candidateEvent , $ events , $ ncCalId , $ eventColors );
208+ }
209+ }
210+
211+ return $ eventData ;
212+ }
213+
73214 /**
74215 * @param string $hexColor
75216 * @return string closest CSS color name
@@ -194,10 +335,11 @@ public function importCalendar(string $userId, string $calId, string $calName, ?
194335 date_default_timezone_set ('UTC ' );
195336 $ utcTimezone = new DateTimeZone ('-0000 ' );
196337 $ allEvents = $ this ->config ->getUserValue ($ userId , Application::APP_ID , 'consider_all_events ' , '1 ' ) === '1 ' ;
197- $ events = $ this ->getCalendarEvents ($ userId , $ calId , $ allEvents );
338+ $ eventsGenerator = $ this ->getCalendarEvents ($ userId , $ calId , $ allEvents );
339+ $ events = iterator_to_array ($ eventsGenerator );
198340 $ nbAdded = 0 ;
199341 $ nbUpdated = 0 ;
200- /** @var array{id: string, start?: array{date?: string, dateTime?: string, timeZone?: string}, end?: array{date?: string, dateTime?: string, timeZone?: string}, colorId?: string, summary?: string, visibility?: string, sequence?: string, location?: string, description?: string, status?: string, created?: string, updated?: string, reminders?: array{useDefault?: bool, overrides?: list{array{minutes?: string, hours?: string, days?: string, weeks?: string}}}, recurrence?: list<string>} $e */
342+ /** @var Event $e */
201343 foreach ($ events as $ e ) {
202344 $ objectUri = $ e ['id ' ];
203345
@@ -226,107 +368,21 @@ public function importCalendar(string $userId, string $calId, string $calName, ?
226368 }
227369 }
228370
229- $ calData = 'BEGIN:VCALENDAR ' . "\n"
230- . 'VERSION:2.0 ' . "\n"
231- . 'PRODID:NextCloud Calendar ' . "\n"
232- . 'BEGIN:VEVENT ' . "\n" ;
233-
234- $ calData .= 'UID: ' . $ ncCalId . '- ' . $ objectUri . "\n" ;
235- if (isset ($ e ['colorId ' ], $ eventColors [$ e ['colorId ' ]], $ eventColors [$ e ['colorId ' ]]['background ' ])) {
236- $ closestCssColor = $ this ->getClosestCssColor ($ eventColors [$ e ['colorId ' ]]['background ' ]);
237- $ calData .= 'COLOR: ' . $ closestCssColor . "\n" ;
238- }
239- $ calData .= isset ($ e ['summary ' ])
240- ? ('SUMMARY: ' . substr (str_replace ("\n" , '\n ' , $ e ['summary ' ]), 0 , 250 ) . "\n" )
241- : (($ e ['visibility ' ] ?? '' ) === 'private '
242- ? ('SUMMARY: ' . $ this ->l10n ->t ('Private event ' ) . "\n" )
243- : '' );
244- $ calData .= isset ($ e ['sequence ' ]) ? ('SEQUENCE: ' . $ e ['sequence ' ] . "\n" ) : '' ;
245- $ calData .= isset ($ e ['location ' ])
246- ? ('LOCATION: ' . substr (str_replace ("\n" , '\n ' , $ e ['location ' ]), 0 , 250 ) . "\n" )
247- : '' ;
248- $ calData .= isset ($ e ['description ' ])
249- ? ('DESCRIPTION: ' . substr (str_replace ("\n" , '\n ' , $ e ['description ' ]), 0 , 250 ) . "\n" )
250- : '' ;
251- $ calData .= isset ($ e ['status ' ]) ? ('STATUS: ' . strtoupper (str_replace ("\n" , '\n ' , $ e ['status ' ])) . "\n" ) : '' ;
252-
253- if (isset ($ e ['created ' ])) {
254- $ created = new DateTime ($ e ['created ' ]);
255- $ created ->setTimezone ($ utcTimezone );
256- $ calData .= 'CREATED: ' . $ created ->format ('Ymd\THis\Z ' ) . "\n" ;
257- }
258-
259- if (isset ($ e ['updated ' ])) {
260- $ updated = new DateTime ($ e ['updated ' ]);
261- $ updated ->setTimezone ($ utcTimezone );
262- $ calData .= 'LAST-MODIFIED: ' . $ updated ->format ('Ymd\THis\Z ' ) . "\n" ;
263- }
264-
265- if (isset ($ e ['reminders ' ], $ e ['reminders ' ]['useDefault ' ]) && $ e ['reminders ' ]['useDefault ' ]) {
266- // 15 min before, default alarm
267- $ calData .= 'BEGIN:VALARM ' . "\n"
268- . 'ACTION:DISPLAY ' . "\n"
269- . 'TRIGGER;RELATED=START:-PT15M ' . "\n"
270- . 'END:VALARM ' . "\n" ;
271- }
272- if (isset ($ e ['reminders ' ], $ e ['reminders ' ]['overrides ' ])) {
273- foreach ($ e ['reminders ' ]['overrides ' ] as $ o ) {
274- $ nbMin = 0 ;
275- if (isset ($ o ['minutes ' ])) {
276- $ nbMin += (int )$ o ['minutes ' ];
277- }
278- if (isset ($ o ['hours ' ])) {
279- $ nbMin += ((int )$ o ['hours ' ]) * 60 ;
280- }
281- if (isset ($ o ['days ' ])) {
282- $ nbMin += ((int )$ o ['days ' ]) * 60 * 24 ;
283- }
284- if (isset ($ o ['weeks ' ])) {
285- $ nbMin += ((int )$ o ['weeks ' ]) * 60 * 24 * 7 ;
286- }
287- $ calData .= 'BEGIN:VALARM ' . "\n"
288- . 'ACTION:DISPLAY ' . "\n"
289- . 'TRIGGER;RELATED=START:-PT ' . $ nbMin . 'M ' . "\n"
290- . 'END:VALARM ' . "\n" ;
291- }
371+ // For recurring events, the parent event recursively calls generateEventData
372+ if (isset ($ e ['recurringEventId ' ])) {
373+ continue ;
292374 }
293375
294- if (isset ($ e ['recurrence ' ]) && is_array ($ e ['recurrence ' ])) {
295- foreach ($ e ['recurrence ' ] as $ r ) {
296- $ calData .= $ r . "\n" ;
297- }
298- }
376+ $ eventData = $ this ->generateEventData ($ e , $ events , $ ncCalId , $ eventColors );
299377
300- if (isset ($ e ['start ' ], $ e ['start ' ]['dateTime ' ], $ e ['end ' ], $ e ['end ' ]['dateTime ' ])) {
301- $ start = new DateTime ($ e ['start ' ]['dateTime ' ]);
302- $ end = new DateTime ($ e ['end ' ]['dateTime ' ]);
303-
304- if (isset ($ e ['start ' ]['timeZone ' ], $ e ['end ' ]['timeZone ' ])) {
305- $ timezoneStart = $ e ['start ' ]['timeZone ' ];
306- $ start ->setTimezone (new DateTimeZone ($ timezoneStart ));
307- $ calData .= "DTSTART;TZID= $ timezoneStart: " . $ start ->format ('Ymd\THis ' ) . "\n" ;
308- $ timezoneEnd = $ e ['end ' ]['timeZone ' ];
309- $ end ->setTimezone (new DateTimeZone ($ timezoneEnd ));
310- $ calData .= "DTEND;TZID= $ timezoneEnd: " . $ end ->format ('Ymd\THis ' ) . "\n" ;
311- } else {
312- $ start ->setTimezone ($ utcTimezone );
313- $ calData .= 'DTSTART;VALUE=DATE-TIME: ' . $ start ->format ('Ymd\THis\Z ' ) . "\n" ;
314- $ end ->setTimezone ($ utcTimezone );
315- $ calData .= 'DTEND;VALUE=DATE-TIME: ' . $ end ->format ('Ymd\THis\Z ' ) . "\n" ;
316- }
317- } elseif (isset ($ e ['start ' ], $ e ['start ' ]['date ' ], $ e ['end ' ], $ e ['end ' ]['date ' ])) {
318- // whole days
319- $ start = new DateTime ($ e ['start ' ]['date ' ]);
320- $ calData .= 'DTSTART;VALUE=DATE: ' . $ start ->format ('Ymd ' ) . "\n" ;
321- $ end = new DateTime ($ e ['end ' ]['date ' ]);
322- $ calData .= 'DTEND;VALUE=DATE: ' . $ end ->format ('Ymd ' ) . "\n" ;
323- } else {
324- // skip entries without any date
378+ if ($ eventData == '' ) {
325379 continue ;
326380 }
327381
328- $ calData .= 'CLASS:PUBLIC ' . "\n"
329- . 'END:VEVENT ' . "\n"
382+ $ calData = 'BEGIN:VCALENDAR ' . "\n"
383+ . 'VERSION:2.0 ' . "\n"
384+ . 'PRODID:NextCloud Calendar ' . "\n"
385+ . $ eventData
330386 . 'END:VCALENDAR ' ;
331387
332388 if ($ existingEvent !== null ) {
@@ -352,7 +408,7 @@ public function importCalendar(string $userId, string $calId, string $calName, ?
352408 }
353409 }
354410
355- $ eventGeneratorReturn = $ events ->getReturn ();
411+ $ eventGeneratorReturn = $ eventsGenerator ->getReturn ();
356412 if (isset ($ eventGeneratorReturn ['error ' ])) {
357413 return $ eventGeneratorReturn ;
358414 }
0 commit comments