@@ -42,12 +42,17 @@ class Calendar extends Model implements JsonSerializable {
4242 'permissions ' ,
4343 'custom_fields ' ,
4444 'title_template ' ,
45+ 'sync_version ' ,
46+ 'last_full_sync ' ,
47+ 'force_legacy_mode ' ,
4548 ];
4649
4750 protected $ casts = [
4851 'enabled ' => 'boolean ' ,
4952 'permissions ' => 'array ' ,
5053 'custom_fields ' => 'array ' ,
54+ 'force_legacy_mode ' => 'boolean ' ,
55+ 'last_full_sync ' => 'datetime ' ,
5156 ];
5257
5358 /**
@@ -247,11 +252,84 @@ private function getICSAsCalendarItems( int $daysBeforeToday, int $daysAfterToda
247252 * @throws Exception
248253 */
249254 public function findEventById ( string $ eventId ): ?array {
255+ // Check if we should use legacy mode
256+ $ forceLegacy = config ('ljpccalendarmodule.performance.force_legacy_mode ' , false ) || $ this ->force_legacy_mode ;
257+ $ enableEventIndex = config ('ljpccalendarmodule.performance.enable_event_index ' , false );
258+ $ enableCalDAVReports = config ('ljpccalendarmodule.performance.enable_caldav_reports ' , false );
259+
250260 if ( $ this ->type === 'normal ' ) {
251261 // This case is already handled efficiently in the controller
252262 return null ;
253263 }
254264
265+ // Step 1: Try database index if enabled and not in legacy mode
266+ if ( !$ forceLegacy && $ enableEventIndex ) {
267+ try {
268+ $ startTime = microtime (true );
269+
270+ $ indexedEvent = CalendarEventIndex::where ('calendar_id ' , $ this ->id )
271+ ->where ('event_uid ' , $ eventId )
272+ ->first ();
273+
274+ if ( $ indexedEvent ) {
275+ // Check if the data is fresh based on refresh interval
276+ $ refreshString = $ this ->custom_fields ['refresh ' ] ?? '1 hour ' ;
277+ $ refreshInterval = $ this ->getRefreshIntervalInSeconds ($ refreshString );
278+ if ( $ indexedEvent ->isFresh ($ refreshInterval ) ) {
279+ $ indexedEvent ->recordAccess ();
280+
281+ // Log performance metrics
282+ if ( config ('ljpccalendarmodule.performance.enable_metrics ' , false ) ) {
283+ Log::info ('Calendar event found in index ' , [
284+ 'calendar_id ' => $ this ->id ,
285+ 'event_uid ' => $ eventId ,
286+ 'lookup_time_ms ' => round ((microtime (true ) - $ startTime ) * 1000 , 2 ),
287+ 'cache_hit ' => true
288+ ]);
289+ }
290+
291+ return $ indexedEvent ->toCalendarItemArray ();
292+ }
293+ }
294+ } catch ( \Exception $ e ) {
295+ Log::error ('Error accessing event index ' , [
296+ 'calendar_id ' => $ this ->id ,
297+ 'event_uid ' => $ eventId ,
298+ 'error ' => $ e ->getMessage ()
299+ ]);
300+ // Continue to other methods
301+ }
302+ }
303+
304+ // Step 2: For CalDAV, try REPORT query if enabled
305+ if ( !$ forceLegacy && $ this ->type === 'caldav ' && $ enableCalDAVReports ) {
306+ try {
307+ $ eventData = $ this ->fetchCalDAVEventByUid ( $ eventId );
308+ if ( $ eventData !== null ) {
309+ // Update index if enabled
310+ if ( $ enableEventIndex ) {
311+ try {
312+ CalendarEventIndex::updateFromEventData ( $ this ->id , $ eventData , $ this ->sync_version );
313+ } catch ( \Exception $ e ) {
314+ Log::warning ('Failed to update event index after CalDAV fetch ' , [
315+ 'calendar_id ' => $ this ->id ,
316+ 'event_uid ' => $ eventId ,
317+ 'error ' => $ e ->getMessage ()
318+ ]);
319+ }
320+ }
321+
322+ return $ eventData ;
323+ }
324+ } catch ( \Exception $ e ) {
325+ Log::warning ('CalDAV REPORT query failed, falling back to full fetch ' , [
326+ 'calendar_id ' => $ this ->id ,
327+ 'event_uid ' => $ eventId ,
328+ 'error ' => $ e ->getMessage ()
329+ ]);
330+ }
331+ }
332+
255333 if ( $ this ->type === 'ics ' || $ this ->type === 'caldav ' ) {
256334 // Try to get the event from cache first
257335 $ eventCacheKey = 'calendar_event_ ' . $ this ->id . '_ ' . md5 ( $ eventId );
@@ -434,5 +512,141 @@ public function findEventById( string $eventId ): ?array {
434512 return null ;
435513 }
436514
515+ /**
516+ * Fetch a single CalDAV event by UID using REPORT query
517+ *
518+ * @param string $eventId The event UID
519+ * @return array|null The event data or null if not found
520+ */
521+ protected function fetchCalDAVEventByUid ( string $ eventId ): ?array {
522+ $ fullUrl = $ this ->custom_fields ['url ' ];
523+ $ baseUrl = substr ( $ fullUrl , 0 , strpos ( $ fullUrl , '/ ' , 8 ) );
524+ $ remainingUrl = substr ( $ fullUrl , strpos ( $ fullUrl , '/ ' , 8 ) );
525+
526+ $ caldavClient = new CalDAV ( $ baseUrl , $ this ->custom_fields ['username ' ], $ this ->custom_fields ['password ' ] );
527+
528+ // Check if server supports REPORT queries
529+ if ( !$ caldavClient ->supportsReportQuery ( $ remainingUrl ) ) {
530+ Log::info ('CalDAV server does not support REPORT queries ' , [
531+ 'calendar_id ' => $ this ->id ,
532+ 'url ' => $ fullUrl
533+ ]);
534+ return null ;
535+ }
536+
537+ // Try to fetch the specific event
538+ $ icsData = $ caldavClient ->getEventByUid ( $ remainingUrl , $ eventId );
539+
540+ if ( $ icsData === null ) {
541+ return null ;
542+ }
543+
544+ // Parse the ICS data
545+ try {
546+ $ ical = new ICal ();
547+ $ ical ->initString ( $ icsData );
548+
549+ $ events = $ ical ->events ();
550+ if ( empty ( $ events ) ) {
551+ return null ;
552+ }
553+
554+ // Process the first (and should be only) event
555+ $ event = $ events [0 ];
556+
557+ $ start = DateTimeImmutable::createFromMutable (
558+ $ ical ->iCalDateToDateTime ( $ event ->dtstart_array [3 ] )->setTimezone ( new DateTimeZone ( 'UTC ' ) )
559+ );
560+
561+ if ( ! empty ( $ event ->dtend ) ) {
562+ $ end = DateTimeImmutable::createFromMutable (
563+ $ ical ->iCalDateToDateTime ( $ event ->dtend_array [3 ] )->setTimezone ( new DateTimeZone ( 'UTC ' ) )
564+ );
565+ } else {
566+ if ( ! empty ( $ event ->duration ) ) {
567+ $ end = $ start ->add ( new DateInterval ( $ event ->duration ) );
568+ } else {
569+ $ end = $ start ->add ( new DateInterval ( 'PT1H ' ) );
570+ }
571+ }
572+
573+ $ modifiedEnd = $ end ;
574+ if ( DateTimeRange::isAllDay ( $ start , $ end ) ) {
575+ $ modifiedEnd = $ end ->modify ( '-1 second ' );
576+ }
577+
578+ $ body = $ event ->description ;
579+ $ customFields = [];
580+
581+ // Parse YAML from description if present
582+ try {
583+ if ( $ body !== null ) {
584+ $ parsedResult = Yaml::parse ( $ body , Loader::IGNORE_COMMENTS | Loader::IGNORE_DIRECTIVES | Loader::NO_OBJECT_FOR_DATE );
585+ // Handle both array and object results
586+ if ( is_object ( $ parsedResult ) && method_exists ( $ parsedResult , 'jsonSerialize ' ) ) {
587+ $ parsedYaml = $ parsedResult ->jsonSerialize ();
588+ } else {
589+ $ parsedYaml = $ parsedResult ;
590+ }
591+ if ( is_array ( $ parsedYaml ) && array_has ( $ parsedYaml , 'body ' ) ) {
592+ $ body = $ parsedYaml ['body ' ];
593+ if ( isset ( $ parsedYaml ['custom_fields ' ] ) ) {
594+ $ customFields = $ parsedYaml ['custom_fields ' ];
595+ }
596+ }
597+ }
598+ } catch ( Exception $ e ) {
599+ // Ignore parsing errors
600+ }
601+
602+ // Create CalendarItem-compatible array
603+ return [
604+ 'id ' => $ event ->uid ,
605+ 'uid ' => $ event ->uid ,
606+ 'calendar_id ' => $ this ->id ,
607+ 'title ' => $ event ->summary ,
608+ 'location ' => $ event ->location ,
609+ 'body ' => $ body ,
610+ 'state ' => $ event ->status ,
611+ 'start ' => $ start ->format ( 'Y-m-d H:i:s ' ),
612+ 'end ' => $ modifiedEnd ->format ( 'Y-m-d H:i:s ' ),
613+ 'is_all_day ' => DateTimeRange::isAllDay ( $ start , $ end ),
614+ 'is_private ' => false ,
615+ 'is_read_only ' => false , // CalDAV events can be edited
616+ 'custom_fields ' => $ customFields ,
617+ ];
618+
619+ } catch ( \Exception $ e ) {
620+ Log::error ('Failed to parse CalDAV event data ' , [
621+ 'calendar_id ' => $ this ->id ,
622+ 'event_uid ' => $ eventId ,
623+ 'error ' => $ e ->getMessage ()
624+ ]);
625+ return null ;
626+ }
627+ }
628+
629+ /**
630+ * Convert refresh interval string to seconds
631+ *
632+ * @param string $refreshString
633+ * @return int
634+ */
635+ protected function getRefreshIntervalInSeconds ( string $ refreshString ): int {
636+ $ intervals = [
637+ '1 minute ' => 60 ,
638+ '5 minutes ' => 300 ,
639+ '15 minutes ' => 900 ,
640+ '30 minutes ' => 1800 ,
641+ '1 hour ' => 3600 ,
642+ '2 hours ' => 7200 ,
643+ '6 hours ' => 21600 ,
644+ '12 hours ' => 43200 ,
645+ 'daily ' => 86400 ,
646+ ];
647+
648+ return $ intervals [$ refreshString ] ?? 3600 ; // Default to 1 hour
649+ }
650+
437651
438652}
0 commit comments