getDecryptedConfig(); $client = $this->buildClient($config); try { $response = $client->request('PROPFIND', $config['url'], [ 'headers' => ['Depth' => '0'], 'body' => ' ', ]); if ($response->getStatusCode() < 300) { return ['success' => true, 'message' => 'Connessione CalDAV riuscita.']; } return ['success' => false, 'message' => 'Errore HTTP: ' . $response->getStatusCode()]; } catch (\Exception $e) { return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()]; } } public function listCalendars(CalendarioConnessione $connessione): array { $config = $connessione->getDecryptedConfig(); $client = $this->buildClient($config); try { $response = $client->request('PROPFIND', $config['url'], [ 'headers' => ['Depth' => '1'], 'body' => ' ', ]); $body = (string) $response->getBody(); return $this->parseCalendarList($body); } catch (\Exception $e) { Log::error('CalDAV listCalendars: ' . $e->getMessage()); return []; } } public function syncEvents(CalendarioConnessione $connessione): array { $config = $connessione->getDecryptedConfig(); $client = $this->buildClient($config); $calendarUrl = $config['calendar_url'] ?? $config['url']; $imported = 0; $exported = 0; $errors = []; $start = Carbon::now()->subYear()->format('Ymd\THis\Z'); $end = Carbon::now()->addYear()->format('Ymd\THis\Z'); $direction = $connessione->sync_direction ?? 'bidirectional'; try { if ($direction === 'import' || $direction === 'bidirectional') { $result = $this->fetchRemoteEvents($client, $calendarUrl, $start, $end); $imported = $this->importEvents($result, $errors); } if ($direction === 'export' || $direction === 'bidirectional') { $localEvents = Evento::where(function ($q) use ($connessione) { $q->whereNotNull('uid_esterno') ->orWhereNull('uid_esterno'); })->get(); $exported = $this->exportEvents($client, $calendarUrl, $localEvents, $connessione, $errors); } } catch (\Exception $e) { $errors[] = $e->getMessage(); Log::error('CalDAV syncEvents: ' . $e->getMessage()); } return [ 'success' => empty($errors), 'imported' => $imported, 'exported' => $exported, 'errors' => $errors, ]; } private function fetchRemoteEvents($client, string $url, string $start, string $end): array { $body = ' '; $response = $client->request('REPORT', $url, [ 'headers' => ['Depth' => '1', 'Content-Type' => 'application/xml; charset=utf-8'], 'body' => $body, ]); return $this->parseCalendarData((string) $response->getBody()); } private function importEvents(array $remoteEvents, array &$errors): int { $count = 0; foreach ($remoteEvents as $event) { try { $vcalendar = \Sabre\VObject\Reader::read($event['data']); foreach ($vcalendar->VEVENT as $vevent) { $uid = (string) ($vevent->UID ?? Str::uuid()); $existing = Evento::where('uid_esterno', $uid)->first(); if ($existing) { continue; } $summary = (string) ($vevent->SUMMARY ?? 'Evento importato'); $description = (string) ($vevent->DESCRIPTION ?? ''); $location = (string) ($vevent->LOCATION ?? ''); $dtstart = $vevent->DTSTART ? $this->parseDateTime($vevent->DTSTART) : null; $dtend = $vevent->DTEND ? $this->parseDateTime($vevent->DTEND) : null; $data = [ 'nome_evento' => $summary, 'descrizione_evento' => mb_substr($description, 0, 255), 'tipo_recorrenza' => 'singolo', 'luogo_indirizzo' => $location, 'uid_esterno' => $uid, ]; if ($dtstart) { $data['data_specifica'] = $dtstart->format('Y-m-d'); $data['ora_inizio'] = $dtstart->format('H:i'); } if ($dtend && $dtstart) { $data['durata_minuti'] = (int) $dtstart->diffInMinutes($dtend); } $rrule = $vevent->RRULE ? (string) $vevent->RRULE : null; if ($rrule) { $parsed = $this->parseRRule($rrule); $data = array_merge($data, $parsed); } Evento::create($data); $count++; } } catch (\Exception $e) { $errors[] = 'Errore parsing evento: ' . $e->getMessage(); } } return $count; } private function exportEvents($client, string $url, iterable $localEvents, CalendarioConnessione $connessione, array &$errors): int { $count = 0; $existingUids = $this->getExistingEventUids($client, $url); foreach ($localEvents as $evento) { try { if ($evento->uid_esterno && !in_array($evento->uid_esterno, $existingUids)) { continue; } $uid = $evento->uid_esterno ?? ('local-' . $evento->id . '-' . Str::uuid()); $icsData = $this->buildIcsForExport($evento, $uid); $eventUrl = rtrim($url, '/') . '/' . $uid . '.ics'; $response = $client->request('PUT', $eventUrl, [ 'headers' => ['Content-Type' => 'text/calendar; charset=utf-8'], 'body' => $icsData, ]); if ($response->getStatusCode() < 300) { if (!$evento->uid_esterno) { $evento->updateQuietly(['uid_esterno' => $uid]); } $count++; } } catch (\Exception $e) { $errors[] = 'Errore export ' . $evento->nome_evento . ': ' . $e->getMessage(); } } return $count; } private function getExistingEventUids($client, string $url): array { try { $response = $client->request('REPORT', $url, [ 'headers' => ['Depth' => '1'], 'body' => ' ', ]); $body = (string) $response->getBody(); $xml = simplexml_load_string($body); $xml->registerXPathNamespace('d', self::DAV_NS); $uids = []; foreach ($xml->xpath('//d:href') as $href) { $path = (string) $href; $name = basename($path); if (str_ends_with($name, '.ics')) { $uids[] = str_replace('.ics', '', $name); } } return $uids; } catch (\Exception) { return []; } } private function buildIcsForExport(Evento $evento, string $uid): string { $service = app(IcsExportService::class); return $service->generateSingle($evento); } private function parseCalendarList(string $xml): array { $calendars = []; try { $dom = new \DOMDocument(); $dom->loadXML($xml); $xpath = new \DOMXPath($dom); $xpath->registerNamespace('d', self::DAV_NS); $xpath->registerNamespace('c', self::CALDAV_NS); $responses = $xpath->query('//d:multistatus/d:response'); foreach ($responses as $response) { $href = $xpath->query('d:href', $response)->item(0)?->textContent ?? ''; $displayName = $xpath->query('d:propstat/d:prop/d:displayname', $response)->item(0)?->textContent ?? $href; $hasCalendar = false; $resourcetypes = $xpath->query('d:propstat/d:prop/d:resourcetype/*', $response); foreach ($resourcetypes as $type) { if ($type->localName === 'calendar' && $type->namespaceURI === self::CALDAV_NS) { $hasCalendar = true; break; } } if ($hasCalendar && $href) { $calendars[] = [ 'href' => $href, 'displayname' => $displayName, ]; } } } catch (\Exception $e) { Log::error('CalDAV parseCalendarList: ' . $e->getMessage()); } return $calendars; } private function parseCalendarData(string $xml): array { $events = []; try { $dom = new \DOMDocument(); $dom->loadXML($xml); $xpath = new \DOMXPath($dom); $xpath->registerNamespace('d', self::DAV_NS); $xpath->registerNamespace('c', self::CALDAV_NS); $responses = $xpath->query('//d:multistatus/d:response'); foreach ($responses as $response) { $dataNodes = $xpath->query('d:propstat/d:prop/c:calendar-data', $response); foreach ($dataNodes as $node) { $events[] = ['data' => $node->textContent]; } } } catch (\Exception $e) { Log::error('CalDAV parseCalendarData: ' . $e->getMessage()); } return $events; } private function parseDateTime($dt): ?Carbon { try { if ($dt instanceof \Sabre\VObject\Property\ICalendar\DateTime) { $dtValue = $dt->getDateTime(); return Carbon::instance($dtValue); } } catch (\Exception) { } return null; } private function parseRRule(string $rrule): array { $result = []; try { $parts = explode(';', $rrule); foreach ($parts as $part) { [$key, $value] = explode('=', $part, 2); if ($key === 'FREQ') { $result['tipo_recorrenza'] = match (strtolower($value)) { 'weekly' => 'settimanale', 'monthly' => 'mensile', 'yearly' => 'annuale', default => 'singolo', }; } elseif ($key === 'BYDAY' && isset($result['tipo_recorrenza'])) { $dayMap = ['SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6]; $byDay = strtoupper($value); if (preg_match('/^(-?\d+)?([A-Z]+)$/', $byDay, $m)) { $result['giorno_settimana'] = $dayMap[$m[2]] ?? null; if (!empty($m[1]) && $result['tipo_recorrenza'] === 'mensile') { $result['occorrenza_mese'] = $m[1] . ',' . ($dayMap[$m[2]] ?? '1'); } } } elseif ($key === 'BYMONTHDAY') { $result['giorno_mese'] = (int) $value; } elseif ($key === 'BYMONTH') { if ($result['tipo_recorrenza'] ?? '' === 'annuale') { $result['mese_annuale'] = (int) $value; } else { $result['mesi_recorrenza'] = explode(',', $value); } } } } catch (\Exception) { } return $result; } private function buildClient(array $config): \GuzzleHttp\Client { $options = [ 'verify' => false, 'timeout' => 30, 'http_errors' => false, ]; if (!empty($config['username']) && !empty($config['password'])) { $options['auth'] = [$config['username'], $config['password'], 'basic']; } return new \GuzzleHttp\Client($options); } }