Line data Source code
1 : #pragma once
2 :
3 : #include <QList>
4 : #include <QObject>
5 : #include <QSet>
6 : #include <QString>
7 :
8 : #include "data/CalendarModels.h"
9 :
10 : class QNetworkAccessManager;
11 : class QNetworkReply;
12 : class QTimer;
13 :
14 : // ═══════════════════════════════════════════════════════
15 : // CalDAV client for calendar discovery and sync (Sprint 32 – T-331)
16 : // Architecture mirrors CardDavClient (Sprint 16).
17 : // ═══════════════════════════════════════════════════════
18 :
19 : class CalDavClient : public QObject {
20 : Q_OBJECT
21 : public:
22 : CalDavClient(const QString &serverUrl, const QString &username,
23 : const QString &password, QObject *parent = nullptr);
24 :
25 : /// Discover available calendars via PROPFIND
26 : void discoverCalendars();
27 :
28 : /// Sync all events (VEVENT) from a calendar via REPORT
29 : void syncCalendar(const QString &calendarPath);
30 :
31 : /// Sync all tasks (VTODO) from a calendar via REPORT
32 : void syncTasks(const QString &calendarPath);
33 :
34 : // ── Sprint 39 – T-530: Write API ──────────────────────
35 :
36 : /// Create a new event on the server (HTTP PUT + If-None-Match)
37 : void createEvent(const QString &calendarPath, const CalendarEvent &event);
38 : /// Update an existing event (HTTP PUT + If-Match: etag)
39 : void updateEvent(const CalendarEvent &event);
40 : /// Delete an event (HTTP DELETE + If-Match: etag)
41 : void deleteEvent(const CalendarEvent &event);
42 :
43 : /// Create a new task on the server
44 : void createTask(const QString &calendarPath, const CalendarTask &task);
45 : /// Update an existing task
46 : void updateTask(const CalendarTask &task);
47 : /// Delete a task
48 : void deleteTask(const CalendarTask &task);
49 :
50 : /// Serialize a CalendarEvent to iCalendar format (RFC 5545 VEVENT)
51 : static QByteArray eventToICalendar(const CalendarEvent &event);
52 : /// Serialize a CalendarTask to iCalendar format (RFC 5545 VTODO)
53 : static QByteArray taskToICalendar(const CalendarTask &task);
54 :
55 : /// Test seam: replace the network access manager (takes ownership). Lets
56 : /// unit tests inject a fake NAM that returns canned replies for the write API.
57 : void setNetworkAccessManager(QNetworkAccessManager *nam);
58 :
59 : // T-79.F1/H8: number of network replies still in flight. The client
60 : // emits allRequestsFinished() when it drops to 0 — callers connect that
61 : // to deleteLater() instead of a fixed-lifetime timer.
62 2 : int pendingReplies() const { return m_pendingReplies; }
63 :
64 : signals:
65 : // T-79.F1/H8: the last outstanding reply finished (or the inactivity
66 : // watchdog gave up). Safe point to deleteLater() the client.
67 : void allRequestsFinished();
68 :
69 : void calendarsDiscovered(const QList<CalendarInfo> &calendars);
70 : void eventsSynced(const QString &calendarPath,
71 : const QList<CalendarEvent> &events);
72 : void tasksSynced(const QString &calendarPath,
73 : const QList<CalendarTask> &tasks);
74 : void syncFailed(const QString &error);
75 :
76 : // T-530 write result signals
77 : void eventSaved(const CalendarEvent &event);
78 : void eventDeleted(const QString &uid);
79 : void taskSaved(const CalendarTask &task);
80 : void taskDeleted(const QString &uid);
81 : void writeFailed(const QString &error);
82 :
83 : private:
84 : friend class TestCalDav;
85 : friend class TestSprint39;
86 : friend class TestSprint79ICal;
87 : friend class TestSprint79Services;
88 :
89 : // T-79.F1/H8: register a dispatched reply for lifetime tracking.
90 : // Call AFTER connecting the reply's business handler, so follow-up
91 : // requests started from result signals are counted before the zero
92 : // check runs.
93 : void trackReply(QNetworkReply *reply);
94 : void onInactivityTimeout();
95 :
96 : void onDiscoverReply(QNetworkReply *reply);
97 : void onSyncEventsReply(QNetworkReply *reply, const QString &calendarPath);
98 : void onSyncTasksReply(QNetworkReply *reply, const QString &calendarPath);
99 :
100 : /// Build the CalDAV resource URL for a new resource path + UID
101 : QString resourceUrl(const QString &calendarPath, const QString &uid) const;
102 : QString resourceUrlForExistingResource(const QString &resourceHref,
103 : const QString &calendarPath,
104 : const QString &uid) const;
105 : QString resolveDavUrl(const QString &href) const;
106 :
107 : /// Parse iCal text into CalendarEvent list
108 : static QList<CalendarEvent> parseICalEvents(const QByteArray &xmlData);
109 : static bool parseICalEvents(const QByteArray &xmlData,
110 : QList<CalendarEvent> *events,
111 : QString *error);
112 :
113 : /// Parse iCal text into CalendarTask list
114 : static QList<CalendarTask> parseICalTasks(const QByteArray &xmlData);
115 : static bool parseICalTasks(const QByteArray &xmlData,
116 : QList<CalendarTask> *tasks,
117 : QString *error);
118 :
119 : QByteArray authHeader() const;
120 :
121 : // Validate the complete RFC entity-tag grammar before setting If-Match.
122 : static bool isValidEtag(const QString &etag);
123 :
124 : QNetworkAccessManager *m_nam = nullptr;
125 : QString m_serverUrl;
126 : QString m_username;
127 : QString m_password;
128 :
129 : // T-79.F1/H8: outstanding-reply tracking + inactivity watchdog.
130 : // The watchdog restarts on every request/response; a stall longer than
131 : // kInactivityTimeoutMs fails the outstanding work (syncFailed +
132 : // writeFailed) and releases the client via allRequestsFinished().
133 : int m_pendingReplies = 0;
134 : QSet<QNetworkReply *> m_trackedReplies;
135 : QTimer *m_inactivityTimer = nullptr;
136 : static constexpr int kInactivityTimeoutMs = 60000;
137 : };
|