diff --git a/buteo-sync-plugin-carddav.pro b/buteo-sync-plugin-carddav.pro new file mode 100644 index 0000000..6e761ac --- /dev/null +++ b/buteo-sync-plugin-carddav.pro @@ -0,0 +1,3 @@ +TEMPLATE=subdirs +SUBDIRS=src +OTHER_FILES+=rpm/buteo-sync-plugin-carddav.spec diff --git a/rpm/buteo-sync-plugin-carddav.spec b/rpm/buteo-sync-plugin-carddav.spec new file mode 100644 index 0000000..891fe28 --- /dev/null +++ b/rpm/buteo-sync-plugin-carddav.spec @@ -0,0 +1,52 @@ +Name: buteo-sync-plugin-carddav +Summary: Syncs calendar data from CardDAV services +Version: 0.0.1 +Release: 1 +Group: System/Libraries +License: LGPLv2.1 +URL: https://github.com/nemomobile/buteo-sync-plugin-carddav +Source0: %{name}-%{version}.tar.bz2 +BuildRequires: pkgconfig(Qt5Core) +BuildRequires: pkgconfig(Qt5Gui) +BuildRequires: pkgconfig(Qt5DBus) +BuildRequires: pkgconfig(Qt5Sql) +BuildRequires: pkgconfig(Qt5Network) +BuildRequires: pkgconfig(Qt5Contacts) +BuildRequires: pkgconfig(Qt5Versit) +BuildRequires: pkgconfig(mlite5) +BuildRequires: pkgconfig(buteosyncfw5) +BuildRequires: pkgconfig(accounts-qt5) +BuildRequires: pkgconfig(libsignon-qt5) +BuildRequires: pkgconfig(libsailfishkeyprovider) +BuildRequires: pkgconfig(qtcontacts-sqlite-qt5-extensions) +Requires: buteo-syncfw-qt5-msyncd + +%description +A Buteo plugin which syncs contact data from CardDAV services + +%files +%defattr(-,root,root,-) +#out-of-process-plugin +/usr/lib/buteo-plugins-qt5/oopp/carddav-client +#in-process-plugin +#/usr/lib/buteo-plugins-qt5/libcarddav-client.so +%config %{_sysconfdir}/buteo/profiles/client/carddav.xml +%config %{_sysconfdir}/buteo/profiles/sync/carddav.Contacts.xml + +%prep +%setup -q -n %{name}-%{version} + +%build +%qmake5 "DEFINES+=BUTEO_OUT_OF_PROCESS_SUPPORT" +make %{?jobs:-j%jobs} + +%pre +rm -f /home/nemo/.cache/msyncd/sync/client/carddav.xml +rm -f /home/nemo/.cache/msyncd/sync/carddav.Contacts.xml + +%install +rm -rf %{buildroot} +%qmake5_install + +%post +su nemo -c "systemctl --user restart msyncd.service" || : diff --git a/src/auth.cpp b/src/auth.cpp new file mode 100644 index 0000000..a2c62d3 --- /dev/null +++ b/src/auth.cpp @@ -0,0 +1,168 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include "auth_p.h" +#include +#include + +namespace { + QString skp_storedKey(const QString &provider, const QString &service, const QString &key) + { + QString retn; + char *value = NULL; + int success = SailfishKeyProvider_storedKey(provider.toLatin1(), service.toLatin1(), key.toLatin1(), &value); + if (value) { + if (success == 0) { + retn = QString::fromLatin1(value); + } + free(value); + } + return retn; + } +} + +Auth::Auth(QObject *parent) + : QObject(parent) + , m_account(0) + , m_ident(0) + , m_session(0) +{ +} + +Auth::~Auth() +{ + delete m_account; + if (m_ident && m_session) { + m_ident->destroySession(m_session); + } + delete m_ident; +} + +void Auth::signIn(int accountId) +{ + m_account = m_manager.account(accountId); + if (!m_account) { + qWarning() << Q_FUNC_INFO << "unable to load account" << accountId; + emit signInError(); + return; + } + + // determine which service to sign in with. + Accounts::Service srv; + Accounts::ServiceList services = m_account->services(); + Q_FOREACH (const Accounts::Service &s, services) { + if (s.serviceType().toLower() == QStringLiteral("carddav")) { + srv = s; + break; + } + } + + if (!srv.isValid()) { + qWarning() << Q_FUNC_INFO << "unable to find carddav service for account" << accountId; + emit signInError(); + return; + } + + // determine the remote server URL from the account settings, and then sign in. + m_account->selectService(srv); + m_serverUrl = m_account->value("CardDAVServerUrl").toString(); // TODO: use "Remote database" for consistency? + if (m_serverUrl.isEmpty()) { + qWarning() << Q_FUNC_INFO << "no valid server url setting in account" << accountId; + emit signInError(); + return; + } + + m_ident = m_account->credentialsId() > 0 ? SignOn::Identity::existingIdentity(m_account->credentialsId()) : 0; + if (!m_ident) { + qWarning() << Q_FUNC_INFO << "no valid credentials for account" << accountId; + emit signInError(); + return; + } + + Accounts::AccountService accSrv(m_account, srv); + QString method = accSrv.authData().method(); + QString mechanism = accSrv.authData().mechanism(); + SignOn::AuthSession *session = m_ident->createSession(method); + if (!session) { + qWarning() << Q_FUNC_INFO << "unable to create authentication session with account" << accountId; + emit signInError(); + return; + } + + QString providerName = m_account->providerName(); + QString clientId = skp_storedKey(providerName, QString(), QStringLiteral("client_id")); + QString clientSecret = skp_storedKey(providerName, QString(), QStringLiteral("client_secret")); + QString consumerKey = skp_storedKey(providerName, QString(), QStringLiteral("consumer_key")); + QString consumerSecret = skp_storedKey(providerName, QString(), QStringLiteral("consumer_secret")); + + QVariantMap signonSessionData = accSrv.authData().parameters(); + signonSessionData.insert("UiPolicy", SignOn::NoUserInteractionPolicy); + if (!clientId.isEmpty()) signonSessionData.insert("ClientId", clientId); + if (!clientSecret.isEmpty()) signonSessionData.insert("ClientSecret", clientSecret); + if (!consumerKey.isEmpty()) signonSessionData.insert("ConsumerKey", consumerKey); + if (!consumerSecret.isEmpty()) signonSessionData.insert("ConsumerSecret", consumerSecret); + + connect(session, SIGNAL(response(SignOn::SessionData)), + this, SLOT(signOnResponse(SignOn::SessionData)), + Qt::UniqueConnection); + connect(session, SIGNAL(error(SignOn::Error)), + this, SLOT(signOnError(SignOn::Error)), + Qt::UniqueConnection); + + session->setProperty("accountId", accountId); + session->setProperty("mechanism", mechanism); + session->setProperty("signonSessionData", signonSessionData); + session->process(SignOn::SessionData(signonSessionData), mechanism); +} + +void Auth::signOnResponse(const SignOn::SessionData &response) +{ + QString username, password, accessToken; + Q_FOREACH (const QString &key, response.propertyNames()) { + if (key.toLower() == QStringLiteral("username")) { + username = response.getProperty(key).toString(); + } else if (key.toLower() == QStringLiteral("secret")) { + password = response.getProperty(key).toString(); + } else if (key.toLower() == QStringLiteral("password")) { + password = response.getProperty(key).toString(); + } else if (key.toLower() == QStringLiteral("accesstoken")) { + accessToken = response.getProperty(key).toString(); + } + } + + // we need both username+password, OR accessToken. + if (!accessToken.isEmpty()) { + emit signInCompleted(m_serverUrl, QString(), QString(), accessToken); + } else if (!username.isEmpty() && !password.isEmpty()) { + emit signInCompleted(m_serverUrl, username, password, QString()); + } else { + qWarning() << Q_FUNC_INFO << "authentication succeeded, but couldn't find valid credentials"; + emit signInError(); + } +} + +void Auth::signOnError(const SignOn::Error &error) +{ + qWarning() << Q_FUNC_INFO << "authentication error:" << error.type() << ":" << error.message(); + emit signInError(); + return; +} diff --git a/src/auth_p.h b/src/auth_p.h new file mode 100644 index 0000000..359272f --- /dev/null +++ b/src/auth_p.h @@ -0,0 +1,60 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class Auth : public QObject +{ + Q_OBJECT + +public: + Auth(QObject *parent); + ~Auth(); + + void signIn(int accountId); + +Q_SIGNALS: + void signInCompleted(const QString &serverUrl, const QString &username, const QString &password, const QString &accessToken); + void signInError(); + +private Q_SLOTS: + void signOnResponse(const SignOn::SessionData &response); + void signOnError(const SignOn::Error &error); + +private: + Accounts::Manager m_manager; + Accounts::Account *m_account; + SignOn::Identity *m_ident; + SignOn::AuthSession *m_session; + QString m_serverUrl; +}; + + diff --git a/src/carddav.Contacts.xml b/src/carddav.Contacts.xml new file mode 100644 index 0000000..ba8c37f --- /dev/null +++ b/src/carddav.Contacts.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/carddav.cpp b/src/carddav.cpp new file mode 100644 index 0000000..c76309a --- /dev/null +++ b/src/carddav.cpp @@ -0,0 +1,812 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include "carddav_p.h" +#include "syncer_p.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { + void debugDumpData(const QString &data) + { + QString dbgout; + Q_FOREACH (const QChar &c, data) { + if (c == '\r' || c == '\n') { + if (!dbgout.isEmpty()) { + qWarning() << dbgout; + dbgout.clear(); + } + } else { + dbgout += c; + } + } + if (!dbgout.isEmpty()) { + qWarning() << dbgout; + } + } +} + +CardDavVCardConverter::CardDavVCardConverter() +{ +} + +CardDavVCardConverter::~CardDavVCardConverter() +{ +} + +QStringList CardDavVCardConverter::supportedPropertyNames() +{ + // We only support a small number of (core) vCard properties + // in this sync adapter. The rest of the properties will + // be cached so that we can stitch them back into the vCard + // we upload on modification. + QStringList supportedProperties; + supportedProperties << "VERSION" << "PRODID" << "REV" + << "N" << "FN" << "NICKNAME" << "BDAY" << "X-GENDER" + << "EMAIL" << "TEL" << "ADR" << "URL" + << "ORG" << "TITLE" << "ROLE" + << "UID"; + return supportedProperties; +} + +QPair CardDavVCardConverter::convertVCardToContact(const QString &vcard, bool *ok) +{ + m_unsupportedProperties.clear(); + QVersitReader reader(vcard.toUtf8()); + reader.startReading(); + reader.waitForFinished(); + QList vdocs = reader.results(); + if (vdocs.size() != 1) { + qWarning() << Q_FUNC_INFO + << "invalid results during vcard import, got" + << vdocs.size() << "output from input:\n" << vcard; + *ok = false; + return QPair(); + } + + // convert the vCard into a QContact + QVersitContactImporter importer; + importer.setPropertyHandler(this); + importer.importDocuments(vdocs); + QList importedContacts = importer.contacts(); + if (importedContacts.size() != 1) { + qWarning() << Q_FUNC_INFO + << "invalid results during vcard conversion, got" + << importedContacts.size() << "output from input:\n" << vcard; + *ok = false; + return QPair(); + } + + QContact importedContact = importedContacts.first(); + QStringList unsupportedProperties = m_unsupportedProperties.value(importedContact.detail().guid()); + m_unsupportedProperties.clear(); + + *ok = true; + return qMakePair(importedContact, unsupportedProperties); +} + +QString CardDavVCardConverter::convertContactToVCard(const QContact &c, const QStringList &unsupportedProperties) +{ + QList exportList; exportList << c; + QVersitContactExporter e; + e.setDetailHandler(this); + e.exportContacts(exportList); + QByteArray output; + QBuffer vCardBuffer(&output); + vCardBuffer.open(QBuffer::WriteOnly); + QVersitWriter writer(&vCardBuffer); + writer.startWriting(e.documents()); + writer.waitForFinished(); + QString retn = QString::fromUtf8(output); + + // now add back the unsupported properties. + Q_FOREACH (const QString &propStr, unsupportedProperties) { + int endIdx = retn.lastIndexOf(QStringLiteral("END:VCARD")); + if (endIdx > 0) { + QString ecrlf = propStr + '\r' + '\n'; + retn.insert(endIdx, ecrlf); + } + } + +/* + qDebug() << "generated vcard:"; + debugDumpData(retn); +*/ + + return retn; +} + +QString CardDavVCardConverter::convertPropertyToString(const QVersitProperty &p) const +{ + QVersitDocument d(QVersitDocument::VCard30Type); + d.addProperty(p); + QByteArray out; + QBuffer bout(&out); + bout.open(QBuffer::WriteOnly); + QVersitWriter w(&bout); + w.startWriting(d); + w.waitForFinished(); + QString retn = QString::fromLatin1(out); + + // strip out the BEGIN:VCARD\r\nVERSION:3.0\r\n and END:VCARD\r\n\r\n bits. + int headerIdx = retn.indexOf(QStringLiteral("VERSION:3.0")) + 11; + int footerIdx = retn.indexOf(QStringLiteral("END:VCARD")); + if (headerIdx > 11 && footerIdx > 0 && footerIdx > headerIdx) { + retn = retn.mid(headerIdx, footerIdx - headerIdx).trimmed(); + return retn; + } + + qWarning() << Q_FUNC_INFO << "no string conversion possible for versit property:" << p.name(); + return QString(); +} + +void CardDavVCardConverter::propertyProcessed(const QVersitDocument &, const QVersitProperty &property, + const QContact &, bool *alreadyProcessed, + QList *updatedDetails) +{ + static QStringList supportedProperties(supportedPropertyNames()); + const QString propertyName(property.name().toUpper()); + if (supportedProperties.contains(propertyName)) { + // do nothing, let the default handler import them. + *alreadyProcessed = true; + return; + } + + // cache the unsupported property string, and remove any detail + // which was added by the default handler for this property. + *alreadyProcessed = true; + QString unsupportedProperty = convertPropertyToString(property); + m_tempUnsupportedProperties.append(unsupportedProperty); + updatedDetails->clear(); +} + +void CardDavVCardConverter::documentProcessed(const QVersitDocument &, QContact *c) +{ + // the UID of the contact will be contained in the QContactGuid detail. + QString uid = c->detail().guid(); + if (uid.isEmpty()) { + qWarning() << Q_FUNC_INFO << "imported contact has no UID, discarding unsupported properties!"; + } else { + m_unsupportedProperties.insert(uid, m_tempUnsupportedProperties); + } + + // get ready for the next import. + m_tempUnsupportedProperties.clear(); +} + +void CardDavVCardConverter::contactProcessed(const QContact &, QVersitDocument *) +{ +} + +void CardDavVCardConverter::detailProcessed(const QContact &, const QContactDetail &, + const QVersitDocument &, QSet *, + QList *, QList *toBeAdded) +{ + static QStringList supportedProperties(supportedPropertyNames()); + for (int i = toBeAdded->size() - 1; i >= 0; --i) { + if (!supportedProperties.contains(toBeAdded->at(i).name().toUpper())) { + // we don't support importing these properties, so we shouldn't + // attempt to export them. + toBeAdded->removeAt(i); + } + } +} + +CardDav::CardDav(Syncer *parent, + const QString &serverUrl, + const QString &username, + const QString &password) + : QObject(parent) + , q(parent) + , m_converter(new CardDavVCardConverter) + , m_request(new RequestGenerator(q, username, password)) + , m_parser(new ReplyParser(q, m_converter)) + , m_serverUrl(serverUrl) + , m_downsyncRequests(0) + , m_upsyncRequests(0) +{ +} + +CardDav::CardDav(Syncer *parent, + const QString &serverUrl, + const QString &accessToken) + : QObject(parent) + , q(parent) + , m_converter(new CardDavVCardConverter) + , m_request(new RequestGenerator(q, accessToken)) + , m_parser(new ReplyParser(q, m_converter)) + , m_serverUrl(serverUrl) + , m_downsyncRequests(0) + , m_upsyncRequests(0) +{ +} + +CardDav::~CardDav() +{ + delete m_converter; + delete m_parser; + delete m_request; +} + +void CardDav::errorOccurred() +{ + emit error(); +} + +void CardDav::determineRemoteAMR() +{ + // The CardDAV sequence for determining the A/M/R delta is: + // a) fetch user information from the principle URL + // b) fetch addressbooks home url + // c) fetch addressbook information + // d) for each addressbook, either: + // i) perform immediate delta sync (if webdav-sync enabled) OR + // ii) fetch etags, manually calculate delta + // e) fetch full contacts for delta. + + // We start by fetching user information. + fetchUserInformation(); +} + +void CardDav::fetchUserInformation() +{ + qDebug() << Q_FUNC_INFO << "requesting principle urls for user"; + QNetworkReply *reply = m_request->currentUserInformation(m_serverUrl); + if (!reply) { + emit error(); + return; + } + + connect(reply, SIGNAL(finished()), this, SLOT(userInformationResponse())); +} + +void CardDav::userInformationResponse() +{ + QNetworkReply *reply = qobject_cast(sender()); + QByteArray data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << Q_FUNC_INFO << "error:" << reply->error() + << "(" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ")"; + debugDumpData(QString::fromUtf8(data)); + errorOccurred(); + return; + } + + QString userPath = m_parser->parseUserPrinciple(data); + if (userPath.isEmpty()) { + qWarning() << Q_FUNC_INFO << "unable to parse user principle from response"; + emit error(); + return; + } + + fetchAddressbookUrls(userPath); +} + +void CardDav::fetchAddressbookUrls(const QString &userPath) +{ + qDebug() << Q_FUNC_INFO << "requesting addressbook urls for user"; + QNetworkReply *reply = m_request->addressbookUrls(m_serverUrl, userPath); + if (!reply) { + emit error(); + return; + } + + connect(reply, SIGNAL(finished()), this, SLOT(addressbookUrlsResponse())); +} + +void CardDav::addressbookUrlsResponse() +{ + QNetworkReply *reply = qobject_cast(sender()); + QByteArray data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << Q_FUNC_INFO << "error:" << reply->error() + << "(" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ")"; + debugDumpData(QString::fromUtf8(data)); + errorOccurred(); + return; + } + + QString addressbooksHomePath = m_parser->parseAddressbookHome(data); + if (addressbooksHomePath.isEmpty()) { + qWarning() << Q_FUNC_INFO << "unable to parse addressbook home from response"; + emit error(); + return; + } + + fetchAddressbooksInformation(addressbooksHomePath); +} + +void CardDav::fetchAddressbooksInformation(const QString &addressbooksHomePath) +{ + qDebug() << Q_FUNC_INFO << "requesting addressbook sync information"; + QNetworkReply *reply = m_request->addressbooksInformation(m_serverUrl, addressbooksHomePath); + if (!reply) { + emit error(); + return; + } + + connect(reply, SIGNAL(finished()), this, SLOT(addressbooksInformationResponse())); +} + +void CardDav::addressbooksInformationResponse() +{ + QNetworkReply *reply = qobject_cast(sender()); + QByteArray data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << Q_FUNC_INFO << "error:" << reply->error() + << "(" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ")"; + debugDumpData(QString::fromUtf8(data)); + errorOccurred(); + return; + } + + QList infos = m_parser->parseAddressbookInformation(data); + if (infos.isEmpty()) { + qWarning() << Q_FUNC_INFO << "unable to parse addressbook info from response"; + emit error(); + return; + } + + // for addressbooks which support sync-token syncing, use that style. + for (int i = 0; i < infos.size(); ++i) { + // set a default addressbook if we haven't seen one yet. + // we will store newly added local contacts to that addressbook. + if (q->m_defaultAddressbook.isEmpty()) { + q->m_defaultAddressbook = infos[i].url; + } + + if (infos[i].syncToken.isEmpty()) { + // we cannot use sync-token for this addressbook, but instead ctag. + const QString &existingCtag(q->m_addressbookCtags[infos[i].url]); // from OOB + if (existingCtag.isEmpty()) { + // first time sync + q->m_addressbookCtags[infos[i].url] = infos[i].ctag; // insert + // now do etag request, the delta will be all remote additions + fetchContactMetadata(infos[i].url); + } else if (existingCtag != infos[i].ctag) { + // changes have occurred since last sync + q->m_addressbookCtags[infos[i].url] = infos[i].ctag; // update + // perform etag request and then manually calculate deltas. + fetchContactMetadata(infos[i].url); + } else { + // no changes have occurred in this addressbook since last sync + qDebug() << Q_FUNC_INFO << "no changes since last sync for" + << infos[i].url << "from account" << q->m_accountId; + m_downsyncRequests += 1; + QTimer::singleShot(0, this, SLOT(downsyncComplete())); + } + } else { + // the server supports webdav-sync for this addressbook. + const QString &existingSyncToken(q->m_addressbookSyncTokens[infos[i].url]); // from OOB + // store the ctag anyway just in case the server has + // forgotten the syncToken we cached from last time. + if (!infos[i].ctag.isEmpty()) { + q->m_addressbookCtags[infos[i].url] = infos[i].ctag; + } + // attempt to perform synctoken sync + if (existingSyncToken.isEmpty()) { + // first time sync + q->m_addressbookSyncTokens[infos[i].url] = infos[i].syncToken; // insert + // perform slow sync / full report + fetchContactMetadata(infos[i].url); + } else if (existingSyncToken != infos[i].syncToken) { + // changes have occurred since last sync. + q->m_addressbookSyncTokens[infos[i].url] = infos[i].syncToken; // update + // perform immediate delta sync, by passing the old sync token to the server. + fetchImmediateDelta(infos[i].url, existingSyncToken); + } else { + // no changes have occurred in this addressbook since last sync + qDebug() << Q_FUNC_INFO << "no changes since last sync for" + << infos[i].url << "from account" << q->m_accountId; + m_downsyncRequests += 1; + QTimer::singleShot(0, this, SLOT(downsyncComplete())); + } + } + } +} + +void CardDav::fetchImmediateDelta(const QString &addressbookUrl, const QString &syncToken) +{ + qDebug() << Q_FUNC_INFO + << "requesting immediate delta for addressbook" << addressbookUrl + << "with sync token" << syncToken; + + QNetworkReply *reply = m_request->syncTokenDelta(m_serverUrl, addressbookUrl, syncToken); + if (!reply) { + emit error(); + return; + } + + m_downsyncRequests += 1; // when this reaches zero, we've finished all addressbook deltas + reply->setProperty("addressbookUrl", addressbookUrl); + connect(reply, SIGNAL(finished()), this, SLOT(immediateDeltaResponse())); +} + +void CardDav::immediateDeltaResponse() +{ + QNetworkReply *reply = qobject_cast(sender()); + QString addressbookUrl = reply->property("addressbookUrl").toString(); + QByteArray data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << Q_FUNC_INFO << "error:" << reply->error() + << "(" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ")"; + debugDumpData(QString::fromUtf8(data)); + // The server is allowed to forget the syncToken by the + // carddav protocol. Try a full report sync just in case. + fetchContactMetadata(addressbookUrl); + return; + } + + QString newSyncToken; + QList infos = m_parser->parseSyncTokenDelta(data, &newSyncToken); + q->m_addressbookSyncTokens[addressbookUrl] = newSyncToken; + fetchContacts(addressbookUrl, infos); +} + +void CardDav::fetchContactMetadata(const QString &addressbookUrl) +{ + qDebug() << Q_FUNC_INFO << "requesting contact metadata for addressbook" << addressbookUrl; + QNetworkReply *reply = m_request->contactEtags(m_serverUrl, addressbookUrl); + if (!reply) { + emit error(); + return; + } + + m_downsyncRequests += 1; // when this reaches zero, we've finished all addressbook deltas + reply->setProperty("addressbookUrl", addressbookUrl); + connect(reply, SIGNAL(finished()), this, SLOT(contactMetadataResponse())); +} + +void CardDav::contactMetadataResponse() +{ + QNetworkReply *reply = qobject_cast(sender()); + QString addressbookUrl = reply->property("addressbookUrl").toString(); + QByteArray data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << Q_FUNC_INFO << "error:" << reply->error() + << "(" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ")"; + debugDumpData(QString::fromUtf8(data)); + errorOccurred(); + return; + } + + QList infos = m_parser->parseContactMetadata(data, addressbookUrl); + fetchContacts(addressbookUrl, infos); +} + +void CardDav::fetchContacts(const QString &addressbookUrl, const QList &amrInfo) +{ + qDebug() << Q_FUNC_INFO << "requesting full contact information from addressbook" << addressbookUrl; + + // split into A/M/R request sets + QStringList contactUris; + Q_FOREACH (const ReplyParser::ContactInformation &info, amrInfo) { + if (info.modType == ReplyParser::ContactInformation::Addition) { + q->m_serverAdditionIndices[addressbookUrl].insert(info.uri, q->m_serverAdditions[addressbookUrl].size()); + q->m_serverAdditions[addressbookUrl].append(info); + contactUris.append(info.uri); + } else if (info.modType == ReplyParser::ContactInformation::Modification) { + q->m_serverModificationIndices[addressbookUrl].insert(info.uri, q->m_serverModifications[addressbookUrl].size()); + q->m_serverModifications[addressbookUrl].append(info); + contactUris.append(info.uri); + } else if (info.modType == ReplyParser::ContactInformation::Deletion) { + q->m_serverDeletions[addressbookUrl].append(info); + } else { + qWarning() << Q_FUNC_INFO << "no modification type in info for:" << info.uri; + } + } + + qDebug() << Q_FUNC_INFO << "Have calculated AMR:" + << q->m_serverAdditions[addressbookUrl].size() + << q->m_serverModifications[addressbookUrl].size() + << q->m_serverDeletions[addressbookUrl].size() + << "for addressbook:" << addressbookUrl; + + if (contactUris.isEmpty()) { + // no additions or modifications to fetch. + qDebug() << Q_FUNC_INFO << "no further data to fetch"; + contactAddModsComplete(addressbookUrl); + } else { + // fetch the full contact data for additions/modifications. + qDebug() << Q_FUNC_INFO << "fetching vcard data for" << contactUris.size() << "contacts"; + QNetworkReply *reply = m_request->contactMultiget(m_serverUrl, addressbookUrl, contactUris); + if (!reply) { + emit error(); + return; + } + + reply->setProperty("addressbookUrl", addressbookUrl); + connect(reply, SIGNAL(finished()), this, SLOT(contactsResponse())); + } +} + +void CardDav::contactsResponse() +{ + QNetworkReply *reply = qobject_cast(sender()); + QString addressbookUrl = reply->property("addressbookUrl").toString(); + QByteArray data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << Q_FUNC_INFO << "error:" << reply->error() + << "(" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ")"; + debugDumpData(QString::fromUtf8(data)); + errorOccurred(); + return; + } + + QList added; + QList modified; + + // fill out added/modified. Also keep our addressbookContactGuids state up-to-date. + // The addMods map is a map from server contact uri to . + QMap addMods = m_parser->parseContactData(data); + QMap::const_iterator it = addMods.constBegin(); + for ( ; it != addMods.constEnd(); ++it) { + if (q->m_serverAdditionIndices[addressbookUrl].contains(it.key())) { + QString guid = it.value().contact.detail().guid(); + q->m_serverAdditions[addressbookUrl][q->m_serverAdditionIndices[addressbookUrl].value(it.key())].guid = guid; + q->m_contactEtags[guid] = it.value().etag; + q->m_contactUris[guid] = it.key(); + q->m_contactUnsupportedProperties[guid] = it.value().unsupportedProperties; + // Note: for additions, q->m_contactUids will have been filled out by the reply parser. + q->m_addressbookContactGuids[addressbookUrl].append(guid); + added.append(it.value().contact); + } else if (q->m_serverModificationIndices[addressbookUrl].contains(it.key())) { + QContact c = it.value().contact; + QString guid = c.detail().guid(); + q->m_contactUnsupportedProperties[guid] = it.value().unsupportedProperties; + q->m_contactEtags[guid] = it.value().etag; + if (!q->m_contactIds.contains(guid)) { + qWarning() << Q_FUNC_INFO << "modified contact has no id"; + } else { + c.setId(QContactId::fromString(q->m_contactIds[guid])); + } + modified.append(c); + } else { + qWarning() << Q_FUNC_INFO << "ignoring unknown addition/modification:" << it.key(); + } + } + + // coalesce the added/modified contacts from this addressbook into the complete AMR + m_remoteAdditions.append(added); + m_remoteModifications.append(modified); + + // now handle removals + contactAddModsComplete(addressbookUrl); +} + +void CardDav::contactAddModsComplete(const QString &addressbookUrl) +{ + QList removed; + + // fill out removed set, and remove any state data associated with removed contacts + for (int i = 0; i < q->m_serverDeletions[addressbookUrl].size(); ++i) { + QString guid = q->m_serverDeletions[addressbookUrl][i].guid; + + // create the contact to remove + QContact doomed; + QContactGuid cguid; + cguid.setGuid(guid); + doomed.saveDetail(&cguid); + if (!q->m_contactIds.contains(guid)) { + qWarning() << Q_FUNC_INFO << "removed contact has no id"; + continue; // cannot remove it if we don't know the id + } + doomed.setId(QContactId::fromString(q->m_contactIds[guid])); + removed.append(doomed); + + // update the state data + q->m_contactUids.remove(guid); + q->m_contactUris.remove(guid); + q->m_contactEtags.remove(guid); + q->m_contactIds.remove(guid); + q->m_contactUnsupportedProperties.remove(guid); + q->m_addressbookContactGuids[addressbookUrl].removeOne(guid); + } + + // coalesce the removed contacts from this addressbook into the complete AMR + m_remoteRemovals.append(removed); + + // downsync complete for this addressbook. + // we use a singleshot to ensure that the m_deltaRequests count isn't + // decremented synchronously to zero if the first addressbook didn't + // have any remote additions or modifications (requiring async request). + QTimer::singleShot(0, this, SLOT(downsyncComplete())); +} + +void CardDav::downsyncComplete() +{ + // downsync complete for this addressbook + // if this was the last outstanding addressbook, we're finished. + m_downsyncRequests -= 1; + if (m_downsyncRequests == 0) { + qDebug() << Q_FUNC_INFO + << "downsync complete with total AMR:" + << m_remoteAdditions.size() << "," + << m_remoteModifications.size() << "," + << m_remoteRemovals.size(); + emit remoteChanges(m_remoteAdditions, m_remoteModifications, m_remoteRemovals); + } +} + +void CardDav::upsyncUpdates(const QString &addressbookUrl, const QList &added, const QList &modified, const QList &removed) +{ + qDebug() << Q_FUNC_INFO + << "upsyncing updates to addressbook:" << addressbookUrl + << ":" << added.count() << modified.count() << removed.count(); + + if (added.size() == 0 && modified.size() == 0 && removed.size() == 0) { + // nothing to upsync. Use a singleshot to avoid synchronously + // decrementing the m_upsyncRequests count to zero if there + // happens to be nothing to upsync to the first addressbook. + m_upsyncRequests += 1; + QTimer::singleShot(0, this, SLOT(upsyncComplete())); + } else { + // put local additions + for (int i = 0; i < added.size(); ++i) { + QContact c = added.at(i); + // generate a server-side uid + QString uid = QUuid::createUuid().toString().replace(QRegularExpression(QStringLiteral("[\\-{}]")), QString()); + // transform into local-device guid + QString guid = QStringLiteral("%1:%2").arg(q->m_accountId).arg(uid); + // generate a valid uri + QString uri = addressbookUrl + "/" + uid + ".vcf"; + // update our state data + q->m_contactUids[guid] = uid; + q->m_contactUris[guid] = uri; + q->m_contactIds[guid] = c.id().toString(); + // set the uid not guid so that the UID is generated. + QContactGuid cguid = c.detail(); + cguid.setGuid(uid); + c.saveDetail(&cguid); + // generate a vcard + QString vcard = m_converter->convertContactToVCard(c, QStringList()); + // upload + QNetworkReply *reply = m_request->upsyncAddMod(m_serverUrl, uri, QString(), vcard); + if (!reply) { + emit error(); + return; + } + + m_upsyncRequests += 1; + reply->setProperty("addressbookUrl", addressbookUrl); + reply->setProperty("contactGuid", guid); + connect(reply, SIGNAL(finished()), this, SLOT(upsyncResponse())); + } + + // put local modifications + for (int i = 0; i < modified.size(); ++i) { + QContact c = modified.at(i); + // reinstate the server-side UID into the guid detail + QContactGuid cguid = c.detail(); + QString guidstr = c.detail().guid(); + if (guidstr.isEmpty()) { + qWarning() << Q_FUNC_INFO << "modified contact has no guid:" << c.id().toString(); + continue; // TODO: this is actually an error. + } + QString uidstr = q->m_contactUids[guidstr]; + if (uidstr.isEmpty()) { + qWarning() << Q_FUNC_INFO << "modified contact server uid unknown:" << c.id().toString() << guidstr; + continue; // TODO: this is actually an error. + } + cguid.setGuid(uidstr); + c.saveDetail(&cguid); + QString vcard = m_converter->convertContactToVCard(c, q->m_contactUnsupportedProperties[guidstr]); + // upload + QNetworkReply *reply = m_request->upsyncAddMod(m_serverUrl, + q->m_contactUris[guidstr], + q->m_contactEtags[guidstr], + vcard); + if (!reply) { + emit error(); + return; + } + + m_upsyncRequests += 1; + reply->setProperty("addressbookUrl", addressbookUrl); + reply->setProperty("contactGuid", guidstr); + connect(reply, SIGNAL(finished()), this, SLOT(upsyncResponse())); + } + + // delete local removals + for (int i = 0; i < removed.size(); ++i) { + const QString &guidstr(removed[i].detail().guid()); + QNetworkReply *reply = m_request->upsyncDeletion(m_serverUrl, + q->m_contactUris[guidstr], + q->m_contactEtags[guidstr]); + if (!reply) { + emit error(); + return; + } + + // clear state data for this (deleted) contact + q->m_contactEtags.remove(guidstr); + q->m_contactUris.remove(guidstr); + q->m_contactIds.remove(guidstr); + q->m_contactUids.remove(guidstr); + q->m_addressbookContactGuids[addressbookUrl].removeOne(guidstr); + + m_upsyncRequests += 1; + reply->setProperty("addressbookUrl", addressbookUrl); + connect(reply, SIGNAL(finished()), this, SLOT(upsyncResponse())); + } + } +} + +void CardDav::upsyncResponse() +{ + QNetworkReply *reply = qobject_cast(sender()); + QString guid = reply->property("contactGuid").toString(); + QByteArray data = reply->readAll(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << Q_FUNC_INFO << "error:" << reply->error() + << "(" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << ")"; + debugDumpData(QString::fromUtf8(data)); + errorOccurred(); + return; + } + + if (!guid.isEmpty()) { + // this is an addition or modification. + // get the new etag value reported by the server. + QString etag; + Q_FOREACH(const QByteArray &header, reply->rawHeaderList()) { + if (QString::fromUtf8(header).contains(QLatin1String("etag"), Qt::CaseInsensitive)) { + etag = reply->rawHeader(header); + break; + } + } + + if (!etag.isEmpty()) { + q->m_contactEtags[guid] = etag; + } + } + + // upsync is complete for this addressbook. + upsyncComplete(); +} + +void CardDav::upsyncComplete() +{ + m_upsyncRequests -= 1; + if (m_upsyncRequests == 0) { + // finished upsyncing all data for all addressbooks. + qDebug() << Q_FUNC_INFO << "upsync complete"; + emit upsyncCompleted(); + } +} + diff --git a/src/carddav.xml b/src/carddav.xml new file mode 100644 index 0000000..0ab9f17 --- /dev/null +++ b/src/carddav.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/carddav_p.h b/src/carddav_p.h new file mode 100644 index 0000000..1b9b499 --- /dev/null +++ b/src/carddav_p.h @@ -0,0 +1,138 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#ifndef CARDDAV_P_H +#define CARDDAV_P_H + +#include "requestgenerator_p.h" +#include "replyparser_p.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +QTCONTACTS_USE_NAMESPACE +QTVERSIT_USE_NAMESPACE + +class Syncer; +class CardDavVCardConverter; +class CardDav : public QObject +{ + Q_OBJECT + +public: + CardDav(Syncer *parent, + const QString &serverUrl, + const QString &username, + const QString &password); + CardDav(Syncer *parent, + const QString &serverUrl, + const QString &accessToken); + ~CardDav(); + + void determineRemoteAMR(); + void upsyncUpdates(const QString &addressbookUrl, + const QList &added, + const QList &modified, + const QList &removed); + +Q_SIGNALS: + void error(); + void remoteChanges(const QList &added, + const QList &modified, + const QList &removed); + void upsyncCompleted(); + +private: + void fetchUserInformation(); + void fetchAddressbookUrls(const QString &userPath); + void fetchAddressbooksInformation(const QString &addressbooksHomePath); + void fetchImmediateDelta(const QString &addressbookUrl, const QString &syncToken); + void fetchContactMetadata(const QString &addressbookUrl); + void fetchContacts(const QString &addressbookUrl, const QList &amrInfo); + +private Q_SLOTS: + void userInformationResponse(); + void addressbookUrlsResponse(); + void addressbooksInformationResponse(); + void immediateDeltaResponse(); + void contactMetadataResponse(); + void contactsResponse(); + void downsyncComplete(); + void upsyncResponse(); + void upsyncComplete(); + void errorOccurred(); + +private: + void contactAddModsComplete(const QString &addressbookUrl); + Syncer *q; + CardDavVCardConverter *m_converter; + RequestGenerator *m_request; + ReplyParser *m_parser; + QString m_serverUrl; + + QList m_remoteAdditions; + QList m_remoteModifications; + QList m_remoteRemovals; + int m_downsyncRequests; + int m_upsyncRequests; +}; + +class CardDavVCardConverter : public QVersitContactImporterPropertyHandlerV2, + public QVersitContactExporterDetailHandlerV2 +{ +public: + CardDavVCardConverter(); + ~CardDavVCardConverter(); + + // QVersitContactImporterPropertyHandlerV2 + void propertyProcessed(const QVersitDocument &d, const QVersitProperty &property, + const QContact &c, bool *alreadyProcessed, + QList *updatedDetails); + void documentProcessed(const QVersitDocument &d, QContact *c); + + // QVersitContactExporterDetailHandlerV2 + void contactProcessed(const QContact &c, QVersitDocument *d); + void detailProcessed(const QContact &c, const QContactDetail &detail, + const QVersitDocument &d, QSet *processedFields, + QList *toBeRemoved, QList *toBeAdded); + + // API exposed to clients + QPair convertVCardToContact(const QString &vcard, bool *ok); + QString convertContactToVCard(const QContact &c, const QStringList &unsupportedProperties); + +private: + static QStringList supportedPropertyNames(); + QString convertPropertyToString(const QVersitProperty &p) const; + QMap m_unsupportedProperties; // uid -> unsupported properties + QStringList m_tempUnsupportedProperties; +}; + +#endif // CARDDAV_P_H + diff --git a/src/carddavclient.cpp b/src/carddavclient.cpp new file mode 100644 index 0000000..b406d5e --- /dev/null +++ b/src/carddavclient.cpp @@ -0,0 +1,169 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include "carddavclient.h" +#include "syncer_p.h" + +#include +#include +#include +#include + +extern "C" CardDavClient* createPlugin(const QString& aPluginName, + const Buteo::SyncProfile& aProfile, + Buteo::PluginCbInterface *aCbInterface) +{ + return new CardDavClient(aPluginName, aProfile, aCbInterface); +} + +extern "C" void destroyPlugin(CardDavClient *aClient) +{ + delete aClient; +} + +CardDavClient::CardDavClient(const QString& aPluginName, + const Buteo::SyncProfile& aProfile, + Buteo::PluginCbInterface *aCbInterface) + : ClientPlugin(aPluginName, aProfile, aCbInterface) + , m_syncer(0) + , m_accountId(0) +{ + FUNCTION_CALL_TRACE; +} + +CardDavClient::~CardDavClient() +{ + FUNCTION_CALL_TRACE; +} + +void CardDavClient::connectivityStateChanged(Sync::ConnectivityType aType, bool aState) +{ + FUNCTION_CALL_TRACE; + LOG_DEBUG("Received connectivity change event:" << aType << " changed to " << aState); +} + +bool CardDavClient::init() +{ + FUNCTION_CALL_TRACE; + + QString accountIdString = iProfile.key(Buteo::KEY_ACCOUNT_ID); + m_accountId = accountIdString.toInt(); + if (m_accountId == 0) { + LOG_CRITICAL("profile does not specify" << Buteo::KEY_ACCOUNT_ID); + return false; + } + + m_syncDirection = iProfile.syncDirection(); + m_conflictResPolicy = iProfile.conflictResolutionPolicy(); + if (!m_syncer) { + m_syncer = new Syncer(this, &iProfile); + connect(m_syncer, SIGNAL(syncSucceeded()), + this, SLOT(syncSucceeded())); + connect(m_syncer, SIGNAL(syncFailed()), + this, SLOT(syncFailed())); + } + + return true; +} + +bool CardDavClient::uninit() +{ + FUNCTION_CALL_TRACE; + delete m_syncer; + m_syncer = 0; + return true; +} + +bool CardDavClient::startSync() +{ + FUNCTION_CALL_TRACE; + if (m_accountId == 0) return false; + m_syncer->startSync(m_accountId); + return true; +} + +void CardDavClient::syncSucceeded() +{ + syncFinished(Buteo::SyncResults::NO_ERROR, QString()); +} + +void CardDavClient::syncFailed() +{ + syncFinished(Buteo::SyncResults::INTERNAL_ERROR, QString()); +} + +void CardDavClient::abortSync(Sync::SyncStatus aStatus) +{ + FUNCTION_CALL_TRACE; + abort(aStatus); +} + +void CardDavClient::abort(Sync::SyncStatus status) +{ + FUNCTION_CALL_TRACE; + syncFinished(status, QStringLiteral("Sync aborted")); +} + +void CardDavClient::syncFinished(int minorErrorCode, const QString &message) +{ + FUNCTION_CALL_TRACE; + + if (minorErrorCode == Buteo::SyncResults::NO_ERROR) { + LOG_DEBUG("CardDAV sync succeeded!" << message); + m_results = Buteo::SyncResults(QDateTime::currentDateTimeUtc(), + Buteo::SyncResults::SYNC_RESULT_SUCCESS, + Buteo::SyncResults::NO_ERROR); + emit success(getProfileName(), message); + } else { + LOG_CRITICAL("CardDAV sync failed:" << minorErrorCode << message); + m_results = Buteo::SyncResults(iProfile.lastSuccessfulSyncTime(), // don't change the last sync time + Buteo::SyncResults::SYNC_RESULT_FAILED, + minorErrorCode); + emit error(getProfileName(), message, minorErrorCode); + } +} + +Buteo::SyncResults CardDavClient::getSyncResults() const +{ + FUNCTION_CALL_TRACE; + return m_results; +} + +bool CardDavClient::cleanUp() +{ + FUNCTION_CALL_TRACE; + + // This function is called after the account has been deleted. + QString accountIdString = iProfile.key(Buteo::KEY_ACCOUNT_ID); + m_accountId = accountIdString.toInt(); + if (m_accountId == 0) { + LOG_CRITICAL("profile does not specify" << Buteo::KEY_ACCOUNT_ID); + return false; + } + + if (!m_syncer) m_syncer = new Syncer(this, &iProfile); + m_syncer->purgeAccount(m_accountId); + delete m_syncer; + m_syncer = 0; + + return true; +} diff --git a/src/carddavclient.h b/src/carddavclient.h new file mode 100644 index 0000000..c493325 --- /dev/null +++ b/src/carddavclient.h @@ -0,0 +1,91 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#ifndef CARDDAVCLIENT_H +#define CARDDAVCLIENT_H + +#include +#include +#include +#include + +#include +#include + +class Syncer; +class Q_DECL_EXPORT CardDavClient : public Buteo::ClientPlugin +{ + Q_OBJECT + +public: + CardDavClient(const QString &aPluginName, + const Buteo::SyncProfile &aProfile, + Buteo::PluginCbInterface *aCbInterface); + ~CardDavClient(); + + bool init(); + bool uninit(); + bool startSync(); + void abortSync(Sync::SyncStatus aStatus = Sync::SYNC_ABORTED); + Buteo::SyncResults getSyncResults() const; + bool cleanUp(); + +public Q_SLOTS: + void connectivityStateChanged(Sync::ConnectivityType aType, bool aState); + +private Q_SLOTS: + void syncSucceeded(); + void syncFailed(); + +private: + void abort(Sync::SyncStatus aStatus = Sync::SYNC_ABORTED); + void syncFinished(int minorErrorCode, const QString &message); + Buteo::SyncProfile::SyncDirection syncDirection(); + Buteo::SyncProfile::ConflictResolutionPolicy conflictResolutionPolicy(); + + Sync::SyncStatus m_syncStatus; + Buteo::SyncResults m_results; + Buteo::SyncProfile::SyncDirection m_syncDirection; + Buteo::SyncProfile::ConflictResolutionPolicy m_conflictResPolicy; + + Syncer* m_syncer; + int m_accountId; +}; + +/*! \brief Creates CardDav client plugin + * + * @param aPluginName Name of this client plugin + * @param aProfile Profile to use + * @param aCbInterface Pointer to the callback interface + * @return Client plugin on success, otherwise NULL + */ +extern "C" CardDavClient* createPlugin(const QString &aPluginName, + const Buteo::SyncProfile &aProfile, + Buteo::PluginCbInterface *aCbInterface); + +/*! \brief Destroys CardDav client plugin + * + * @param aClient CardDav client plugin instance to destroy + */ +extern "C" void destroyPlugin(CardDavClient *aClient); + +#endif // CARDDAVCLIENT_H diff --git a/src/replyparser.cpp b/src/replyparser.cpp new file mode 100644 index 0000000..56f6481 --- /dev/null +++ b/src/replyparser.cpp @@ -0,0 +1,520 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include "replyparser_p.h" +#include "syncer_p.h" +#include "carddav_p.h" + +#include +#include +#include +#include +#include + +#include + +#include + +namespace { + QVariantMap elementToVMap(QXmlStreamReader &reader) + { + QVariantMap element; + + // store the attributes of the element + QXmlStreamAttributes attrs = reader.attributes(); + while (attrs.size()) { + QXmlStreamAttribute attr = attrs.takeFirst(); + element.insert(attr.name().toString(), attr.value().toString()); + } + + while (reader.readNext() != QXmlStreamReader::EndElement) { + if (reader.isCharacters()) { + // store the text of the element, if any + QString elementText = reader.text().toString(); + if (!elementText.isEmpty()) { + element.insert(QLatin1String("@text"), elementText); + } + } else if (reader.isStartElement()) { + // recurse if necessary. + QString subElementName = reader.name().toString(); + QVariantMap subElement = elementToVMap(reader); + if (element.contains(subElementName)) { + // already have an element with this name. + // create a variantlist and append. + QVariant existing = element.value(subElementName); + QVariantList subElementList; + if (existing.type() == QVariant::Map) { + // we need to convert the value into a QVariantList + subElementList << existing.toMap(); + } else if (existing.type() == QVariant::List) { + subElementList = existing.toList(); + } + subElementList << subElement; + element.insert(subElementName, subElementList); + } else { + // first element with this name. insert as a map. + element.insert(subElementName, subElement); + } + } + } + + return element; + } + + QVariantMap xmlToVMap(QXmlStreamReader &reader) + { + QVariantMap retn; + while (!reader.atEnd() && !reader.hasError() && reader.readNextStartElement()) { + QString elementName = reader.name().toString(); + QVariantMap element = elementToVMap(reader); + retn.insert(elementName, element); + } + return retn; + } +} + +ReplyParser::ReplyParser(Syncer *parent, CardDavVCardConverter *converter) + : q(parent), m_converter(converter) +{ +} + +ReplyParser::~ReplyParser() +{ +} + +QString ReplyParser::parseUserPrinciple(const QByteArray &userInformationResponse) const +{ + /* We expect a response of the form: + HTTP/1.1 207 Multi-status + Content-Type: application/xml; charset=utf-8 + + + + / + + + + /principals/users/johndoe/ + + + HTTP/1.1 200 OK + + + + */ + QXmlStreamReader reader(userInformationResponse); + QString statusText; + QString userPrinciple; + + while (!reader.atEnd() && !reader.hasError()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if (token == QXmlStreamReader::StartElement) { + if (reader.name().toString() == QLatin1String("current-user-principal")) { + if (reader.readNextStartElement() && reader.name().toString() == QLatin1String("href")) { + userPrinciple = reader.readElementText(); + } + } else if (reader.name().toString() == QLatin1String("status")) { + statusText = reader.readElementText(); + } + } + } + + if (!statusText.contains(QLatin1String("200 OK"))) { + qWarning() << Q_FUNC_INFO << "invalid status response to current user information request:" << statusText; + } + + return userPrinciple; +} + +QString ReplyParser::parseAddressbookHome(const QByteArray &addressbookUrlsResponse) const +{ + /* We expect a response of the form: + HTTP/1.1 207 Multi-status + Content-Type: application/xml; charset=utf-8 + + + + / + + + + /addressbooks/johndoe/ + + + HTTP/1.1 200 OK + + + + */ + QXmlStreamReader reader(addressbookUrlsResponse); + QString statusText; + QString addressbookHome; + + while (!reader.atEnd() && !reader.hasError()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if (token == QXmlStreamReader::StartElement) { + if (reader.name().toString() == QLatin1String("addressbook-home-set")) { + if (reader.readNextStartElement() && reader.name().toString() == QLatin1String("href")) { + addressbookHome = reader.readElementText(); + } + } else if (reader.name().toString() == QLatin1String("status")) { + statusText = reader.readElementText(); + } + } + } + + if (!statusText.contains(QLatin1String("200 OK"))) { + qWarning() << Q_FUNC_INFO << "invalid status response to addressbook home request:" << statusText; + } + + return addressbookHome; +} + +QList ReplyParser::parseAddressbookInformation(const QByteArray &addressbookInformationResponse) const +{ + /* We expect a response of the form: + + + /addressbooks/johndoe/contacts/ + + + My Address Book + 3145 + http://sabredav.org/ns/sync-token/3145 + + HTTP/1.1 200 OK + + + + */ + QXmlStreamReader reader(addressbookInformationResponse); + QList infos; + + QVariantMap vmap = xmlToVMap(reader); + QVariantMap multistatusMap = vmap[QLatin1String("multistatus")].toMap(); + QVariantList responses; + if (multistatusMap[QLatin1String("response")].type() == QVariant::List) { + // multiple addressbooks. + responses = multistatusMap[QLatin1String("response")].toList(); + } else { + // only one addressbook. + QVariantMap response = multistatusMap[QLatin1String("response")].toMap(); + responses << response; + } + + Q_FOREACH (const QVariant &rv, responses) { + QVariantMap rmap = rv.toMap(); + ReplyParser::AddressBookInformation currInfo; + currInfo.url = rmap.value("href").toMap().value("@text").toString(); + currInfo.ctag = rmap.value("propstat").toMap().value("prop").toMap().value("getctag").toMap().value("@text").toString(); + currInfo.syncToken = rmap.value("propstat").toMap().value("prop").toMap().value("sync-token").toMap().value("@text").toString(); + currInfo.displayName = rmap.value("propstat").toMap().value("prop").toMap().value("displayname").toMap().value("@text").toString(); + QStringList resourceTypeKeys = rmap.value("propstat").toMap().value("prop").toMap().value("resourcetype").toMap().keys(); + if (!resourceTypeKeys.contains(QStringLiteral("addressbook"), Qt::CaseInsensitive)) { + qDebug() << Q_FUNC_INFO << "ignoring non-addressbook response"; + continue; + } + QString status = rmap.value("propstat").toMap().value("status").toMap().value("@text").toString(); + if (status.contains(QRegularExpression("2[0-9][0-9]"))) { // any HTTP 2xx response + if (currInfo.ctag.isEmpty() && currInfo.syncToken.isEmpty()) { + qDebug() << Q_FUNC_INFO << "ignoring addressbook:" << currInfo.url << "due to lack of ctag"; + } else { + qDebug() << Q_FUNC_INFO << "found valid addressbook:" << currInfo.url; + infos.append(currInfo); + } + } else { + qDebug() << Q_FUNC_INFO << "ignoring addressbook:" << currInfo.url << "due to invalid status:" << status; + } + } + + return infos; +} + +QList ReplyParser::parseSyncTokenDelta(const QByteArray &syncTokenDeltaResponse, QString *newSyncToken) const +{ + /* We expect a response of the form: + + + + /addressbooks/johndoe/contacts/newcard.vcf + + + "33441-34321" + + HTTP/1.1 200 OK + + + + /addressbooks/johndoe/contacts/updatedcard.vcf + + + "33541-34696" + + HTTP/1.1 200 OK + + + + /addressbooks/johndoe/contacts/deletedcard.vcf + HTTP/1.1 404 Not Found + + http://sabredav.org/ns/sync/5001 + + */ + QList info; + QXmlStreamReader reader(syncTokenDeltaResponse); + QVariantMap vmap = xmlToVMap(reader); + QVariantMap multistatusMap = vmap[QLatin1String("multistatus")].toMap(); + if (newSyncToken) { + *newSyncToken = multistatusMap.value("sync-token").toMap().value("@text").toString(); + } + + QVariantList responses; + if (multistatusMap[QLatin1String("response")].type() == QVariant::List) { + // multiple updates in the delta. + responses = multistatusMap[QLatin1String("response")].toList(); + } else { + // only one update in the delta. + QVariantMap response = multistatusMap[QLatin1String("response")].toMap(); + responses << response; + } + + Q_FOREACH (const QVariant &rv, responses) { + QVariantMap rmap = rv.toMap(); + ReplyParser::ContactInformation currInfo; + currInfo.uri = rmap.value("href").toMap().value("@text").toString(); + currInfo.etag = rmap.value("propstat").toMap().value("prop").toMap().value("getetag").toMap().value("@text").toString(); + QMap::const_iterator it = q->m_contactUris.constBegin(); + for ( ; it != q->m_contactUris.constEnd(); ++it) { + if (it.value() == currInfo.uri) { + currInfo.guid = it.key(); + } + } + QString status = rmap.value("propstat").toMap().value("status").toMap().value("@text").toString(); + if (status.contains(QLatin1String("200 OK"))) { + currInfo.modType = currInfo.guid.isEmpty() + ? ReplyParser::ContactInformation::Addition + : ReplyParser::ContactInformation::Modification; + } else if (status.contains(QLatin1String("404 Not Found"))) { + currInfo.modType = ReplyParser::ContactInformation::Deletion; + } else { + qWarning() << Q_FUNC_INFO << "unknown response:" << currInfo.uri << currInfo.etag << status; + } + info.append(currInfo); + } + + return info; +} + +QList ReplyParser::parseContactMetadata(const QByteArray &contactMetadataResponse, const QString &addressbookUrl) const +{ + /* We expect a response of the form: + HTTP/1.1 207 Multi-status + Content-Type: application/xml; charset=utf-8 + + + + /addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf + + + "2134-888" + + HTTP/1.1 200 OK + + + + /addressbooks/johndoe/contacts/acme-12345.vcf + + + "9999-2344"" + + HTTP/1.1 200 OK + + + + */ + QList info; + QXmlStreamReader reader(contactMetadataResponse); + QVariantMap vmap = xmlToVMap(reader); + QVariantMap multistatusMap = vmap[QLatin1String("multistatus")].toMap(); + QVariantList responses; + if (multistatusMap[QLatin1String("response")].type() == QVariant::List) { + // multiple updates in the delta. + responses = multistatusMap[QLatin1String("response")].toList(); + } else { + // only one update in the delta. + QVariantMap response = multistatusMap[QLatin1String("response")].toMap(); + responses << response; + } + + QSet seenUris; + Q_FOREACH (const QVariant &rv, responses) { + QVariantMap rmap = rv.toMap(); + ReplyParser::ContactInformation currInfo; + currInfo.uri = rmap.value("href").toMap().value("@text").toString(); + currInfo.etag = rmap.value("propstat").toMap().value("prop").toMap().value("getetag").toMap().value("@text").toString(); + QMap::const_iterator it = q->m_contactUris.constBegin(); + for ( ; it != q->m_contactUris.constEnd(); ++it) { + if (it.value() == currInfo.uri) { + currInfo.guid = it.key(); + } + } + QString status = rmap.value("propstat").toMap().value("status").toMap().value("@text").toString(); + if (status.contains(QLatin1String("200 OK"))) { + seenUris.insert(currInfo.uri); + currInfo.modType = currInfo.guid.isEmpty() + ? ReplyParser::ContactInformation::Addition + : ReplyParser::ContactInformation::Modification; + if (currInfo.modType == ReplyParser::ContactInformation::Addition + || q->m_contactEtags[currInfo.guid] != currInfo.etag) { + // only append if it's an addition or an actual modification + // the etag will have changed since the last time we saw it, + // if the contact has been modified server-side since last sync. + info.append(currInfo); + } + } else { + qWarning() << Q_FUNC_INFO << "unknown response:" << currInfo.uri << currInfo.etag << status; + } + } + + // we now need to determine deletions. + QStringList contactGuidsInAddressbook = q->m_addressbookContactGuids[addressbookUrl]; + Q_FOREACH (const QString &guid, contactGuidsInAddressbook) { + const QString &uri(q->m_contactUris[guid]); + if (!seenUris.contains(uri)) { + // this uri wasn't listed in the report, so this contact must have been deleted. + ReplyParser::ContactInformation currInfo; + currInfo.etag = q->m_contactEtags[guid]; + currInfo.uri = uri; + currInfo.guid = guid; + currInfo.modType = ReplyParser::ContactInformation::Deletion; + info.append(currInfo); + } + } + + return info; +} + +QMap ReplyParser::parseContactData(const QByteArray &contactData) const +{ + /* We expect a response of the form: + HTTP/1.1 207 Multi-status + Content-Type: application/xml; charset=utf-8 + + + + /addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf + + + "2134-314" + BEGIN:VCARD + VERSION:3.0 + FN:My Mother + UID:abc-def-fez-1234546578 + END:VCARD + + + HTTP/1.1 200 OK + + + + /addressbooks/johndoe/contacts/someapplication-12345678.vcf + + + "5467-323" + BEGIN:VCARD + VERSION:3.0 + FN:Your Mother + UID:foo-bar-zim-gir-1234567 + END:VCARD + + + HTTP/1.1 200 OK + + + + */ + QXmlStreamReader reader(contactData); + QVariantMap vmap = xmlToVMap(reader); + QVariantMap multistatusMap = vmap[QLatin1String("multistatus")].toMap(); + QVariantList responses; + if (multistatusMap[QLatin1String("response")].type() == QVariant::List) { + // multiple updates in the delta. + responses = multistatusMap[QLatin1String("response")].toList(); + } else { + // only one update in the delta. + QVariantMap response = multistatusMap[QLatin1String("response")].toMap(); + responses << response; + } + + QMap uriToContactData; + Q_FOREACH (const QVariant &rv, responses) { + QVariantMap rmap = rv.toMap(); + QString uri = rmap.value("href").toMap().value("@text").toString(); + QString etag = rmap.value("propstat").toMap().value("prop").toMap().value("getetag").toMap().value("@text").toString(); + QString vcard = rmap.value("propstat").toMap().value("prop").toMap().value("address-data").toMap().value("@text").toString(); + + // import the data as a vCard + bool ok = true; + QPair result = m_converter->convertVCardToContact(vcard, &ok); + if (!ok) { + continue; + } + + // fix up various details of the contact. + QContact importedContact = result.first; + QContactGuid guid = importedContact.detail(); + QString uid = guid.guid(); // at this stage it's a UID. + if (uid.isEmpty()) { + qWarning() << Q_FUNC_INFO + << "contact import from vcard has no UID:\n" << vcard; + continue; + } + bool found = false; + QMap::const_iterator it = q->m_contactUids.constBegin(); + for ( ; it != q->m_contactUids.constEnd(); ++it) { + // see if the UID exists in our map already + if (it.value() == uid) { + // it does; use the local-device GUID instead. + guid.setGuid(it.key()); + found = true; + break; + } + } + if (!found) { + // this is a server addition. mutate the uid into a per-account device guid. + guid.setGuid(QStringLiteral("%1:%2").arg(q->m_accountId).arg(uid)); + // also set the guid to uid mapping for the server-side addition. + q->m_contactUids.insert(guid.guid(), uid); + } + importedContact.saveDetail(&guid); + + // and insert into the return map. + ReplyParser::FullContactInformation fci; + fci.contact = importedContact; + fci.unsupportedProperties = result.second; + fci.etag = etag; + uriToContactData.insert(uri, fci); + } + + return uriToContactData; +} + diff --git a/src/replyparser_p.h b/src/replyparser_p.h new file mode 100644 index 0000000..1ded27a --- /dev/null +++ b/src/replyparser_p.h @@ -0,0 +1,86 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#ifndef REPLYPARSER_P_H +#define REPLYPARSER_P_H + +#include +#include +#include +#include + +#include + +QTCONTACTS_USE_NAMESPACE + +class CardDavVCardConverter; +class Syncer; +class ReplyParser +{ +public: + class AddressBookInformation { + public: + QString url; + QString displayName; + QString ctag; + QString syncToken; + }; + + class ContactInformation { + public: + enum ModificationType { + Uninitialized = 0, + Addition, + Modification, + Deletion + }; + ContactInformation() : modType(Uninitialized) {} + ModificationType modType; + QString uri; + QString guid; // this is the prefixed form of the UID (accountNumber:UID) + QString etag; + }; + + class FullContactInformation { + public: + QContact contact; + QStringList unsupportedProperties; + QString etag; + }; + + ReplyParser(Syncer *parent, CardDavVCardConverter *converter); + ~ReplyParser(); + + QString parseUserPrinciple(const QByteArray &userInformationResponse) const; + QString parseAddressbookHome(const QByteArray &addressbookUrlsResponse) const; + QList parseAddressbookInformation(const QByteArray &addressbookInformationResponse) const; + QList parseSyncTokenDelta(const QByteArray &syncTokenDeltaResponse, QString *newSyncToken) const; + QList parseContactMetadata(const QByteArray &contactMetadataResponse, const QString &addresbookUrl) const; + QMap parseContactData(const QByteArray &contactData) const; + +private: + Syncer *q; + mutable CardDavVCardConverter *m_converter; +}; + +#endif // REPLYPARSER_P_H + diff --git a/src/requestgenerator.cpp b/src/requestgenerator.cpp new file mode 100644 index 0000000..4e7b8eb --- /dev/null +++ b/src/requestgenerator.cpp @@ -0,0 +1,385 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include "requestgenerator_p.h" +#include "syncer_p.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +RequestGenerator::RequestGenerator(Syncer *parent, + const QString &username, + const QString &password) + : q(parent) + , m_username(username) + , m_password(password) +{ +} + +RequestGenerator::RequestGenerator(Syncer *parent, + const QString &accessToken) + : q(parent) + , m_accessToken(accessToken) +{ +} + +QNetworkReply *RequestGenerator::generateRequest(const QString &url, + const QString &path, + const QString &depth, + const QString &requestType, + const QString &request) const +{ + QByteArray requestData(request.toUtf8()); + QUrl reqUrl = url.endsWith(path) + ? QUrl(url) + : QUrl(QStringLiteral("%1/%2").arg(url).arg(path)); + if (!m_username.isEmpty() && !m_password.isEmpty()) { + reqUrl.setUserName(m_username); + reqUrl.setPassword(m_password); + } + + QNetworkRequest req(reqUrl); + req.setHeader(QNetworkRequest::ContentTypeHeader, + "application/xml; charset=utf-8"); + req.setHeader(QNetworkRequest::ContentLengthHeader, + requestData.length()); + if (!depth.isEmpty()) { + req.setRawHeader("Depth", depth.toUtf8()); + } + if (!m_accessToken.isEmpty()) { + req.setRawHeader("Authorization", + QString(QLatin1String("Bearer ") + + m_accessToken).toUtf8()); + } + + QBuffer *requestDataBuffer = new QBuffer(q); + requestDataBuffer->setData(requestData); +/* qWarning() << "generateRequest():" << m_accessToken << url << path << depth << requestType << QString::fromUtf8(requestData); */ + return q->m_qnam.sendCustomRequest(req, requestType.toLatin1(), requestDataBuffer); +} + +QNetworkReply *RequestGenerator::generateUpsyncRequest(const QString &url, + const QString &path, + const QString &ifMatch, + const QString &contentType, + const QString &requestType, + const QString &request) const +{ + QByteArray requestData(request.toUtf8()); + QUrl reqUrl = url.endsWith(path) + ? QUrl(url) + : QUrl(QStringLiteral("%1/%2").arg(url).arg(path)); + if (!m_username.isEmpty() && !m_password.isEmpty()) { + reqUrl.setUserName(m_username); + reqUrl.setPassword(m_password); + } + + QNetworkRequest req(reqUrl); + if (!contentType.isEmpty()) { + req.setHeader(QNetworkRequest::ContentTypeHeader, + contentType); + } + if (!request.isEmpty()) { + req.setHeader(QNetworkRequest::ContentLengthHeader, + requestData.length()); + } + if (!ifMatch.isEmpty()) { + req.setRawHeader("If-Match", ifMatch.toUtf8()); + } + if (!m_accessToken.isEmpty()) { + req.setRawHeader("Authorization", + QString(QLatin1String("Bearer ") + + m_accessToken).toUtf8()); + } + + if (!request.isEmpty()) { + QBuffer *requestDataBuffer = new QBuffer(q); + requestDataBuffer->setData(requestData); + return q->m_qnam.sendCustomRequest(req, requestType.toLatin1(), requestDataBuffer); + } + + return q->m_qnam.sendCustomRequest(req, requestType.toLatin1()); +} + +QNetworkReply *RequestGenerator::currentUserInformation(const QString &serverUrl) +{ + if (Q_UNLIKELY(serverUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "server url empty, aborting"; + return 0; + } + + QString requestStr = QStringLiteral( + "" + "" + "" + "" + ""); + + return generateRequest(serverUrl, QString(), QLatin1String("0"), QLatin1String("PROPFIND"), requestStr); +} + +QNetworkReply *RequestGenerator::addressbookUrls(const QString &serverUrl, const QString &userPath) +{ + if (Q_UNLIKELY(userPath.isEmpty())) { + qWarning() << Q_FUNC_INFO << "user path empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(serverUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "server url empty, aborting"; + return 0; + } + + QString requestStr = QStringLiteral( + "" + "" + "" + "" + ""); + + return generateRequest(serverUrl, userPath, QLatin1String("0"), QLatin1String("PROPFIND"), requestStr); +} + +QNetworkReply *RequestGenerator::addressbooksInformation(const QString &serverUrl, const QString &userAddressbooksPath) +{ + if (Q_UNLIKELY(userAddressbooksPath.isEmpty())) { + qWarning() << Q_FUNC_INFO << "addressbooks path empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(serverUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "server url empty, aborting"; + return 0; + } + + QString requestStr = QStringLiteral( + "" + "" + "" + "" + "" + "" + ""); + + return generateRequest(serverUrl, userAddressbooksPath, QLatin1String("1"), QLatin1String("PROPFIND"), requestStr); +} + +QNetworkReply *RequestGenerator::addressbookInformation(const QString &serverUrl, const QString &addressbookPath) +{ + if (Q_UNLIKELY(addressbookPath.isEmpty())) { + qWarning() << Q_FUNC_INFO << "addressbook path empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(serverUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "server url empty, aborting"; + return 0; + } + + QString requestStr = QStringLiteral( + "" + "" + "" + "" + "" + ""); + + return generateRequest(serverUrl, addressbookPath, QLatin1String("0"), QLatin1String("PROPFIND"), requestStr); +} + +QNetworkReply *RequestGenerator::syncTokenDelta(const QString &serverUrl, const QString &addressbookUrl, const QString &syncToken) +{ + if (Q_UNLIKELY(syncToken.isEmpty())) { + qWarning() << Q_FUNC_INFO << "sync token empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(addressbookUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "addressbook url empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(serverUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "server url empty, aborting"; + return 0; + } + + QString requestStr = QStringLiteral( + "" + "" + "%1" + "1" + "" + "" + "" + "").arg(syncToken.toHtmlEscaped()); + + return generateRequest(serverUrl, addressbookUrl, QString(), QLatin1String("REPORT"), requestStr); +} + +QNetworkReply *RequestGenerator::contactEtags(const QString &serverUrl, const QString &addressbookPath) +{ + if (Q_UNLIKELY(addressbookPath.isEmpty())) { + qWarning() << Q_FUNC_INFO << "addressbook path empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(serverUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "server url empty, aborting"; + return 0; + } + + QString requestStr = QStringLiteral( + "" + "" + "" + "" + ""); + + return generateRequest(serverUrl, addressbookPath, QLatin1String("1"), QLatin1String("PROPFIND"), requestStr); +} + +QNetworkReply *RequestGenerator::contactData(const QString &serverUrl, const QString &addressbookPath, const QStringList &contactEtags) +{ + if (Q_UNLIKELY(contactEtags.isEmpty())) { + qWarning() << Q_FUNC_INFO << "etag list empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(addressbookPath.isEmpty())) { + qWarning() << Q_FUNC_INFO << "addressbook path empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(serverUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "server url empty, aborting"; + return 0; + } + + // Note: this may not work with all cardDav servers, since according to the RFC: + // "The filter component is not optional, but required." Thus, may need to use the + // PROPFIND query to get etags, then perform a filter with those etags. + Q_UNUSED(contactEtags); // TODO + QString requestStr = QStringLiteral( + "" + "" + "" + "" + "" + ""); + + return generateRequest(serverUrl, addressbookPath, QLatin1String("1"), QLatin1String("REPORT"), requestStr); +} + +QNetworkReply *RequestGenerator::contactMultiget(const QString &serverUrl, const QString &addressbookPath, const QStringList &contactUris) +{ + if (Q_UNLIKELY(contactUris.isEmpty())) { + qWarning() << Q_FUNC_INFO << "etag list empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(addressbookPath.isEmpty())) { + qWarning() << Q_FUNC_INFO << "addressbook path empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(serverUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "server url empty, aborting"; + return 0; + } + + QString uriHrefs; + Q_FOREACH (const QString &uri, contactUris) { + // note: uriHref is of form: /addressbooks/johndoe/contacts/acme-12345.vcf etc. + if (uri.endsWith(QStringLiteral(".vcf")) && uri.startsWith(addressbookPath)) { + uriHrefs.append(QStringLiteral("%1").arg(uri.toHtmlEscaped())); + } else { + uriHrefs.append(QStringLiteral("%1/%2.vcf").arg(addressbookPath).arg(uri.toHtmlEscaped())); + } + } + + QString requestStr = QStringLiteral( + "" + "" + "" + "" + "" + "%1" + "").arg(uriHrefs); + + return generateRequest(serverUrl, addressbookPath, QLatin1String("1"), QLatin1String("REPORT"), requestStr); +} + +QNetworkReply *RequestGenerator::upsyncAddMod(const QString &serverUrl, const QString &contactPath, const QString &etag, const QString &vcard) +{ + if (Q_UNLIKELY(vcard.isEmpty())) { + qWarning() << Q_FUNC_INFO << "vcard empty, aborting"; + return 0; + } + + // the etag can be empty if it's an addition + + if (Q_UNLIKELY(contactPath.isEmpty())) { + qWarning() << Q_FUNC_INFO << "contact uri empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(serverUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "server url empty, aborting"; + return 0; + } + + return generateUpsyncRequest(serverUrl, contactPath, etag, + QStringLiteral("text/vcard; charset=utf-8"), + QStringLiteral("PUT"), vcard); +} + +QNetworkReply *RequestGenerator::upsyncDeletion(const QString &serverUrl, const QString &contactPath, const QString &etag) +{ + if (Q_UNLIKELY(etag.isEmpty())) { + qWarning() << Q_FUNC_INFO << "etag empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(contactPath.isEmpty())) { + qWarning() << Q_FUNC_INFO << "contact uri empty, aborting"; + return 0; + } + + if (Q_UNLIKELY(serverUrl.isEmpty())) { + qWarning() << Q_FUNC_INFO << "server url empty, aborting"; + return 0; + } + + return generateUpsyncRequest(serverUrl, contactPath, etag, QString(), + QStringLiteral("DELETE"), QString()); +} diff --git a/src/requestgenerator_p.h b/src/requestgenerator_p.h new file mode 100644 index 0000000..6b61e72 --- /dev/null +++ b/src/requestgenerator_p.h @@ -0,0 +1,71 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#ifndef REQUESTGENERATOR_P_H +#define REQUESTGENERATOR_P_H + +#include +#include +#include +#include + +#include + +QTCONTACTS_USE_NAMESPACE + +class Syncer; +class RequestGenerator +{ +public: + RequestGenerator(Syncer *parent, const QString &username, const QString &password); + RequestGenerator(Syncer *parent, const QString &accessToken); + + QNetworkReply *currentUserInformation(const QString &serverUrl); + QNetworkReply *addressbookUrls(const QString &serverUrl, const QString &userPath); + QNetworkReply *addressbooksInformation(const QString &serverUrl, const QString &userAddressbooksPath); + QNetworkReply *addressbookInformation(const QString &serverUrl, const QString &addressbookPath); + QNetworkReply *syncTokenDelta(const QString &serverUrl, const QString &addressbookUrl, const QString &syncToken); + QNetworkReply *contactEtags(const QString &serverUrl, const QString &addressbookPath); + QNetworkReply *contactData(const QString &serverUrl, const QString &addressbookPath, const QStringList &contactEtags); + QNetworkReply *contactMultiget(const QString &serverUrl, const QString &addressbookPath, const QStringList &contactUris); + QNetworkReply *upsyncAddMod(const QString &serverUrl, const QString &contactPath, const QString &etag, const QString &vcard); + QNetworkReply *upsyncDeletion(const QString &serverUrl, const QString &contactPath, const QString &etag); + +private: + QNetworkReply *generateRequest(const QString &url, + const QString &path, + const QString &depth, + const QString &requestType, + const QString &request) const; + QNetworkReply *generateUpsyncRequest(const QString &url, + const QString &path, + const QString &ifMatch, + const QString &contentType, + const QString &requestType, + const QString &request) const; + Syncer *q; + QString m_username; + QString m_password; + QString m_accessToken; +}; + +#endif // REQUESTGENERATOR_P_H diff --git a/src/src.pro b/src/src.pro new file mode 100644 index 0000000..dc5106f --- /dev/null +++ b/src/src.pro @@ -0,0 +1,66 @@ +TARGET = carddav-client + +QT -= gui +QT += network dbus + +CONFIG += link_pkgconfig console +PKGCONFIG += buteosyncfw5 libsignon-qt5 accounts-qt5 libsailfishkeyprovider +PKGCONFIG += Qt5Versit Qt5Contacts qtcontacts-sqlite-qt5-extensions +QT += contacts-private + +QMAKE_CXXFLAGS = -Wall \ + -g \ + -Wno-cast-align \ + -O2 -finline-functions + +SOURCES += \ + carddavclient.cpp \ + syncer.cpp \ + auth.cpp \ + carddav.cpp \ + requestgenerator.cpp \ + replyparser.cpp + +HEADERS += \ + carddavclient.h \ + syncer_p.h \ + auth_p.h \ + carddav_p.h \ + requestgenerator_p.h \ + replyparser_p.h + +OTHER_FILES += \ + carddav.xml \ + carddav.Contacts.xml + +!contains (DEFINES, BUTEO_OUT_OF_PROCESS_SUPPORT) { + TEMPLATE = lib + CONFIG += plugin + target.path = /usr/lib/buteo-plugins-qt5 +} + +contains (DEFINES, BUTEO_OUT_OF_PROCESS_SUPPORT) { + TEMPLATE = app + target.path = /usr/lib/buteo-plugins-qt5/oopp + DEFINES += CLIENT_PLUGIN + DEFINES += "CLASSNAME=CardDavClient" + DEFINES += CLASSNAME_H=\\\"carddavclient.h\\\" + INCLUDE_DIR = $$system(pkg-config --cflags buteosyncfw5|cut -f2 -d'I') + + HEADERS += $$INCLUDE_DIR/ButeoPluginIfaceAdaptor.h \ + $$INCLUDE_DIR/PluginCbImpl.h \ + $$INCLUDE_DIR/PluginServiceObj.h + + SOURCES += $$INCLUDE_DIR/ButeoPluginIfaceAdaptor.cpp \ + $$INCLUDE_DIR/PluginCbImpl.cpp \ + $$INCLUDE_DIR/PluginServiceObj.cpp \ + $$INCLUDE_DIR/plugin_main.cpp +} + +sync.path = /etc/buteo/profiles/sync +sync.files = carddav.Contacts.xml + +client.path = /etc/buteo/profiles/client +client.files = carddav.xml + +INSTALLS += target sync client diff --git a/src/syncer.cpp b/src/syncer.cpp new file mode 100644 index 0000000..7b689de --- /dev/null +++ b/src/syncer.cpp @@ -0,0 +1,553 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include "syncer_p.h" +#include "carddav_p.h" +#include "auth_p.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define CARDDAV_CONTACTS_SYNCTARGET QLatin1String("carddav") + +Syncer::Syncer(QObject *parent, Buteo::SyncProfile *syncProfile) + : QObject(parent), QtContactsSqliteExtensions::TwoWayContactSyncAdapter(CARDDAV_CONTACTS_SYNCTARGET) + , m_syncProfile(syncProfile) + , m_cardDav(0) + , m_auth(0) +{ +} + +Syncer::~Syncer() +{ + delete m_auth; + delete m_cardDav; +} + +bool Syncer::testAccountProvenance(const QContact &contact, const QString &accountId) +{ + return contact.detail().guid().startsWith(QStringLiteral("%1:").arg(accountId)); +} + +void Syncer::startSync(int accountId) +{ + Q_ASSERT(accountId != 0); + m_accountId = accountId; + m_auth = new Auth(this); + connect(m_auth, SIGNAL(signInCompleted(QString,QString,QString,QString)), + this, SLOT(sync(QString,QString,QString,QString))); + connect(m_auth, SIGNAL(signInError()), + this, SLOT(signInError())); + qDebug() << Q_FUNC_INFO << "starting carddav sync with account" << m_accountId; + m_auth->signIn(accountId); +} + +void Syncer::signInError() +{ + emit syncFailed(); +} + +void Syncer::sync(const QString &serverUrl, const QString &username, const QString &password, const QString &accessToken) +{ + m_serverUrl = serverUrl; + m_username = username; + m_password = password; + m_accessToken = accessToken; + + QDateTime remoteSince; + if (!initSyncAdapter(QString::number(m_accountId)) + || !readSyncStateData(&remoteSince, QString::number(m_accountId)) + || !readExtraStateData(m_accountId)) { + qWarning() << Q_FUNC_INFO << "unable to init carddav sync for account" << m_accountId; + cardDavError(); + return; + } + + determineRemoteChanges(remoteSince, QString::number(m_accountId)); +} + +void Syncer::determineRemoteChanges(const QDateTime &, const QString &) +{ + m_cardDav = m_username.isEmpty() + ? new CardDav(this, m_serverUrl, m_accessToken) + : new CardDav(this, m_serverUrl, m_username, m_password); + connect(m_cardDav, SIGNAL(remoteChanges(QList,QList,QList)), + this, SLOT(continueSync(QList,QList,QList))); + connect(m_cardDav, SIGNAL(upsyncCompleted()), + this, SLOT(syncFinished())); + connect(m_cardDav, SIGNAL(error()), + this, SLOT(cardDavError())); + m_cardDav->determineRemoteAMR(); +} + +void Syncer::cardDavError() +{ + purgeSyncStateData(QString::number(m_accountId)); + emit syncFailed(); +} + +void Syncer::continueSync(const QList &added, const QList &modified, const QList &removed) +{ + // store the remote changes locally + QList addMod = added+modified, del = removed; + qDebug() << Q_FUNC_INFO << "storing remote changes to local device: AMR:" + << added.count() << modified.count() << removed.count() + << "for account:" << m_accountId; + if (!storeRemoteChanges(del, &addMod, QString::number(m_accountId))) { + qWarning() << Q_FUNC_INFO << "unable to store remote changes for account" << m_accountId; + cardDavError(); + return; + } + + // now update our id mapping in case anything changed. + // this is necessary especially for added contacts, which previously had no id. + Q_FOREACH (const QContact &c, addMod) { + if (c.id().isNull()) { + qWarning() << Q_FUNC_INFO << "no contact id specified for contact with guid" + << c.detail().guid() << "from account" << m_accountId; + cardDavError(); + return; + } else { + m_contactIds.insert(c.detail().guid(), c.id().toString()); + } + } + + // continue with the upsync half of the sync process. + QDateTime localSince; + QList locallyAdded, locallyModified, locallyDeleted; + if (!determineLocalChanges(&localSince, &locallyAdded, &locallyModified, &locallyDeleted, QString::number(m_accountId))) { + qWarning() << Q_FUNC_INFO << "unable to determine local changes for account" << m_accountId; + cardDavError(); + return; + } + + if (!m_syncProfile || m_syncProfile->syncDirection() != Buteo::SyncProfile::SYNC_DIRECTION_FROM_REMOTE) { + upsyncLocalChanges(localSince, locallyAdded, locallyModified, locallyDeleted, QString::number(m_accountId)); + } else { + qDebug() << Q_FUNC_INFO << "skipping upsync due to sync profile direction setting"; + syncFinished(); + } +} + +void Syncer::upsyncLocalChanges(const QDateTime &, + const QList &locallyAdded, + const QList &locallyModified, + const QList &locallyDeleted, + const QString &) +{ + qDebug() << Q_FUNC_INFO << "upsyncing local changes to remove server: AMR:" + << locallyAdded.count() << locallyModified.count() << locallyDeleted.count() + << "for account:" << m_accountId; + + // segment the changes according to the addressbook the contacts are from + QSet modifiedAddressbookUrls; + QMap > added; + QMap > modified; + QMap > deleted; + + QString addedContactsAddressbook = m_defaultAddressbook; + if (addedContactsAddressbook.isEmpty()) { + addedContactsAddressbook = m_addressbookCtags.keys().size() + ? m_addressbookCtags.keys().first() + : QString(); + } + if (addedContactsAddressbook.isEmpty()) { + addedContactsAddressbook = m_addressbookSyncTokens.keys().size() + ? m_addressbookSyncTokens.keys().first() + : QString(); + } + if (addedContactsAddressbook.isEmpty()) { + qWarning() << Q_FUNC_INFO << "no known addressbooks, failing"; + cardDavError(); + return; + } + + Q_FOREACH (const QContact &a, locallyAdded) { + added[addedContactsAddressbook].append(a); + modifiedAddressbookUrls.insert(addedContactsAddressbook); + } + Q_FOREACH (const QContact &m, locallyModified) { + Q_FOREACH (const QString &addressbookUrl, m_addressbookContactGuids.keys()) { + if (m_addressbookContactGuids[addressbookUrl].contains(m.detail().guid())) { + modified[addressbookUrl].append(m); + modifiedAddressbookUrls.insert(addressbookUrl); + } + } + } + Q_FOREACH (const QContact &d, locallyDeleted) { + Q_FOREACH (const QString &addressbookUrl, m_addressbookContactGuids.keys()) { + if (m_addressbookContactGuids[addressbookUrl].contains(d.detail().guid())) { + deleted[addressbookUrl].append(d); + modifiedAddressbookUrls.insert(addressbookUrl); + } + } + } + + // now upsync the changes for each addressbook + if (modifiedAddressbookUrls.size()) { + Q_FOREACH (const QString &addressbookUrl, modifiedAddressbookUrls) { + m_cardDav->upsyncUpdates(addressbookUrl, + added[addressbookUrl], + modified[addressbookUrl], + deleted[addressbookUrl]); + } + } else { + // nothing to upsync. + syncFinished(); + } +} + +void Syncer::syncFinished() +{ + // finished upsync. Just need to store our state data and we're done. + if (!storeExtraStateData(m_accountId) || !storeSyncStateData(QString::number(m_accountId))) { + qWarning() << Q_FUNC_INFO << "unable to finalise sync state"; + cardDavError(); // actually in this case we have already stored stuff to local and server...? + return; + } + + qDebug() << Q_FUNC_INFO << "carddav sync with account" << m_accountId << "finished successfully!"; + + // Success. + emit syncSucceeded(); +} + +void Syncer::purgeAccount(int accountId) +{ + QContactDetailFilter syncTargetFilter; + syncTargetFilter.setDetailType(QContactDetail::TypeSyncTarget, QContactSyncTarget::FieldSyncTarget); + syncTargetFilter.setValue(CARDDAV_CONTACTS_SYNCTARGET); + QContactDetailFilter guidFilter; + guidFilter.setDetailType(QContactDetail::TypeGuid, QContactGuid::FieldGuid); + guidFilter.setValue(QStringLiteral("%1:").arg(accountId)); + guidFilter.setMatchFlags(QContactDetailFilter::MatchStartsWith); + QList contactsToRemove = m_contactManager.contactIds(syncTargetFilter & guidFilter); + + // now write the changes to the database. + bool success = true; + if (contactsToRemove.size()) { + success = m_contactManager.removeContacts(contactsToRemove); + if (!success) { + qWarning() << "Failed to remove stale contacts during purge of account" << accountId + << ":" << m_contactManager.error(); + } + } + + // ensure we remove the OOB data for the account. + // We can't rely on d->m_stateData[QString::number(accountId)].m_oobScope containing the + // correct value, as the purge codepath can be called from cleanUp() on account + // removal, during which no cached state data exists. + // Also, it may be called for an account which was previously removed but for which + // artifacts still remain (eg, if msyncd wasn't running at the time that the account + // was removed, due to a crash, etc) - in which case the cached value would be wrong. + QString oobScope = QStringLiteral("%1-%2").arg(CARDDAV_CONTACTS_SYNCTARGET).arg(accountId); + if (!d->m_engine->removeOOB(oobScope)) { + success = false; + qWarning() << Q_FUNC_INFO << "Error occurred while purging OOB data for removed CardDAV account" << accountId; + } + + if (success) { + qDebug() << Q_FUNC_INFO << "Purged account" << accountId + << "and successfully removed" << contactsToRemove.size() << "contacts"; + } +} + +// this function must be called directly after readSyncStateData() +bool Syncer::readExtraStateData(int accountId) +{ + QMap values; + QStringList keys; + keys << QStringLiteral("addressbookContactGuids") + << QStringLiteral("addressbookCtags") + << QStringLiteral("addressbookSyncTokens") + << QStringLiteral("contactUids") + << QStringLiteral("contactUris") + << QStringLiteral("contactEtags") + << QStringLiteral("contactIds") + << QStringLiteral("contactUnsupportedProperties"); + if (!d->m_engine->fetchOOB(d->m_stateData[QString::number(accountId)].m_oobScope, keys, &values)) { + qWarning() << Q_FUNC_INFO << "failed to read extra data for carddav account" << accountId; + d->clear(QString::number(accountId)); + return false; + } + + // m_addressbookContactGuids + QVariant acgValue = values.value(QStringLiteral("addressbookContactGuids")); + QByteArray acgValueBA = acgValue.toByteArray(); + QJsonObject acgJsonObj = QJsonDocument::fromBinaryData(acgValueBA).object(); + QStringList addressbookUrls = acgJsonObj.keys(); + QMap addressbookUrlToContactGuids; + foreach (const QString &url, addressbookUrls) { + QVariantList contactGuidsVL = acgJsonObj.value(url).toArray().toVariantList(); + QStringList contactGuids; + foreach (const QVariant &v, contactGuidsVL) { + if (!v.toString().isEmpty()) { + contactGuids.append(v.toString()); + } + } + + addressbookUrlToContactGuids.insert(url, contactGuids); + } + m_addressbookContactGuids = addressbookUrlToContactGuids; + + // m_addressbookCtags + QVariant acValue = values.value(QStringLiteral("addressbookCtags")); + QByteArray acValueBA = acValue.toByteArray(); + QJsonObject acJsonObj = QJsonDocument::fromBinaryData(acValueBA).object(); + addressbookUrls = acJsonObj.keys(); + QMap addressbookUrlToCtag; + foreach (const QString &url, addressbookUrls) { + addressbookUrlToCtag.insert(url, acJsonObj.value(url).toString()); + } + m_addressbookCtags = addressbookUrlToCtag; + + // m_addressbookSyncTokens + QVariant asValue = values.value(QStringLiteral("addressbookSyncTokens")); + QByteArray asValueBA = asValue.toByteArray(); + QJsonObject asJsonObj = QJsonDocument::fromBinaryData(asValueBA).object(); + addressbookUrls = asJsonObj.keys(); + QMap addressbookUrlToSyncToken; + foreach (const QString &url, addressbookUrls) { + addressbookUrlToSyncToken.insert(url, asJsonObj.value(url).toString()); + } + m_addressbookSyncTokens = addressbookUrlToSyncToken; + + // m_contactUids + QVariant cuiValue = values.value(QStringLiteral("contactUids")); + QByteArray cuiValueBA = cuiValue.toByteArray(); + QJsonObject cuiJsonObj = QJsonDocument::fromBinaryData(cuiValueBA).object(); + QStringList contactGuids = cuiJsonObj.keys(); + QMap guidToContactUid; + foreach (const QString &guid, contactGuids) { + guidToContactUid.insert(guid, cuiJsonObj.value(guid).toString()); + } + m_contactUids = guidToContactUid; + + // m_contactUris + QVariant cuValue = values.value(QStringLiteral("contactUris")); + QByteArray cuValueBA = cuValue.toByteArray(); + QJsonObject cuJsonObj = QJsonDocument::fromBinaryData(cuValueBA).object(); + contactGuids = cuJsonObj.keys(); + QMap guidToContactUri; + foreach (const QString &guid, contactGuids) { + guidToContactUri.insert(guid, cuJsonObj.value(guid).toString()); + } + m_contactUris = guidToContactUri; + + // m_contactEtags + QVariant ceValue = values.value(QStringLiteral("contactEtags")); + QByteArray ceValueBA = ceValue.toByteArray(); + QJsonObject ceJsonObj = QJsonDocument::fromBinaryData(ceValueBA).object(); + contactGuids = ceJsonObj.keys(); + QMap guidToContactEtag; + foreach (const QString &guid, contactGuids) { + guidToContactEtag.insert(guid, ceJsonObj.value(guid).toString()); + } + m_contactEtags = guidToContactEtag; + + // m_contactIds + QVariant ciValue = values.value(QStringLiteral("contactIds")); + QByteArray ciValueBA = ciValue.toByteArray(); + QJsonObject ciJsonObj = QJsonDocument::fromBinaryData(ciValueBA).object(); + contactGuids = ciJsonObj.keys(); + QMap guidToContactId; + foreach (const QString &guid, contactGuids) { + guidToContactId.insert(guid, ciJsonObj.value(guid).toString()); + } + m_contactIds = guidToContactId; + + // m_contactUnsupportedProperties + QVariant cupValue = values.value(QStringLiteral("contactUnsupportedProperties")); + QByteArray cupValueBA = cupValue.toByteArray(); + QJsonObject cupJsonObj = QJsonDocument::fromBinaryData(cupValueBA).object(); + contactGuids = cupJsonObj.keys(); + QMap contactGuidToUnsupportedProperties; + foreach (const QString &guid, contactGuids) { + QVariantList unsupportedPropertiesVL = cupJsonObj.value(guid).toArray().toVariantList(); + QStringList unsupportedProperties; + foreach (const QVariant &v, unsupportedPropertiesVL) { + if (!v.toString().isEmpty()) { + unsupportedProperties.append(v.toString()); + } + } + + contactGuidToUnsupportedProperties.insert(guid, unsupportedProperties); + } + m_contactUnsupportedProperties = contactGuidToUnsupportedProperties; + + // Finally, if we're doing a "clean sync" we should pre-populate our prevRemote + // list with the current state of the local database. + // This is to avoid clean-syncs causing contact duplication. + if (!d->m_stateData[QString::number(m_accountId)].m_localSince.isValid()) { + QDateTime maxTimestamp; + QList existingContacts; + QContactManager::Error error = QContactManager::NoError; + if (!d->m_engine->fetchSyncContacts(CARDDAV_CONTACTS_SYNCTARGET, + QDateTime(), + QList(), + &existingContacts, + 0, + 0, + &maxTimestamp, + &error)) { + qWarning() << Q_FUNC_INFO << "failed to fetch pre-existing contacts for account" << m_accountId; + d->clear(QString::number(accountId)); + return false; + } + + // filter out any which don't come from this account. + QList prevRemote; + QList exportedIds; + foreach (const QContact &c, existingContacts) { + if (c.detail().guid().startsWith(QStringLiteral("%1:").arg(accountId))) { + prevRemote.append(c); + exportedIds.append(c.id()); + m_contactIds.insert(c.detail().guid(), c.id().toString()); + } + } + + // set our state data. + d->m_stateData[QString::number(accountId)].m_prevRemote = prevRemote; + d->m_stateData[QString::number(accountId)].m_exportedIds = exportedIds; + } + + // done. + return true; +} + +// this function must be called directly before storeSyncStateData() +bool Syncer::storeExtraStateData(int accountId) +{ + // m_addressbookContactGuids + QJsonObject acgJsonObj; + for (QMap::const_iterator it = m_addressbookContactGuids.constBegin(); + it != m_addressbookContactGuids.constEnd(); ++it) { + acgJsonObj.insert(it.key(), QJsonValue(QJsonArray::fromStringList(it.value()))); + } + QJsonDocument acgJsonDoc(acgJsonObj); + QVariant acgValue(acgJsonDoc.toBinaryData()); + + // m_addressbookCtags + QJsonObject acJsonObj; + for (QMap::const_iterator it = m_addressbookCtags.constBegin(); + it != m_addressbookCtags.constEnd(); ++it) { + acJsonObj.insert(it.key(), QJsonValue(it.value())); + } + QJsonDocument acJsonDoc(acJsonObj); + QVariant acValue(acJsonDoc.toBinaryData()); + + // m_addressbookSyncTokens + QJsonObject asJsonObj; + for (QMap::const_iterator it = m_addressbookSyncTokens.constBegin(); + it != m_addressbookSyncTokens.constEnd(); ++it) { + asJsonObj.insert(it.key(), QJsonValue(it.value())); + } + QJsonDocument asJsonDoc(asJsonObj); + QVariant asValue(asJsonDoc.toBinaryData()); + + // m_contactUids + QJsonObject cuiJsonObj; + for (QMap::const_iterator it = m_contactUids.constBegin(); + it != m_contactUids.constEnd(); ++it) { + cuiJsonObj.insert(it.key(), QJsonValue(it.value())); + } + QJsonDocument cuiJsonDoc(cuiJsonObj); + QVariant cuiValue(cuiJsonDoc.toBinaryData()); + + // m_contactUris + QJsonObject cuJsonObj; + for (QMap::const_iterator it = m_contactUris.constBegin(); + it != m_contactUris.constEnd(); ++it) { + cuJsonObj.insert(it.key(), QJsonValue(it.value())); + } + QJsonDocument cuJsonDoc(cuJsonObj); + QVariant cuValue(cuJsonDoc.toBinaryData()); + + // m_contactEtags + QJsonObject ceJsonObj; + for (QMap::const_iterator it = m_contactEtags.constBegin(); + it != m_contactEtags.constEnd(); ++it) { + ceJsonObj.insert(it.key(), QJsonValue(it.value())); + } + QJsonDocument ceJsonDoc(ceJsonObj); + QVariant ceValue(ceJsonDoc.toBinaryData()); + + // m_contactIds + QJsonObject ciJsonObj; + for (QMap::const_iterator it = m_contactIds.constBegin(); + it != m_contactIds.constEnd(); ++it) { + ciJsonObj.insert(it.key(), QJsonValue(it.value())); + } + QJsonDocument ciJsonDoc(ciJsonObj); + QVariant ciValue(ciJsonDoc.toBinaryData()); + + // m_contactUnsupportedProperties + QJsonObject cupJsonObj; + for (QMap::const_iterator it = m_contactUnsupportedProperties.constBegin(); + it != m_contactUnsupportedProperties.constEnd(); ++it) { + cupJsonObj.insert(it.key(), QJsonValue(QJsonArray::fromStringList(it.value()))); + } + QJsonDocument cupJsonDoc(cupJsonObj); + QVariant cupValue(cupJsonDoc.toBinaryData()); + + // store to OOB + QMap values; + values.insert("addressbookContactGuids", acgValue); + values.insert("addressbookCtags", acValue); + values.insert("addressbookSyncTokens", asValue); + values.insert("contactUids", cuiValue); + values.insert("contactUris", cuValue); + values.insert("contactEtags", ceValue); + values.insert("contactIds", ciValue); + values.insert("contactUnsupportedProperties", cupValue); + if (!d->m_engine->storeOOB(d->m_stateData[QString::number(accountId)].m_oobScope, values)) { + qWarning() << Q_FUNC_INFO << "failed to store extra state data for carddav account" << accountId; + d->clear(QString::number(accountId)); + return false; + } + + return true; +} diff --git a/src/syncer_p.h b/src/syncer_p.h new file mode 100644 index 0000000..c9982ca --- /dev/null +++ b/src/syncer_p.h @@ -0,0 +1,120 @@ +/* + * This file is part of buteo-sync-plugin-carddav package + * + * Copyright (C) 2014 Jolla Ltd. and/or its subsidiary(-ies). + * + * Contributors: Chris Adams + * + * This program/library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * version 2.1 as published by the Free Software Foundation. + * + * This program/library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program/library; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#ifndef SYNCER_P_H +#define SYNCER_P_H + +#include "replyparser_p.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +QTCONTACTS_USE_NAMESPACE + +class Auth; +class CardDav; +class RequestGenerator; +namespace Buteo { class SyncProfile; } + +class Syncer : public QObject, public QtContactsSqliteExtensions::TwoWayContactSyncAdapter +{ + Q_OBJECT + +public: + Syncer(QObject *parent, Buteo::SyncProfile *profile); + ~Syncer(); + + void startSync(int accountId); + void purgeAccount(int accountId); + +Q_SIGNALS: + void syncSucceeded(); + void syncFailed(); + +protected: + // implementing the TWCSA interface + bool testAccountProvenance(const QContact &contact, const QString &accountId); + void determineRemoteChanges(const QDateTime &remoteSince, + const QString &accountId); + void upsyncLocalChanges(const QDateTime &localSince, + const QList &locallyAdded, + const QList &locallyModified, + const QList &locallyDeleted, + const QString &accountId); + +private: + bool readExtraStateData(int accountId); + bool storeExtraStateData(int accountId); + +private Q_SLOTS: + void sync(const QString &serverUrl, const QString &username, const QString &password, const QString &accessToken); + void continueSync(const QList &added, const QList &modified, const QList &removed); + void syncFinished(); + void signInError(); + void cardDavError(); + +private: + friend class CardDav; + friend class RequestGenerator; + friend class ReplyParser; + Buteo::SyncProfile *m_syncProfile; + CardDav *m_cardDav; + Auth *m_auth; + QContactManager m_contactManager; + QNetworkAccessManager m_qnam; + + // auth related + int m_accountId; + QString m_serverUrl; + QString m_username; + QString m_password; + QString m_accessToken; + + // transient + QString m_defaultAddressbook; + QMap > m_serverAdditionIndices; // uri to index into m_serverAdditions + QMap > m_serverModificationIndices; // uti to index into m_serverModifications + QMap > m_serverAdditions; // contacts added server-side, per addressbook. + QMap > m_serverModifications; // contacts modified server-side, per addressbook. + QMap > m_serverDeletions; // contacts deleted server-side, per addressbook. + + // loaded from OOB data. + QMap m_addressbookContactGuids; // addressbookUrl to list of contact guids + QMap m_addressbookCtags; + QMap m_addressbookSyncTokens; + QMap m_contactUids; // contact guid -> contact UID + QMap m_contactUris; // contact guid -> contact uri + QMap m_contactEtags; // contact guid -> contact etag + QMap m_contactIds; // contact guid -> contact id + QMap m_contactUnsupportedProperties; // contact guid -> prop strings +}; + +#endif // SYNCER_P_H