aggiunta calendario
This commit is contained in:
@@ -39,4 +39,35 @@ class ActivityLogController extends Controller
|
||||
|
||||
return view('admin.activity-logs.index', compact('logs', 'users', 'modules'));
|
||||
}
|
||||
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$query = ActivityLog::query();
|
||||
|
||||
if ($request->filled('user_id')) {
|
||||
$query->where('user_id', $request->user_id);
|
||||
}
|
||||
|
||||
if ($request->filled('module')) {
|
||||
$query->where('module', $request->module);
|
||||
}
|
||||
|
||||
if ($request->filled('action')) {
|
||||
$query->where('action', $request->action);
|
||||
}
|
||||
|
||||
if ($request->filled('from')) {
|
||||
$query->whereDate('created_at', '>=', $request->from);
|
||||
}
|
||||
|
||||
if ($request->filled('to')) {
|
||||
$query->whereDate('created_at', '<=', $request->to);
|
||||
}
|
||||
|
||||
$count = $query->count();
|
||||
$query->delete();
|
||||
|
||||
return redirect()->route('admin.activity-logs.index')
|
||||
->with('success', "Eliminati {$count} log.");
|
||||
}
|
||||
}
|
||||
@@ -257,6 +257,18 @@ class EmailSettingsController extends Controller
|
||||
return back()->with('success', 'Mittente eliminato.');
|
||||
}
|
||||
|
||||
public function destroy(): RedirectResponse
|
||||
{
|
||||
$this->authorizeDelete('settings');
|
||||
|
||||
$settings = EmailSetting::first();
|
||||
if ($settings) {
|
||||
$settings->delete();
|
||||
}
|
||||
|
||||
return redirect('/impostazioni/email')->with('success', 'Configurazione email eliminata. Tutti i dati sono stati rimossi.');
|
||||
}
|
||||
|
||||
public function senderTestSmtp(Request $request, int $id)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Services\CalDavService;
|
||||
use App\Services\GoogleCalendarSyncService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CalendarioConnessioneController extends Controller
|
||||
{
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'tipo' => 'required|in:google_calendar,caldav',
|
||||
'sync_direction' => 'nullable|in:import,export,bidirectional',
|
||||
'sync_interval_minutes' => 'nullable|integer|min:5|max:1440',
|
||||
'is_active' => 'boolean',
|
||||
'config' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$maxOrdine = CalendarioConnessione::max('ordine') ?? 0;
|
||||
|
||||
$connessione = new CalendarioConnessione();
|
||||
$connessione->nome = $data['nome'];
|
||||
$connessione->tipo = $data['tipo'];
|
||||
$connessione->sync_direction = $data['sync_direction'] ?? 'bidirectional';
|
||||
$connessione->sync_interval_minutes = $data['sync_interval_minutes'] ?? 60;
|
||||
$connessione->is_active = $request->boolean('is_active', true);
|
||||
$connessione->stato = 'disconnesso';
|
||||
$connessione->ordine = $maxOrdine + 1;
|
||||
$connessione->encryptAndSetConfig($data['config'] ?? []);
|
||||
$connessione->save();
|
||||
|
||||
return redirect('/impostazioni#calendario')->with('success', 'Connessione calendario "' . $connessione->nome . '" creata.');
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): RedirectResponse
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
$connessione = CalendarioConnessione::findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'tipo' => 'required|in:google_calendar,caldav',
|
||||
'sync_direction' => 'nullable|in:import,export,bidirectional',
|
||||
'sync_interval_minutes' => 'nullable|integer|min:5|max:1440',
|
||||
'is_active' => 'boolean',
|
||||
'config' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$connessione->nome = $data['nome'];
|
||||
$connessione->tipo = $data['tipo'];
|
||||
$connessione->sync_direction = $data['sync_direction'] ?? 'bidirectional';
|
||||
$connessione->sync_interval_minutes = $data['sync_interval_minutes'] ?? 60;
|
||||
$connessione->is_active = $request->boolean('is_active', true);
|
||||
|
||||
if (!empty($data['config'])) {
|
||||
$connessione->encryptAndSetConfig($data['config']);
|
||||
}
|
||||
|
||||
$connessione->save();
|
||||
|
||||
return redirect('/impostazioni#calendario')->with('success', 'Connessione calendario "' . $connessione->nome . '" aggiornata.');
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$this->authorizeDelete('settings');
|
||||
$connessione = CalendarioConnessione::findOrFail($id);
|
||||
$connessione->delete();
|
||||
|
||||
return response()->json(['success' => true, 'message' => 'Connessione calendario eliminata.']);
|
||||
}
|
||||
|
||||
public function test(int $id): JsonResponse
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$connessione = CalendarioConnessione::findOrFail($id);
|
||||
|
||||
$result = match ($connessione->tipo) {
|
||||
'caldav' => app(CalDavService::class)->testConnection($connessione),
|
||||
'google_calendar' => app(GoogleCalendarSyncService::class)->testConnection($connessione),
|
||||
default => ['success' => false, 'message' => 'Tipo sconosciuto'],
|
||||
};
|
||||
|
||||
if ($result['success']) {
|
||||
$connessione->update(['stato' => 'connesso', 'last_error_at' => null, 'last_error_message' => null]);
|
||||
} else {
|
||||
$connessione->update(['stato' => 'errore', 'last_error_at' => now(), 'last_error_message' => $result['message'] ?? 'Errore sconosciuto']);
|
||||
}
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
public function sync(int $id): RedirectResponse
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$connessione = CalendarioConnessione::findOrFail($id);
|
||||
|
||||
$result = match ($connessione->tipo) {
|
||||
'caldav' => app(CalDavService::class)->syncEvents($connessione),
|
||||
'google_calendar' => app(GoogleCalendarSyncService::class)->syncEvents($connessione),
|
||||
default => ['success' => false, 'imported' => 0, 'exported' => 0, 'errors' => ['Tipo sconosciuto']],
|
||||
};
|
||||
|
||||
$connessione->update(['last_sync_at' => now()]);
|
||||
|
||||
if ($result['success']) {
|
||||
$msg = 'Sincronizzazione completata: ' . $result['imported'] . ' importati, ' . $result['exported'] . ' esportati.';
|
||||
$connessione->update(['stato' => 'connesso']);
|
||||
return redirect('/impostazioni#calendario')->with('success', $msg);
|
||||
}
|
||||
|
||||
$errorMsg = implode('; ', $result['errors']);
|
||||
$connessione->update(['stato' => 'errore', 'last_error_at' => now(), 'last_error_message' => $errorMsg]);
|
||||
|
||||
return redirect('/impostazioni#calendario')->with('error', 'Errore sync: ' . $errorMsg);
|
||||
}
|
||||
|
||||
public function reorder(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$order = $request->input('order', []);
|
||||
foreach ($order as $index => $id) {
|
||||
CalendarioConnessione::where('id', $id)->update(['ordine' => $index]);
|
||||
}
|
||||
return response()->json(['success' => true]);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\AppSetting;
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Models\Documento;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\Ruolo;
|
||||
@@ -30,8 +31,9 @@ class ImpostazioniController extends Controller
|
||||
$senderAccounts = SenderAccount::orderBy('email_address')->get();
|
||||
$repositories = StorageRepository::orderBy('ordine')->get();
|
||||
$googleDriveNewToken = session('google_drive_new_token');
|
||||
$calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get();
|
||||
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken'));
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni'));
|
||||
}
|
||||
|
||||
public function saveAppSettings(Request $request)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class CalendarioConnessione extends Model
|
||||
{
|
||||
protected $table = 'calendario_connessioni';
|
||||
|
||||
protected $fillable = [
|
||||
'nome',
|
||||
'tipo',
|
||||
'config',
|
||||
'stato',
|
||||
'sync_direction',
|
||||
'sync_interval_minutes',
|
||||
'last_sync_at',
|
||||
'last_error_at',
|
||||
'last_error_message',
|
||||
'is_active',
|
||||
'ordine',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'config' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'sync_interval_minutes' => 'integer',
|
||||
'last_sync_at' => 'datetime',
|
||||
'last_error_at' => 'datetime',
|
||||
'ordine' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeAttivi($query)
|
||||
{
|
||||
return $query->where('is_active', true)->orderBy('ordine');
|
||||
}
|
||||
|
||||
public function getDecryptedConfig(): array
|
||||
{
|
||||
$config = $this->config;
|
||||
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
|
||||
if (isset($config[$field])) {
|
||||
try {
|
||||
$config[$field] = Crypt::decryptString($config[$field]);
|
||||
} catch (\Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function encryptAndSetConfig(array $config): void
|
||||
{
|
||||
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
|
||||
if (isset($config[$field]) && $config[$field] !== '' && !str_starts_with($config[$field], 'eyJpdiI')) {
|
||||
$config[$field] = Crypt::encryptString($config[$field]);
|
||||
}
|
||||
}
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public static function opzioniTipo(): array
|
||||
{
|
||||
return ['google_calendar', 'caldav'];
|
||||
}
|
||||
|
||||
public static function etichettaTipo(string $tipo): string
|
||||
{
|
||||
return match ($tipo) {
|
||||
'google_calendar' => 'Google Calendar',
|
||||
'caldav' => 'CalDAV (Infomaniak/NextCloud)',
|
||||
default => ucfirst($tipo),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Models\Evento;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CalDavService
|
||||
{
|
||||
private const CALDAV_NS = 'urn:ietf:params:xml:ns:caldav';
|
||||
private const DAV_NS = 'DAV:';
|
||||
|
||||
public function testConnection(CalendarioConnessione $connessione): array
|
||||
{
|
||||
$config = $connessione->getDecryptedConfig();
|
||||
$client = $this->buildClient($config);
|
||||
|
||||
try {
|
||||
$response = $client->request('PROPFIND', $config['url'], [
|
||||
'headers' => ['Depth' => '0'],
|
||||
'body' => '<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/">
|
||||
<d:prop>
|
||||
<d:displayname/>
|
||||
<cs:getctag/>
|
||||
</d:prop>
|
||||
</d:propfind>',
|
||||
]);
|
||||
|
||||
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' => '<?xml version="1.0" encoding="utf-8"?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<d:resourcetype/>
|
||||
<d:displayname/>
|
||||
<c:supported-calendar-component-set/>
|
||||
</d:prop>
|
||||
</d:propfind>',
|
||||
]);
|
||||
|
||||
$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 = '<?xml version="1.0" encoding="utf-8"?>
|
||||
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<d:getetag/>
|
||||
<c:calendar-data/>
|
||||
</d:prop>
|
||||
<c:filter>
|
||||
<c:comp-filter name="VCALENDAR">
|
||||
<c:comp-filter name="VEVENT">
|
||||
<c:time-range start="' . $start . '" end="' . $end . '"/>
|
||||
</c:comp-filter>
|
||||
</c:comp-filter>
|
||||
</c:filter>
|
||||
</c:calendar-query>';
|
||||
|
||||
$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' => '<?xml version="1.0" encoding="utf-8"?>
|
||||
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<d:getetag/>
|
||||
<d:resourcetype/>
|
||||
</d:prop>
|
||||
<c:filter>
|
||||
<c:comp-filter name="VCALENDAR">
|
||||
<c:comp-filter name="VEVENT"/>
|
||||
</c:comp-filter>
|
||||
</c:filter>
|
||||
</c:calendar-query>',
|
||||
]);
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Models\Evento;
|
||||
use Carbon\Carbon;
|
||||
use Google\Client as GoogleClient;
|
||||
use Google\Service\Calendar as GoogleCalendar;
|
||||
use Google\Service\Calendar\Event as GoogleEvent;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GoogleCalendarSyncService
|
||||
{
|
||||
private ?GoogleCalendar $calendarService = null;
|
||||
private ?string $calendarId = null;
|
||||
|
||||
public function testConnection(CalendarioConnessione $connessione): array
|
||||
{
|
||||
try {
|
||||
$client = $this->buildClient($connessione);
|
||||
$service = new GoogleCalendar($client);
|
||||
$calendarList = $service->calendarList->listCalendarList();
|
||||
return ['success' => true, 'message' => 'Connessione Google Calendar riuscita. Trovati ' . count($calendarList->getItems()) . ' calendari.'];
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function listCalendars(CalendarioConnessione $connessione): array
|
||||
{
|
||||
try {
|
||||
$client = $this->buildClient($connessione);
|
||||
$service = new GoogleCalendar($client);
|
||||
$calendarList = $service->calendarList->listCalendarList();
|
||||
$calendars = [];
|
||||
foreach ($calendarList->getItems() as $cal) {
|
||||
$calendars[] = [
|
||||
'id' => $cal->getId(),
|
||||
'summary' => $cal->getSummary(),
|
||||
];
|
||||
}
|
||||
return $calendars;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('GoogleCalendar listCalendars: ' . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function syncEvents(CalendarioConnessione $connessione): array
|
||||
{
|
||||
$config = $connessione->getDecryptedConfig();
|
||||
$imported = 0;
|
||||
$exported = 0;
|
||||
$errors = [];
|
||||
|
||||
try {
|
||||
$client = $this->buildClient($connessione);
|
||||
$this->calendarService = new GoogleCalendar($client);
|
||||
$this->calendarId = $config['calendar_id'] ?? 'primary';
|
||||
|
||||
$direction = $connessione->sync_direction ?? 'bidirectional';
|
||||
|
||||
if ($direction === 'import' || $direction === 'bidirectional') {
|
||||
$result = $this->importRemoteEvents($connessione);
|
||||
$imported = $result['count'];
|
||||
foreach ($result['errors'] as $err) {
|
||||
$errors[] = $err;
|
||||
}
|
||||
}
|
||||
|
||||
if ($direction === 'export' || $direction === 'bidirectional') {
|
||||
$result = $this->exportLocalEvents($connessione);
|
||||
$exported = $result['count'];
|
||||
foreach ($result['errors'] as $err) {
|
||||
$errors[] = $err;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
Log::error('GoogleCalendar syncEvents: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => empty($errors),
|
||||
'imported' => $imported,
|
||||
'exported' => $exported,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
private function importRemoteEvents(CalendarioConnessione $connessione): array
|
||||
{
|
||||
$count = 0;
|
||||
$errors = [];
|
||||
$service = $this->calendarService;
|
||||
|
||||
try {
|
||||
$timeMin = Carbon::now()->subYear()->toRfc3339String();
|
||||
$timeMax = Carbon::now()->addYear()->toRfc3339String();
|
||||
|
||||
$optParams = [
|
||||
'timeMin' => $timeMin,
|
||||
'timeMax' => $timeMax,
|
||||
'singleEvents' => true,
|
||||
'orderBy' => 'startTime',
|
||||
];
|
||||
|
||||
$events = $service->events->listEvents($this->calendarId, $optParams);
|
||||
|
||||
while (true) {
|
||||
foreach ($events->getItems() as $event) {
|
||||
try {
|
||||
$uid = $event->getId();
|
||||
$existing = Evento::where('uid_esterno', $uid)->first();
|
||||
if ($existing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$summary = $event->getSummary() ?? 'Evento importato';
|
||||
$description = strip_tags($event->getDescription() ?? '');
|
||||
$location = $event->getLocation() ?? '';
|
||||
|
||||
$start = $event->getStart();
|
||||
$end = $event->getEnd();
|
||||
|
||||
$data = [
|
||||
'nome_evento' => mb_substr($summary, 0, 255),
|
||||
'descrizione_evento' => mb_substr($description, 0, 255),
|
||||
'tipo_recorrenza' => 'singolo',
|
||||
'luogo_indirizzo' => $location,
|
||||
'uid_esterno' => $uid,
|
||||
];
|
||||
|
||||
if ($start && $start->getDateTime()) {
|
||||
$dt = Carbon::parse($start->getDateTime());
|
||||
$data['data_specifica'] = $dt->format('Y-m-d');
|
||||
$data['ora_inizio'] = $dt->format('H:i');
|
||||
} elseif ($start && $start->getDate()) {
|
||||
$data['data_specifica'] = $start->getDate();
|
||||
}
|
||||
|
||||
if ($end && $start && $end->getDateTime() && $start->getDateTime()) {
|
||||
$data['durata_minuti'] = (int) Carbon::parse($start->getDateTime())->diffInMinutes(Carbon::parse($end->getDateTime()));
|
||||
}
|
||||
|
||||
if ($event->getRecurrence()) {
|
||||
foreach ($event->getRecurrence() as $rrule) {
|
||||
if (str_starts_with($rrule, 'RRULE:')) {
|
||||
$parsed = $this->parseRRule(substr($rrule, 6));
|
||||
$data = array_merge($data, $parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Evento::create($data);
|
||||
$count++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Errore import evento: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$pageToken = $events->getNextPageToken();
|
||||
if ($pageToken) {
|
||||
$optParams['pageToken'] = $pageToken;
|
||||
$events = $service->events->listEvents($this->calendarId, $optParams);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Errore import Google Calendar: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
return ['count' => $count, 'errors' => $errors];
|
||||
}
|
||||
|
||||
private function exportLocalEvents(CalendarioConnessione $connessione): array
|
||||
{
|
||||
$count = 0;
|
||||
$errors = [];
|
||||
$service = $this->calendarService;
|
||||
|
||||
try {
|
||||
$localEvents = Evento::all();
|
||||
$remoteEventMap = $this->getRemoteEventMap($service);
|
||||
|
||||
foreach ($localEvents as $evento) {
|
||||
try {
|
||||
$googleEvent = new GoogleEvent();
|
||||
$googleEvent->setSummary($evento->nome_evento);
|
||||
|
||||
if ($evento->descrizione_evento) {
|
||||
$googleEvent->setDescription($evento->descrizione_evento);
|
||||
}
|
||||
if ($evento->luogo_indirizzo) {
|
||||
$googleEvent->setLocation($evento->luogo_indirizzo);
|
||||
}
|
||||
|
||||
$startData = $this->buildGoogleDateTime($evento, 'start');
|
||||
$endData = $this->buildGoogleDateTime($evento, 'end');
|
||||
$googleEvent->setStart($startData);
|
||||
$googleEvent->setEnd($endData);
|
||||
|
||||
if ($evento->tipo_recorrenza !== 'singolo') {
|
||||
$rrule = $this->buildGoogleRRule($evento);
|
||||
if ($rrule) {
|
||||
$googleEvent->setRecurrence(['RRULE:' . $rrule]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($evento->uid_esterno && isset($remoteEventMap[$evento->uid_esterno])) {
|
||||
$service->events->update($this->calendarId, $evento->uid_esterno, $googleEvent);
|
||||
} else {
|
||||
$created = $service->events->insert($this->calendarId, $googleEvent);
|
||||
if (!$evento->uid_esterno) {
|
||||
$evento->updateQuietly(['uid_esterno' => $created->getId()]);
|
||||
}
|
||||
}
|
||||
$count++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Errore export ' . $evento->nome_evento . ': ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'Errore export Google Calendar: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
return ['count' => $count, 'errors' => $errors];
|
||||
}
|
||||
|
||||
private function getRemoteEventMap(GoogleCalendar $service): array
|
||||
{
|
||||
$map = [];
|
||||
try {
|
||||
$events = $service->events->listEvents($this->calendarId);
|
||||
foreach ($events->getItems() as $event) {
|
||||
$map[$event->getId()] = true;
|
||||
}
|
||||
} catch (\Exception) {
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
private function buildGoogleDateTime(Evento $evento, string $type): \Google\Service\Calendar\EventDateTime
|
||||
{
|
||||
$dt = new \Google\Service\Calendar\EventDateTime();
|
||||
|
||||
if ($type === 'start') {
|
||||
if ($evento->data_specifica) {
|
||||
$date = $evento->data_specifica instanceof Carbon ? $evento->data_specifica : Carbon::parse($evento->data_specifica);
|
||||
if ($evento->ora_inizio) {
|
||||
$time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio);
|
||||
$dt->setDateTime($date->format('Y-m-d') . 'T' . $time->format('H:i:s'));
|
||||
$dt->setTimeZone('Europe/Rome');
|
||||
} else {
|
||||
$dt->setDate($date->format('Y-m-d'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($evento->data_specifica) {
|
||||
$date = $evento->data_specifica instanceof Carbon ? $evento->data_specifica : Carbon::parse($evento->data_specifica);
|
||||
$durata = (int) ($evento->durata_minuti ?? 60);
|
||||
if ($evento->ora_inizio) {
|
||||
$time = $evento->ora_inizio instanceof Carbon ? $evento->ora_inizio : Carbon::parse($evento->ora_inizio);
|
||||
$endTime = $time->copy()->addMinutes($durata);
|
||||
$dt->setDateTime($date->format('Y-m-d') . 'T' . $endTime->format('H:i:s'));
|
||||
$dt->setTimeZone('Europe/Rome');
|
||||
} else {
|
||||
$dt->setDate($date->copy()->addDay()->format('Y-m-d'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dt;
|
||||
}
|
||||
|
||||
private function buildGoogleRRule(Evento $evento): ?string
|
||||
{
|
||||
$dayMap = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
||||
|
||||
return match ($evento->tipo_recorrenza) {
|
||||
'settimanale' => $evento->giorno_settimana !== null
|
||||
? 'FREQ=WEEKLY;BYDAY=' . ($dayMap[(int) $evento->giorno_settimana] ?? 'MO')
|
||||
: null,
|
||||
'mensile' => $evento->occorrenza_mese
|
||||
? 'FREQ=MONTHLY;BYSETPOS=' . $evento->occorrenza_mese
|
||||
: null,
|
||||
'annuale' => $evento->mese_annuale
|
||||
? 'FREQ=YEARLY;BYMONTH=' . $evento->mese_annuale
|
||||
: null,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function parseRRule(string $rrule): array
|
||||
{
|
||||
$result = [];
|
||||
$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') {
|
||||
$dayMap = ['SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6];
|
||||
if (preg_match('/^(-?\d+)?([A-Z]+)$/', $value, $m)) {
|
||||
$result['giorno_settimana'] = $dayMap[$m[2]] ?? null;
|
||||
if (!empty($m[1])) {
|
||||
$result['occorrenza_mese'] = $m[1] . ',' . ($dayMap[$m[2]] ?? '1');
|
||||
}
|
||||
}
|
||||
} elseif ($key === 'BYMONTHDAY') {
|
||||
$result['giorno_mese'] = (int) $value;
|
||||
} elseif ($key === 'BYMONTH') {
|
||||
$result['mese_annuale'] = (int) $value;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function buildClient(CalendarioConnessione $connessione): GoogleClient
|
||||
{
|
||||
$config = $connessione->getDecryptedConfig();
|
||||
|
||||
$client = new GoogleClient();
|
||||
$client->setApplicationName(config('app.name'));
|
||||
$client->setScopes([GoogleCalendar::CALENDAR]);
|
||||
$client->setAuthConfig([
|
||||
'web' => [
|
||||
'client_id' => $config['client_id'] ?? '',
|
||||
'client_secret' => $config['client_secret'] ?? '',
|
||||
],
|
||||
]);
|
||||
$client->setAccessType('offline');
|
||||
$client->setPrompt('consent');
|
||||
|
||||
if (!empty($config['refresh_token'])) {
|
||||
$client->fetchAccessTokenWithRefreshToken($config['refresh_token']);
|
||||
}
|
||||
|
||||
return $client;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user