From 1fdf45d26dd16ebf8264c842020d514e72982e8a Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Sat, 22 Jan 2011 18:54:05 +0000 Subject: [PATCH] Improved Calendar Support Fixes previous CalDAV support, as MobileMe based calendars now work ok. Additionally, a new 'id' field has been created to provide a persistent and immutable identifier for a ride file (regardless of changes to date/filename). The URL provided in the Calendar config pane can now include '@' symbols (they are translated to %40). The CalDAV URL should be provided for a calendar collection and not for a principal. Examples being (trailing slash is significant): Google : https://www.google.com/calendar/dav/xxxx@gmail.com/events/ MobileMe: https://cal.me.com:8443/calendars/users/x.xxxxxxxxxx/home/ --- src/CalDAV.cpp | 168 ++++++++++++++++++++++++++++++++++++++- src/CalDAV.h | 11 ++- src/DBAccess.cpp | 28 ++++--- src/GcRideFile.cpp | 7 ++ src/JsonRideFile.l | 1 + src/JsonRideFile.y | 30 ++++--- src/MainWindow.cpp | 2 +- src/MetricAggregator.cpp | 1 + src/Pages.cpp | 10 ++- src/RideFile.h | 3 + src/RideMetadata.cpp | 4 + src/SpecialFields.cpp | 1 + src/SummaryMetrics.h | 5 ++ 13 files changed, 239 insertions(+), 32 deletions(-) diff --git a/src/CalDAV.cpp b/src/CalDAV.cpp index 32eba481c..a6c33cbbd 100644 --- a/src/CalDAV.cpp +++ b/src/CalDAV.cpp @@ -39,10 +39,104 @@ CalDAV::download() if (url == "") return false; // not configured QNetworkRequest request = QNetworkRequest(QUrl(url)); + + QByteArray *queryText = new QByteArray( "" + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + "\r\n"); + + request.setRawHeader("Depth", "0"); + request.setRawHeader("Content-Type", "application/xml; charset=\"utf-8\""); + request.setRawHeader("Content-Length", (QString("%1").arg(queryText->size())).toLatin1()); + + QBuffer *query = new QBuffer(queryText); + mode = Events; - QNetworkReply *reply = nam->get(request); + QNetworkReply *reply = nam->sendCustomRequest(request, "REPORT", query); if (reply->error() != QNetworkReply::NoError) { - QMessageBox::warning(main, tr("CalDAV Calendar url error"), reply->errorString()); + QMessageBox::warning(main, tr("CalDAV REPORT url error"), reply->errorString()); + mode = None; + return false; + } + return true; +} + +// +// Get OPTIONS available +// +bool +CalDAV::options() +{ + QString url = appsettings->cvalue(main->cyclist, GC_DVURL, "").toString(); + if (url == "") return false; // not configured + + QNetworkRequest request = QNetworkRequest(QUrl(url)); + + + QByteArray *queryText = new QByteArray("" + "" + " " + ""); + + request.setRawHeader("Depth", "0"); + request.setRawHeader("Content-Type", "text/xml; charset=\"utf-8\""); + request.setRawHeader("Content-Length", (QString("%1").arg(queryText->size())).toLatin1()); + + QBuffer *query = new QBuffer(queryText); + + mode = Options; + QNetworkReply *reply = nam->sendCustomRequest(request, "OPTIONS", query); + if (reply->error() != QNetworkReply::NoError) { + QMessageBox::warning(main, tr("CalDAV OPTIONS url error"), reply->errorString()); + mode = None; + return false; + } + return true; +} + +// +// Get URI Properties via PROPFIND +// +bool +CalDAV::propfind() +{ + QString url = appsettings->cvalue(main->cyclist, GC_DVURL, "").toString(); + if (url == "") return false; // not configured + + QNetworkRequest request = QNetworkRequest(QUrl(url)); + + + QByteArray *queryText = new QByteArray( "" + "" + " " + " " + " " + " " + " " + "\r\n"); + + request.setRawHeader("Content-Type", "text/xml; charset=\"utf-8\""); + request.setRawHeader("Content-Length", (QString("%1").arg(queryText->size())).toLatin1()); + request.setRawHeader("Depth", "0"); + + QBuffer *query = new QBuffer(queryText); + + mode = PropFind; + QNetworkReply *reply = nam->sendCustomRequest(request, "PROPFIND" , query); + if (reply->error() != QNetworkReply::NoError) { + QMessageBox::warning(main, tr("CalDAV OPTIONS url error"), reply->errorString()); mode = None; return false; } @@ -90,9 +184,29 @@ CalDAV::report() static icalcomponent *createEvent(RideItem *rideItem) { + // calendar icalcomponent *root = icalcomponent_new(ICAL_VCALENDAR_COMPONENT); + + // calendar version + icalproperty *version = icalproperty_new_version("2.0"); + icalcomponent_add_property(root, version); + + icalcomponent *event = icalcomponent_new(ICAL_VEVENT_COMPONENT); + // + // Unique ID + // + QString id = rideItem->ride()->id(); + if (id == "") { + id = QUuid::createUuid().toString() + "@" + "goldencheetah.org"; + rideItem->ride()->setId(id); + rideItem->notifyRideMetadataChanged(); + rideItem->setDirty(true); // need to save this! + } + icalproperty *uid = icalproperty_new_uid(id.toLatin1()); + icalcomponent_add_property(event, uid); + // // START DATE // @@ -153,6 +267,50 @@ icalcomponent *createEvent(RideItem *rideItem) return root; } +// extract entries and concatenate +// into a single string. This is from a query response +// where the VEVENTS are embedded within an XML document +static QString extractComponents(QString document) +{ + QString returning = ""; + + // parse the document and extract the multistatus node (there is only one of those) + QDomDocument doc; + if (document == "" || doc.setContent(document) == false) return ""; + QDomNode multistatus = doc.documentElement(); + if (multistatus.isNull()) return ""; + + // Google Calendar retains the namespace prefix in the results + // Apple MobileMe doesn't. This means the element names will + // possibly need a prefix... + QString Dprefix = ""; + QString Cprefix = ""; + if (multistatus.nodeName().startsWith("D:")) { + Dprefix = "D:"; + Cprefix = "C:"; + } + + // read all the responses within the multistatus + for (QDomNode response = multistatus.firstChildElement(Dprefix + "response"); + response.nodeName() == (Dprefix + "response"); response = response.nextSiblingElement(Dprefix + "response")) { + + // skate over the nest of crap to get at the calendar-data + QDomNode propstat = response.firstChildElement(Dprefix + "propstat"); + QDomNode prop = propstat.firstChildElement(Dprefix + "prop"); + QDomNode calendardata = prop.firstChildElement(Cprefix + "calendar-data"); + + // extract the calendar entry - top and tail the other crap + QString text = calendardata.toElement().text(); + int start = text.indexOf("BEGIN:VEVENT"); + int stop = text.indexOf("END:VEVENT"); + + if (start == -1 || stop == -1) continue; + + returning += text.mid(start, stop-start+10) + "\n"; + } + return returning; +} + // // PUT a ride item // @@ -196,13 +354,17 @@ void CalDAV::requestReply(QNetworkReply *reply) { QString response = reply->readAll(); + switch (mode) { case Report: case Events: - main->rideCalendar->refreshRemote(response); + main->rideCalendar->refreshRemote(extractComponents(response)); break; default: + case Options: + case PropFind: case Put: + //nothing at the moment XXX FIXME need some diags / error checking break; } mode = None; diff --git a/src/CalDAV.h b/src/CalDAV.h index ed06339cb..8520520de 100644 --- a/src/CalDAV.h +++ b/src/CalDAV.h @@ -46,12 +46,15 @@ #include "RideFile.h" #include "JsonRideFile.h" +// create a UUID +#include + class CalDAV : public QObject { Q_OBJECT G_OBJECT - enum action { Put, Get, Events, Report, None }; + enum action { Options, PropFind, Put, Get, Events, Report, None }; typedef enum action ActionType; public: @@ -60,6 +63,12 @@ public: public slots: + // Query CalDAV server Options + bool options(); + + // Query CalDAV server Options + bool propfind(); + // authentication (and refresh all events) bool download(); diff --git a/src/DBAccess.cpp b/src/DBAccess.cpp index 964610a53..98f5188e1 100644 --- a/src/DBAccess.cpp +++ b/src/DBAccess.cpp @@ -40,7 +40,7 @@ // DB Schema Version - YOU MUST UPDATE THIS IF THE SCHEMA VERSION CHANGES!!! // Schema version will change if a) the default metadata.xml is updated // or b) new metrics are added / old changed -static int DBSchemaVersion = 23; +static int DBSchemaVersion = 24; DBAccess::DBAccess(MainWindow* main, QDir home) : main(main), home(home) { @@ -145,6 +145,7 @@ bool DBAccess::createMetricsTable() // we need to create it! if (rc && createTables) { QString createMetricTable = "create table metrics (filename varchar primary key," + "identifier varchar," "timestamp integer," "ride_date date," "fingerprint integer"; @@ -374,7 +375,7 @@ bool DBAccess::importRide(SummaryMetrics *summaryMetrics, RideFile *ride, unsign } // construct an insert statement - QString insertStatement = "insert into metrics ( filename, timestamp, ride_date, fingerprint "; + QString insertStatement = "insert into metrics ( filename, identifier, timestamp, ride_date, fingerprint "; const RideMetricFactory &factory = RideMetricFactory::instance(); for (int i=0; irideMetadata()->getFields()) { @@ -406,6 +407,7 @@ bool DBAccess::importRide(SummaryMetrics *summaryMetrics, RideFile *ride, unsign // filename, timestamp, ride date query.addBindValue(summaryMetrics->getFileName()); + query.addBindValue(summaryMetrics->getId()); query.addBindValue(timestamp.toTime_t()); query.addBindValue(summaryMetrics->getRideDate()); query.addBindValue((int)fingerprint); @@ -476,7 +478,7 @@ QList DBAccess::getAllMetricsFor(QDateTime start, QDateTime end) if (end == QDateTime()) end = QDateTime::currentDateTime().addYears(+10); // construct the select statement - QString selectStatement = "SELECT filename, ride_date"; + QString selectStatement = "SELECT filename, identifier, ride_date"; const RideMetricFactory &factory = RideMetricFactory::instance(); for (int i=0; i DBAccess::getAllMetricsFor(QDateTime start, QDateTime end) // filename and date summaryMetrics.setFileName(query.value(0).toString()); - summaryMetrics.setRideDate(query.value(1).toDateTime()); + summaryMetrics.setId(query.value(1).toString()); + summaryMetrics.setRideDate(query.value(2).toDateTime()); // the values int i=0; for (; irideMetadata()->getFields()) { if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) { QString underscored = field.name; - summaryMetrics.setForSymbol(underscored.replace("_"," "), query.value(i+2).toDouble()); + summaryMetrics.setForSymbol(underscored.replace("_"," "), query.value(i+3).toDouble()); i++; } else if (!sp.isMetric(field.name) && field.type < 3) { QString underscored = field.name; // ignore texts for now XXX todo if want metadata from Summary Metrics - summaryMetrics.setText(underscored.replace("_"," "), query.value(i+2).toString()); + summaryMetrics.setText(underscored.replace("_"," "), query.value(i+3).toString()); i++; } } @@ -527,7 +530,7 @@ SummaryMetrics DBAccess::getRideMetrics(QString filename) SummaryMetrics summaryMetrics; // construct the select statement - QString selectStatement = "SELECT filename"; + QString selectStatement = "SELECT filename, identifier"; const RideMetricFactory &factory = RideMetricFactory::instance(); for (int i=0; irideMetadata()->getFields()) { if (!sp.isMetric(field.name) && (field.type == 3 || field.type == 4)) { QString underscored = field.name; - summaryMetrics.setForSymbol(underscored.replace(" ","_"), query.value(i+1).toDouble()); + summaryMetrics.setForSymbol(underscored.replace(" ","_"), query.value(i+2).toDouble()); i++; } else if (!sp.isMetric(field.name) && field.type < 3) { // ignore texts for now XXX todo if want metadata from Summary Metrics QString underscored = field.name; - summaryMetrics.setText(underscored.replace("_"," "), query.value(i+1).toString()); + summaryMetrics.setText(underscored.replace("_"," "), query.value(i+2).toString()); i++; } } diff --git a/src/GcRideFile.cpp b/src/GcRideFile.cpp index e230b84a3..8ce4dd949 100644 --- a/src/GcRideFile.cpp +++ b/src/GcRideFile.cpp @@ -64,6 +64,9 @@ GcFileReader::openRideFile(QFile &file, QStringList &errors) const // now set in localtime rideFile->setStartTime(asUTC.toLocalTime()); } + if (key == "Identifier") { + rideFile->setId(value); + } } // read in metric overrides: @@ -186,6 +189,10 @@ GcFileReader::writeRideFile(const RideFile *ride, QFile &file) const attributes.appendChild(attribute); attribute.setAttribute("key", "Device type"); attribute.setAttribute("value", ride->deviceType()); + attribute = doc.createElement("attribute"); + attributes.appendChild(attribute); + attribute.setAttribute("key", "Identifier"); + attribute.setAttribute("value", ride->id()); // write out in metric overrides: // diff --git a/src/JsonRideFile.l b/src/JsonRideFile.l index 8edbb5ddd..60912e900 100644 --- a/src/JsonRideFile.l +++ b/src/JsonRideFile.l @@ -43,6 +43,7 @@ \"STARTTIME\" return STARTTIME; \"RECINTSECS\" return RECINTSECS; \"DEVICETYPE\" return DEVICETYPE; +\"IDENTIFIER\" return IDENTIFIER; \"OVERRIDES\" return OVERRIDES; \"TAGS\" return TAGS; \"INTERVALS\" return INTERVALS; diff --git a/src/JsonRideFile.y b/src/JsonRideFile.y index 6da304857..292b08c31 100644 --- a/src/JsonRideFile.y +++ b/src/JsonRideFile.y @@ -63,21 +63,22 @@ void JsonRideFileerror(const char *error) // used by parser aka yyerror() // // Escape special characters (JSON compliance) -static QString protect(QString string) +static QString protect(const QString string) { - string.replace("\\", "\\\\"); // backslash - string.replace("\"", "\\\""); // quote - string.replace("\t", "\\t"); // tab - string.replace("\n", "\\n"); // newline - string.replace("\r", "\\r"); // carriage-return - string.replace("\b", "\\b"); // backspace - string.replace("\f", "\\f"); // formfeed - string.replace("/", "\\/"); // solidus - return string; + QString s = string; + s.replace("\\", "\\\\"); // backslash + s.replace("\"", "\\\""); // quote + s.replace("\t", "\\t"); // tab + s.replace("\n", "\\n"); // newline + s.replace("\r", "\\r"); // carriage-return + s.replace("\b", "\\b"); // backspace + s.replace("\f", "\\f"); // formfeed + s.replace("/", "\\/"); // solidus + return s; } // Un-Escape special characters (JSON compliance) -static QString unprotect(QString string) +static QString unprotect(const QString string) { // this is a lexer string so it will be enclosed // in quotes. Lets strip those first @@ -98,7 +99,7 @@ static QString unprotect(QString string) %} %token STRING INTEGER FLOAT -%token RIDE STARTTIME RECINTSECS DEVICETYPE +%token RIDE STARTTIME RECINTSECS DEVICETYPE IDENTIFIER %token OVERRIDES %token TAGS INTERVALS NAME START STOP %token SAMPLES SECS KM WATTS NM CAD KPH HR ALTITUDE LAT LON HEADWIND @@ -124,6 +125,7 @@ rideelement_list: rideelement_list ',' rideelement rideelement: starttime | recordint | devicetype + | identifier | overrides | tags | intervals @@ -140,6 +142,7 @@ starttime: STARTTIME ':' string { } recordint: RECINTSECS ':' number { JsonRide->setRecIntSecs(JsonNumber); } devicetype: DEVICETYPE ':' string { JsonRide->setDeviceType(JsonString); } +identifier: IDENTIFIER ':' string { JsonRide->setId(JsonString); } /* * Metric Overrides @@ -279,7 +282,8 @@ JsonFileReader::writeRideFile(const RideFile *ride, QFile &file) const // first class variables out << "\t\t\"STARTTIME\":\"" << protect(ride->startTime().toUTC().toString(DATETIME_FORMAT)) << "\",\n"; out << "\t\t\"RECINTSECS\":" << ride->recIntSecs() << ",\n"; - out << "\t\t\"DEVICETYPE\":\"" << protect(ride->deviceType()) << "\""; + out << "\t\t\"DEVICETYPE\":\"" << protect(ride->deviceType()) << "\",\n"; + out << "\t\t\"IDENTIFIER\":\"" << protect(ride->id()) << "\""; // // OVERRIDES diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 2340240ba..568f86e98 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -320,7 +320,7 @@ MainWindow::MainWindow(const QDir &home) : #ifdef GC_HAVE_ICAL rideCalendar = new ICalendar(this); // my local/remote calendar entries davCalendar = new CalDAV(this); // remote caldav - davCalendar->download(); // login + davCalendar->download(); // refresh the diary window #endif QTreeWidgetItem *last = NULL; diff --git a/src/MetricAggregator.cpp b/src/MetricAggregator.cpp index 39b0ac788..c009bdb10 100644 --- a/src/MetricAggregator.cpp +++ b/src/MetricAggregator.cpp @@ -175,6 +175,7 @@ bool MetricAggregator::importRide(QDir path, RideFile *ride, QString fileName, u QDateTime dateTime(date, time); summaryMetric->setRideDate(dateTime); + summaryMetric->setId(ride->id()); const RideMetricFactory &factory = RideMetricFactory::instance(); QStringList metrics; diff --git a/src/Pages.cpp b/src/Pages.cpp index 7ee798620..d8443d429 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -517,7 +517,9 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr webcalURL->setText(appsettings->cvalue(mainWindow->cyclist, GC_WEBCAL_URL, "").toString()); dvURL = new QLineEdit(this); - dvURL->setText(appsettings->cvalue(mainWindow->cyclist, GC_DVURL, "https://www.google.com/calendar/dav//events/").toString()); + QString url = appsettings->cvalue(mainWindow->cyclist, GC_DVURL, "").toString(); + url.replace("%40", "@"); // remove escape of @ character + dvURL->setText(url); dvUser = new QLineEdit(this); dvUser->setText(appsettings->cvalue(mainWindow->cyclist, GC_DVUSER, "").toString()); @@ -601,7 +603,11 @@ CredentialsPage::saveClicked() appsettings->setCValue(mainWindow->cyclist, GC_WIUSER, wiUser->text()); appsettings->setCValue(mainWindow->cyclist, GC_WIKEY, wiPass->text()); appsettings->setCValue(mainWindow->cyclist, GC_WEBCAL_URL, webcalURL->text()); - appsettings->setCValue(mainWindow->cyclist, GC_DVURL, dvURL->text()); + + // escape the at character + QString url = dvURL->text(); + url.replace("@", "%40"); + appsettings->setCValue(mainWindow->cyclist, GC_DVURL, url); appsettings->setCValue(mainWindow->cyclist, GC_DVUSER, dvUser->text()); appsettings->setCValue(mainWindow->cyclist, GC_DVPASS, dvPass->text()); saveTwitter(); // get secret key if PIN set diff --git a/src/RideFile.h b/src/RideFile.h index 31305a667..939a21b7f 100644 --- a/src/RideFile.h +++ b/src/RideFile.h @@ -122,6 +122,8 @@ class RideFile : public QObject // QObject to emit signals void setRecIntSecs(double value) { recIntSecs_ = value; } const QString &deviceType() const { return deviceType_; } void setDeviceType(const QString &value) { deviceType_ = value; } + const QString id() const { return id_; } + void setId(const QString &value) { id_ = value; } // Working with INTERVALS const QList &intervals() const { return intervals_; } @@ -179,6 +181,7 @@ class RideFile : public QObject // QObject to emit signals private: + QString id_; // global uuid@goldencheetah.org QDateTime startTime_; // time of day that the ride started double recIntSecs_; // recording interval in seconds QVector dataPoints_; diff --git a/src/RideMetadata.cpp b/src/RideMetadata.cpp index 3886563ce..92f393662 100644 --- a/src/RideMetadata.cpp +++ b/src/RideMetadata.cpp @@ -407,6 +407,9 @@ FormField::editFinished() if (definition.name == "Device") { ourRideItem->ride()->setDeviceType(text); ourRideItem->notifyRideMetadataChanged(); + } else if (definition.name == "Identifier") { + ourRideItem->ride()->setId(text); + ourRideItem->notifyRideMetadataChanged(); } else if (definition.name == "Recording Interval") { ourRideItem->ride()->setRecIntSecs(text.toDouble()); ourRideItem->notifyRideMetadataChanged(); @@ -516,6 +519,7 @@ FormField::metadataChanged() else if (definition.name == "Recording Interval") value = QString("%1").arg(ourRideItem->ride()->recIntSecs()); else if (definition.name == "Start Date") value = ourRideItem->ride()->startTime().date().toString("dd.MM.yyyy"); else if (definition.name == "Start Time") value = ourRideItem->ride()->startTime().time().toString("hh:mm:ss.zzz"); + else if (definition.name == "Identifier") value = ourRideItem->ride()->id(); else { if (sp.isMetric(definition.name)) { // get from metric overrides diff --git a/src/SpecialFields.cpp b/src/SpecialFields.cpp index a8df615c5..39edf0234 100644 --- a/src/SpecialFields.cpp +++ b/src/SpecialFields.cpp @@ -25,6 +25,7 @@ SpecialFields::SpecialFields() { names_ << "Start Date" // linked to RideFile::starttime << "Start Time" // linked to RideFile::starttime + << "Identifier" // linkned to RideFile::id << "Workout Code" // in WKO and possibly others << "Sport" // in WKO and possible others << "Objective" // in WKO as "goal" nad possibly others diff --git a/src/SummaryMetrics.h b/src/SummaryMetrics.h index c239dae44..7c556a023 100644 --- a/src/SummaryMetrics.h +++ b/src/SummaryMetrics.h @@ -31,6 +31,10 @@ class SummaryMetrics QString getFileName() { return fileName; } void setFileName(QString fileName) { this->fileName = fileName; } + // Identifier + QString getId() { return id; } + void setId(QString id) { this->id = id; } + // ride date QDateTime getRideDate() { return rideDate; } void setRideDate(QDateTime rideDate) { this->rideDate = rideDate; } @@ -61,6 +65,7 @@ class SummaryMetrics private: QString fileName; + QString id; QDateTime rideDate; QMap value; QMap texts;