From 0910bb7aa6668d2160654313f8eb743e062d5bc5 Mon Sep 17 00:00:00 2001 From: Mark Liversedge Date: Thu, 21 Sep 2017 17:56:57 +0100 Subject: [PATCH] University of Kent Support 1 of 3 .. just add as a Google Drive service .. need to do upload dialog and file formats as subsequent commits. NOTE: this is a variant of Google Drive, not just subclassed at this point. We may simplify this later to avoid two classes that need bug fixes / changes as the Google API changes over time. --- src/Cloud/AddCloudWizard.cpp | 1 + src/Cloud/KentUniversity.cpp | 766 +++++++++++++++++++++++++++++++++++ src/Cloud/KentUniversity.h | 124 ++++++ src/Cloud/OAuthDialog.cpp | 31 +- src/Cloud/OAuthDialog.h | 3 +- src/Core/Settings.h | 10 + src/src.pro | 4 +- 7 files changed, 931 insertions(+), 8 deletions(-) create mode 100644 src/Cloud/KentUniversity.cpp create mode 100644 src/Cloud/KentUniversity.h diff --git a/src/Cloud/AddCloudWizard.cpp b/src/Cloud/AddCloudWizard.cpp index d551e4bed..e75e53904 100644 --- a/src/Cloud/AddCloudWizard.cpp +++ b/src/Cloud/AddCloudWizard.cpp @@ -344,6 +344,7 @@ AddAuth::updateServiceSettings() { QString cname; if ((cname=wizard->cloudService->settings.value(CloudService::CloudServiceSetting::Combo1, "")) != "") { + qDebug()<<"combo"<text(); wizard->cloudService->setSetting(cname.split("::").at(0), combo->text()); } if ((cname=wizard->cloudService->settings.value(CloudService::CloudServiceSetting::URL, "")) != "") { diff --git a/src/Cloud/KentUniversity.cpp b/src/Cloud/KentUniversity.cpp new file mode 100644 index 000000000..fac1c7fc9 --- /dev/null +++ b/src/Cloud/KentUniversity.cpp @@ -0,0 +1,766 @@ +/* + * Copyright (c) 2015 Magnus Gille (mgille@gmail.com) + * 2017 Mark Liversedge (liversedge@gmail.com) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "KentUniversity.h" + +#include + +#include +#include + +#include "Athlete.h" +#include "Secrets.h" +#include "Settings.h" + +#include +#include +#include +#include +#include + +#ifndef UOK_GOOGLE_DRIVE_DEBUG +// TODO(gille): This should be a command line flag. +#define UOK_GOOGLE_DRIVE_DEBUG false +#endif +#ifdef Q_CC_MSVC +#define printd(fmt, ...) do { \ + if (GOOGLE_DRIVE_DEBUG) { \ + printf("[%s:%d %s] " fmt , __FILE__, __LINE__, \ + __FUNCTION__, __VA_ARGS__); \ + fflush(stdout); \ + } \ +} while(0) +#else +#define printd(fmt, args...) \ + do { \ + if (UOK_GOOGLE_DRIVE_DEBUG) { \ + printf("[%s:%d %s] " fmt , __FILE__, __LINE__, \ + __FUNCTION__, ##args); \ + fflush(stdout); \ + } \ + } while(0) +#endif + +namespace { + static const QString kGoogleApiBaseAddr = "https://www.googleapis.com"; + static const QString kFileMimeType = "application/zip"; + static const QString kDirectoryMimeType = + "application/vnd.google-apps.folder"; + static const QString kMetadataMimeType = "application/json"; + // This is an integer but written as a string. + static const QString kMaxResults = "1000"; +}; + +struct KentUniversity::FileInfo { + QString name; + QString id; // This is the unique identifier. + QString parent; // id of the parent. + QString download_url; + + QDateTime modified; + int size; + bool is_dir; + + // This is a map of names => FileInfos for quick searching of children. + std::map > children; +}; + +KentUniversity::KentUniversity(Context *context) + : GoogleDrive(context), context_(context), root_(NULL) { + if (context) { + printd("KentUniversity::KentUniversity\n"); + nam_ = new QNetworkAccessManager(this); + connect(nam_, SIGNAL(sslErrors(QNetworkReply*, const QList & )), this, SLOT(onSslErrors(QNetworkReply*, const QList & ))); + } + root_ = NULL; + + // config + settings.insert(Combo1, QString("%1::Scope::drive::drive.appdata::drive.file").arg(GC_UOK_GOOGLE_DRIVE_AUTH_SCOPE)); + settings.insert(OAuthToken, GC_UOK_GOOGLE_DRIVE_ACCESS_TOKEN); + settings.insert(Folder, GC_UOK_GOOGLE_DRIVE_FOLDER); + settings.insert(Local1, GC_UOK_GOOGLE_DRIVE_FOLDER_ID); // derived during config, no user action + settings.insert(Local2, GC_UOK_GOOGLE_DRIVE_REFRESH_TOKEN); // derived during config, no user action + settings.insert(Local3, GC_UOK_GOOGLE_DRIVE_LAST_ACCESS_TOKEN_REFRESH); // derived during config, no user action +} + +KentUniversity::~KentUniversity() { + if (context) delete nam_; +} + +void KentUniversity::onSslErrors(QNetworkReply*reply, const QList & ) +{ + reply->ignoreSslErrors(); + +} + +// open by connecting and getting a basic list of folders available +bool KentUniversity::open(QStringList &errors) { + printd("open\n"); + // do we have a token + QString token = getSetting(GC_UOK_GOOGLE_DRIVE_ACCESS_TOKEN, "").toString(); + if (token == "") { + errors << "You must authorise with KentUniversity first"; + return false; + } + + root_ = newCloudServiceEntry(); + root_->name = "GoldenCheetah"; + root_->isDir = true; + root_->size = 0; + + FileInfo* new_root = new FileInfo; + new_root->name = "/"; + new_root->id = GetRootDirId(); + root_dir_.reset(new_root); + + MaybeRefreshCredentials(); + + // if this fails we're toast + readdir(home(), errors); + return errors.count() == 0; +} + +bool KentUniversity::close() { + // nothing to do for now + return true; +} + +// home dire +QString KentUniversity::home() { + return getSetting(GC_UOK_GOOGLE_DRIVE_FOLDER, "").toString(); +} + +QNetworkRequest KentUniversity::MakeRequestWithURL( + const QString& url, const QString& token, const QString& args) { + QString request_url( + kGoogleApiBaseAddr + url + "/?key=" + GC_GOOGLE_DRIVE_API_KEY); + if (args.length() != 0) { + request_url += "&" + args; + } + QNetworkRequest request(request_url); + request.setRawHeader( + "Authorization", (QString("Bearer %1").arg(token)).toLatin1()); + printd("Making request to %s using token: %s\n", + request_url.toStdString().c_str(), token.toStdString().c_str()); + return request; +} + +QNetworkRequest KentUniversity::MakeRequest( + const QString& token, const QString& args) { + return MakeRequestWithURL("/drive/v3/files", token, args); +} + +bool KentUniversity::createFolder(QString path) { + printd("createFolder: %s\n", path.toStdString().c_str()); + MaybeRefreshCredentials(); + QString token = getSetting(GC_UOK_GOOGLE_DRIVE_ACCESS_TOKEN, "").toString(); + if (token == "") { + return false; + } + while (*path.begin() == '/') { + path = path.remove(0, 1); + } + + if (path.size() == 0) { + // Eh? + return true; + } + // TODO(gille): This only supports directories in the root. Fix that. + QStringList parts = path.split("/", QString::SkipEmptyParts); + QString dir_name = parts.back(); + FileInfo* parent_fi = WalkFileInfo(path, true); + if (parent_fi == NULL) { + // This shouldn't happen... + return false; + } + + QNetworkRequest request = MakeRequestWithURL( + "/drive/v3/files", token, ""); + + request.setRawHeader("Content-Type", kMetadataMimeType.toLatin1()); + //QString("multipart/mixed; boundary=\"" + boundary + "\"").toLatin1()); + printd("Creating directory %s\n", path.toStdString().c_str()); + + QJsonObject json_request; + json_request.insert("name", QJsonValue(dir_name)); + json_request.insert("mimeType", QJsonValue(kDirectoryMimeType)); + + if (parent_fi->id != "") {//FIXME + QJsonArray array; + QJsonObject parent; + parent.insert("id", parent_fi->id); + array.append(parent); + json_request.insert("parents", array); + } + + QString requestBody = QJsonDocument(json_request).toJson() + "\r\n";; + printd("Creating: %s\n", requestBody.toStdString().c_str()); + + // post the file + QNetworkReply *reply = nam_->post(request, requestBody.toLatin1()); + // blocking request + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + // did we get a good response ? + QByteArray r = reply->readAll(); + if (reply->error() != 0) { + printd("Got error %d %s\n", reply->error(), r.data()); + // Return an error? + return false; + } + printd("reply: %s\n", r.data()); + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(r, &parseError); + + // If there's an error just give up. + if (parseError.error != QJsonParseError::NoError) { + return false; + } + return true; +} + +KentUniversity::FileInfo* KentUniversity::WalkFileInfo(const QString& path, + bool foo) { + QStringList parts = path.split("/", QString::SkipEmptyParts); + FileInfo* target = root_dir_.data(); + // Lets walk! + for (QStringList::iterator it = parts.begin(); it != parts.end(); ++it) { + std::map >::iterator child = + target->children.find(*it); + if (child == target->children.end()) { + // Directory doesn't exist! + printd("Bailing, because I couldn't find: %s in %s\n", + (*it).toStdString().c_str(), + target->name.toStdString().c_str()); + if (foo) { + return target; + } + return NULL; + } + target = child->second.data(); + } + return target; +} + +QList KentUniversity::readdir(QString path, QStringList &errors) { + // Ugh. Listing files in Google Drive is "different". + // There can be many files with the same name, directories are just metadata + // so we just list everything and then we turn it into a normal structure + // locally. + + // Note, if we call readdir on "/" we nuke all and any caches we have. + // This is to try and keep life "easier". + + printd("readdir %s\n", path.toStdString().c_str()); + // First we need to find out the folder id. + // Then we can list the actual folder. + + QList returning; + // Trim some / if necssary. + while (*path.end() == '/' && path.size() > 1) { + path = path.remove(path.size() - 1, 1); + } + // do we have a token? + MaybeRefreshCredentials(); + QString token = getSetting(GC_UOK_GOOGLE_DRIVE_ACCESS_TOKEN, "").toString(); + if (token == "") { + errors << tr("You must authorise with KentUniversity first"); + return returning; + } + + FileInfo* parent_fi = WalkFileInfo(path, false); + if (parent_fi == NULL) { + // This can happen.. If it does we kind of have to walk our way up + // here even though it'll be slow and painful. + // We could store the directory id and find it that way... + + // Ok. Lets try to fake it out. Who knows maybe it'll work. + // TODO(gille): Handle an empty response/404 below? + if (path == home()) { + printd("Build path for home directory.\n"); + parent_fi = BuildDirectoriesForAthleteDirectory(path); + } + if (parent_fi == NULL) { + errors << tr("No such directory, try setting a new location in " + "options."); + return returning; + } + } + QNetworkRequest request = MakeRequest( + token, MakeQString(parent_fi->id) + + QString("&pageSize=" + kMaxResults + "&fields=nextPageToken,files(explicitlyTrashed,fileExtension,id,kind,mimeType,modifiedTime,name,properties,size,trashed)")); + QString url = request.url().toString(); + QNetworkReply *reply = nam_->get(request); + + // blocking request + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + // did we get a good response ? + QByteArray r = reply->readAll(); + if (reply->error() != 0) { + printd("Got error %d %s\n", reply->error(), r.data()); + // Return an error? + return returning; + } + printd("reply: %s\n", r.data()); + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(r, &parseError); + + // If there's an error just give up. + if (parseError.error != QJsonParseError::NoError) { + printd("json parse error: ....\n"); // FIXME + return returning; + } + + QJsonArray contents = document.object()["files"].toArray(); + // Fetch more files as long as there are more files. + QString next_page_token = document.object()["nextPageToken"].toString(); + while (next_page_token != "") { + printd("Fetching next page!\n"); + document = FetchNextLink(url + "&pageToken=" + next_page_token, token); + // Append items; + QJsonArray tmp = document.object()["files"].toArray(); + for (int i = 0; i < tmp.size(); i++ ) { + contents.push_back(tmp.at(i)); + } + next_page_token = document.object()["nextPageToken"].toString(); + } + // Ok. We have reeeived all the files. + // Technically we could cache this, but if we do we need to figure out + // when to invalidate it. + std::map > file_by_id; + for(int i = 0; i < contents.size(); i++) { + QJsonObject file = contents.at(i).toObject(); + // Some paranoia. + QJsonObject::iterator it = file.find("trashed"); + if (it != file.end() && (*it).toString() == "true") { + continue; + } + it = file.find("explicitlyTrashed"); + if (it != file.end() && (*it).toString() == "true") { + continue; + } + QSharedPointer fi(new FileInfo); + fi->name = file["name"].toString(); + fi->id = file["id"].toString(); + QJsonArray parents = file["parents"].toArray(); + // First parent is best parent! + fi->parent = parents.at(0).toObject()["id"].toString(); + if (fi->parent == "") { + // NOTE(gille): This doesn't really happen since folders in the + // root belong to a specific id. But I'd rather be safe than sorry. + fi->parent = GetRootDirId(); + } + it = file.find("mimeType"); + if (it != file.end() && (*it).toString() == kDirectoryMimeType) { + fi->is_dir = true; + } else { + fi->is_dir = false; + } + fi->size = file["fileSize"].toInt(); + // dates in format "Tue, 19 Jul 2011 21:55:38 +0000" + fi->modified = QDateTime::fromString( + file["modifiedDate"].toString().mid(0,25), + "ddd, dd MMM yyyy hh:mm:ss"); + fi->download_url = QString( + "https://www.googleapis.com/drive/v3/files/" + fi->id + + "?alt=media"); + file_by_id[fi->id] = fi; + } + // Ok. We now have all valid files. Build the tree. We only rebuild the part + // that we updated. + FileInfo* new_root; + if (path == "/") { + new_root = new FileInfo; + new_root->name = "/"; + new_root->id = GetRootDirId(); + root_dir_.reset(new_root); + printd("Creating new root.\n"); + } else { + new_root = parent_fi; + new_root->children.clear(); + printd("Appending to old root.\n"); + } + + // We know they're all where they should be right? + for (std::map >::iterator it = + file_by_id.begin(); + it != file_by_id.end(); ++it) { + new_root->children[it->second->name] = it->second; + } + + // Ok. It's now in a nice format. Lets walk so we can return what the + // other layers expect. + FileInfo* target = WalkFileInfo(path, false); + if (target == NULL) { + printd("Unable to walk the paths.\n"); + return returning; + } + for (std::map >::iterator it = + target->children.begin(); + it != target->children.end(); ++it) { + CloudServiceEntry *add = newCloudServiceEntry(); + // Google Drive just stores the file name. + add->name = it->second->name; + add->id = add->name; + printd("Returning entry: %s\n", add->name.toStdString().c_str()); + add->isDir = it->second->is_dir; + add->size = it->second->size; + add->modified = it->second->modified; + returning << add; + } + // all good! + printd("returning %d entries.\n", returning.size()); + return returning; +} + +// read a file at location (relative to home) into passed array +bool KentUniversity::readFile(QByteArray *data, QString remote_name, QString) { + printd("readfile %s\n", remote_name.toStdString().c_str()); + // this must be performed asyncronously and call made + // to notifyReadComplete(QByteArray &data, QString remotename, + // QString message) when done + + MaybeRefreshCredentials(); + QString token = getSetting(GC_UOK_GOOGLE_DRIVE_ACCESS_TOKEN, "").toString(); + if (token == "") { + return false; + } + + // Before this is done we know we have called readdir so we have the id. + FileInfo* fi = WalkFileInfo(home() + "/" + remote_name, false); + // TODO(gille): Is it worth doing readdir if this fails? + if (fi == NULL) { + printd("Trying to download files that don't exist?\n"); + return false; + } + printd("Trying to download: %s from %s\n", + remote_name.toStdString().c_str(), + fi->download_url.toStdString().c_str()); + QNetworkRequest request(fi->download_url); + request.setRawHeader( + "Authorization", (QString("Bearer %1").arg(token)).toLatin1()); + // Get the file. + QNetworkReply *reply = nam_->get(request); + // remember the file. + mapReply(reply, remote_name); + buffers_.insert(reply, data); + // catch finished signal + connect(reply, SIGNAL(finished()), this, SLOT(readFileCompleted())); + connect(reply, SIGNAL(readyRead()), this, SLOT(readyRead())); + return true; +} + +QString KentUniversity::MakeQString(const QString& parent) { + QString q; + if (parent == "") { + q = QString("q=") + QUrl::toPercentEncoding( + "trashed+=+false+AND+'root'+IN+parents", "+"); + } else { + q = QString("q=") + QUrl::toPercentEncoding( + "trashed+=+false+AND+'" + parent + "'+IN+parents", "+"); + } + return q; +} + +QJsonDocument KentUniversity::FetchNextLink( + const QString& url, const QString& token) { + QNetworkRequest request(url); + request.setRawHeader( + "Authorization", (QString("Bearer %1").arg(token)).toLatin1()); + QNetworkReply* reply = nam_->get(request); + + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + QJsonArray array; + QByteArray r = reply->readAll(); + QJsonDocument empty_doc; + if (reply->error() != 0) { + printd("Got error %d %s\n", reply->error(), r.data()); + // Return an error? + return empty_doc; + } + printd("Got response: %s\n", r.data()); + + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(r, &parseError); + if (parseError.error != QJsonParseError::NoError) { + return empty_doc; // Just return an empty document. + } + return document; +} + +bool KentUniversity::writeFile(QByteArray &data, QString remote_name, RideFile *ride) { + + Q_UNUSED(ride); + + // this must be performed asyncronously and call made + // to notifyWriteCompleted(QString remotename, QString message) when done + + MaybeRefreshCredentials(); + + // do we have a token ? + QString token = getSetting(GC_UOK_GOOGLE_DRIVE_ACCESS_TOKEN, "").toString(); + if (token == "") { + return false; + } + QString path = home(); + FileInfo* parent_fi = WalkFileInfo(path, false); + if (parent_fi == NULL) { + return false; + } + + // KentUniversity is a bit special, more than one file can have the same name + // so we need to check their ID and make sure they are unique. + FileInfo* fi = WalkFileInfo(path + "/" + remote_name, false); + + QNetworkRequest request; + if (fi == NULL) { + // This means we will post the file further down. + request = MakeRequestWithURL( + "/upload/drive/v3/files", token, "uploadType=multipart"); + } else { + // This file is known, just overwrite the old one. + request = MakeRequestWithURL( + "/upload/drive/v3/files/" + fi->id, token, "uploadType=multipart"); + } + QString boundary = "------------------------------------"; + QString delimiter = "\r\n--" + boundary + "\r\n"; + request.setRawHeader( + "Content-Type", + QString("multipart/mixed; boundary=\"" + boundary + "\"").toLatin1()); + QString base64data = data.toBase64(); + printd("Uploading file %s\n", remote_name.toStdString().c_str()); + printd("Using parent id: %s\n", parent_fi->id.toStdString().c_str()); + + QString multipartRequestBody = + delimiter + + "Content-Type: " + kMetadataMimeType + "\r\n\r\n" + + "{ name: \"" + remote_name + "\" "; + if (fi == NULL) { + // NOTE(gille): Parents is only valid on upload. + multipartRequestBody += + " ,parents: [ \"" + parent_fi->id + "\" ] "; + } + multipartRequestBody += + "}" + + delimiter + + "Content-Type: " + kFileMimeType + "\r\n" + + "Content-Transfer-Encoding: base64\r\n\r\n" + + base64data + + "\r\n--" + boundary + "--"; + + // post the file + QNetworkReply *reply; + if (fi == NULL) { + printd("Posting the file\n"); + reply = nam_->post(request, multipartRequestBody.toLatin1()); + } else { + printd("Patching the file\n"); + // Ugh, QT doesn't support Patch and we need it to upload files. + // So we get to handle this all on our own. + QSharedPointer buffer(new QBuffer()); + buffer->open(QBuffer::ReadWrite); + buffer->write(multipartRequestBody.toLatin1()); + buffer->seek(0); + reply = nam_->sendCustomRequest(request, "PATCH", buffer.data()); + + mu_.lock(); + patch_buffers_[reply] = buffer; + mu_.unlock(); + } + + // remember + mapReply(reply, remote_name); + + // catch finished signal + connect(reply, SIGNAL(finished()), this, SLOT(writeFileCompleted())); + return true; +} + +void KentUniversity::writeFileCompleted() { + QNetworkReply *reply = static_cast(QObject::sender()); + QByteArray r = reply->readAll(); + + printd("QNetworkReply: %d\n", reply->error()); + printd("write file: %s\n", r.data()); + + // Erase the data buffer if there is one. + + mu_.lock(); + QMap >::iterator it = + patch_buffers_.find(reply); + if (it != patch_buffers_.end()) { + patch_buffers_.erase(it); + } + mu_.unlock(); + + if (reply->error() == QNetworkReply::NoError) { + notifyWriteComplete( + replyName(static_cast(QObject::sender())), + tr("Completed.")); + } else { + notifyWriteComplete( + replyName(static_cast(QObject::sender())), + tr("Upload failed") + QString(" ") + QString(r.data())); + } +} + +void KentUniversity::readyRead() { + QNetworkReply *reply = static_cast(QObject::sender()); + buffers_.value(reply)->append(reply->readAll()); +} + +void KentUniversity::readFileCompleted() { + QNetworkReply *reply = static_cast(QObject::sender()); + notifyReadComplete(buffers_.value(reply), replyName(reply), + tr("Completed.")); + // erase from buffer? +} + +void KentUniversity::MaybeRefreshCredentials() { + QString last_refresh_str = getSetting(GC_UOK_GOOGLE_DRIVE_LAST_ACCESS_TOKEN_REFRESH, "0").toString(); + QDateTime last_refresh = QDateTime::fromString(last_refresh_str); + // TODO(gille): remember when it expires. + last_refresh = last_refresh.addSecs(45 * 60); + QString refresh_token = getSetting(GC_UOK_GOOGLE_DRIVE_REFRESH_TOKEN, "").toString(); + if (refresh_token == "") { + return; + } + // If we need to refresh the access token do so. + QDateTime now = QDateTime::currentDateTime(); + printd("times: %s %s\n", last_refresh_str.toStdString().c_str(), + now.toString().toStdString().c_str()); + if (now > last_refresh) { // This is true if the refresh is older than 45 + // minutes. + printd("Refreshing credentials.\n"); + QNetworkRequest request(kGoogleApiBaseAddr + "/oauth2/v3/token"); + request.setRawHeader("Content-Type", + "application/x-www-form-urlencoded"); + QString data = QString("client_id="); + data.append(GC_GOOGLE_DRIVE_CLIENT_ID) + .append("&").append("client_secret=") + .append(GC_GOOGLE_DRIVE_CLIENT_SECRET).append("&") + .append("refresh_token=").append(refresh_token).append("&") + .append("grant_type=refresh_token"); + printd("Making request to %s using token: %s data: %s\n", + (kGoogleApiBaseAddr + "/oauth2/v3/token").toStdString().c_str(), + refresh_token.toStdString().c_str(), data.toLatin1().data()); + QNetworkReply* reply = nam_->post(request, data.toLatin1()); + + // blocking request + QEventLoop loop; + connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); + + int statusCode = reply->attribute( + QNetworkRequest::HttpStatusCodeAttribute).toInt(); + printd("HTTP response code: %d\n", statusCode); + + if (reply->error() != 0) { + printd("Got error %d\n", reply->error()); + // Return an error? + return; + } + QByteArray r = reply->readAll(); + printd("Got response: %s\n", r.data()); + + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(r, &parseError); + + // if path was returned all is good, lets set root + if (parseError.error != QJsonParseError::NoError) { + printd("Parse error!\n"); + return; + } + + QString access_token = document.object()["access_token"].toString(); + + // LOCALLY MAINTAINED -- WILL BE AN ISSUE IF ALLOW >1 ACCOUNT XXX + appsettings->setCValue(context_->athlete->cyclist, GC_UOK_GOOGLE_DRIVE_ACCESS_TOKEN, access_token); + appsettings->setCValue(context_->athlete->cyclist, GC_UOK_GOOGLE_DRIVE_LAST_ACCESS_TOKEN_REFRESH, now.toString()); + } else { + printd("Credentials are still good, not refreshing.\n"); + } +} + +QString KentUniversity::GetRootDirId() { + QString scope = getSetting(GC_UOK_GOOGLE_DRIVE_AUTH_SCOPE, "drive.appdata").toString(); + if (scope == "drive.appdata") { + return "appdata"; // dgr : not appDataFolder ? + } else { + return "root"; + } +} + +QString KentUniversity::GetFileId(const QString& path) { + FileInfo* fi = WalkFileInfo(path, false); + if (fi == NULL) { + return ""; + } + return fi->id; +} + +KentUniversity::FileInfo* KentUniversity::BuildDirectoriesForAthleteDirectory( + const QString& path) { + // Because Google Drive is a little bit "different" we can't just read + // "/foo/bar/baz, we need to know the id's of both foo and bar to find + // baz. + const QString id = getSetting(GC_UOK_GOOGLE_DRIVE_FOLDER_ID, "").toString(); + if (id == "") { + // TODO(gille): Maybe we should actually find this dir if this happens + // however this is a weird configuration error, that (tm) + // should not happen. + return NULL; + } + printd("GC_UOK_GOOGLE_DRIVE_FOLDER_ID: %s\n", id.toStdString().c_str()); + QStringList parts = path.split("/", QString::SkipEmptyParts); + FileInfo *fi = root_dir_.data(); + + for (QStringList::iterator it = parts.begin(); it != parts.end(); ++it) { + std::map >::iterator next = + fi->children.find(*it); + if (next == fi->children.end()) { + QSharedPointer next_fi(new FileInfo); + next_fi->name = *it; + next_fi->id = " INVALID "; + next_fi->parent = " INVALID "; + fi->children[*it] = next_fi; + fi = next_fi.data(); + } else { + fi = next->second.data(); + } + } + // Overwrite the final directory id with the right one. + fi->id = id; + return fi; +} + + +static bool addKentUniversity() { + CloudServiceFactory::instance().addService(new KentUniversity(NULL)); + return true; +} + +static bool add = addKentUniversity(); diff --git a/src/Cloud/KentUniversity.h b/src/Cloud/KentUniversity.h new file mode 100644 index 000000000..c9e9d2a15 --- /dev/null +++ b/src/Cloud/KentUniversity.h @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2015 Magnus Gille (mgille@gmail.com) + * 2017 Mark Liversedge (liversedge@gmail.com) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef GC_KENT_UNI_H +#define GC_KENT_UNI_H + +#include "GoogleDrive.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Kent Uni studies share data using Google Drive, but there is only an uploader +// and the file format is specific to their needs (csv file). Metadata is sent +// in a separate text file, with RPE and ROF mandated. +class KentUniversity : public GoogleDrive { + + Q_OBJECT + + public: + KentUniversity(Context *context); + CloudService *clone(Context *context) { return new KentUniversity(context); } + ~KentUniversity(); + + virtual QString id() const { return "University of Kent"; } + virtual QString uiName() const { return tr("University of Kent"); } + virtual QString description() const { return (tr("Participate in academic studies sharing data via google drive.")); } + QImage logo() const { return QImage(":images/services/unikent.jpg"); } + + // open/connect and close/disconnect + virtual bool open(QStringList &errors); + virtual bool close(); + + // home directory + virtual QString home(); + + // write a file + virtual bool writeFile(QByteArray &data, QString remotename, RideFile *ride); + + // read a file + virtual bool readFile(QByteArray *data, QString remotename, QString); + + // create a folder + virtual bool createFolder(QString path); + + // dirent style api + virtual CloudServiceEntry *root() { return root_; } + // Readdir reads the files from the remote side and updates root_dir_ + // with a local cache. readdir will read ALL files and refresh + // everything. + virtual QList readdir( + QString path, QStringList &errors); + + // Returns the fild id or "" if no file was found, uses the local + // cache to determine file id. + QString GetFileId(const QString& path); + + public slots: + + // getting data + void readyRead(); // a readFile operation has work to do + void readFileCompleted(); + + // sending data + void writeFileCompleted(); + + // dealing with SSL handshake problems + void onSslErrors(QNetworkReply*reply, const QList & ); + + private: + struct FileInfo; + + void MaybeRefreshCredentials(); + + // Fetches a JSON document from the given URL. + QJsonDocument FetchNextLink(const QString& url, const QString& token); + + FileInfo* WalkFileInfo(const QString& path, bool foo); + + FileInfo* BuildDirectoriesForAthleteDirectory(const QString& path); + + static QNetworkRequest MakeRequestWithURL( + const QString& url, const QString& token, const QString& args); + static QNetworkRequest MakeRequest( + const QString& token, const QString& args); + // Make QString returns a "q" string usable for searches in Google + // Drive. + static QString MakeQString(const QString& parent); + QString GetRootDirId(); + + Context *context_; + QNetworkAccessManager *nam_; + CloudServiceEntry *root_; + QString root_directory_id_; + QScopedPointer root_dir_; + + QMap buffers_; + QMap > patch_buffers_; + QMutex mu_; +}; + +#endif // GC_KENT_UNI_H diff --git a/src/Cloud/OAuthDialog.cpp b/src/Cloud/OAuthDialog.cpp index 83cffe7a9..5521a1b20 100644 --- a/src/Cloud/OAuthDialog.cpp +++ b/src/Cloud/OAuthDialog.cpp @@ -47,6 +47,7 @@ OAuthDialog::OAuthDialog(Context *context, OAuthSite site, CloudService *service if (service->id() == "Dropbox") site = this->site = DROPBOX; if (service->id() == "Cycling Analytics") site = this->site = CYCLING_ANALYTICS; if (service->id() == "Google Drive") site = this->site = GOOGLE_DRIVE; + if (service->id() == "University of Kent") site = this->site = KENTUNI; if (service->id() == "Today's Plan") site = this->site = TODAYSPLAN; if (service->id() == "Withings") site = this->site = WITHINGS; if (service->id() == "PolarFlow") site = this->site = POLAR; @@ -138,6 +139,18 @@ OAuthDialog::OAuthDialog(Context *context, OAuthSite site, CloudService *service urlstr.append("response_type=code&"); urlstr.append("client_id=").append(GC_GOOGLE_DRIVE_CLIENT_ID); + } else if (site == KENTUNI) { + + const QString scope = service->getSetting(GC_UOK_GOOGLE_DRIVE_AUTH_SCOPE, "drive.appdata").toString(); + + // OAUTH 2.0 - Google flow for installed applications + urlstr = QString("https://accounts.google.com/o/oauth2/auth?"); + // We only request access to the application data folder, not all files. + urlstr.append("scope=https://www.googleapis.com/auth/" + scope + "&"); + urlstr.append("redirect_uri=urn:ietf:wg:oauth:2.0:oob&"); + urlstr.append("response_type=code&"); + urlstr.append("client_id=").append(GC_GOOGLE_DRIVE_CLIENT_ID); + #endif } else if (site == TODAYSPLAN) { @@ -195,7 +208,7 @@ OAuthDialog::OAuthDialog(Context *context, OAuthSite site, CloudService *service // // STEP 1: LOGIN AND AUTHORISE THE APPLICATION // - if (site == DROPBOX || site == STRAVA || site == CYCLING_ANALYTICS || site == POLAR || site == SPORTTRACKS || site == GOOGLE_DRIVE || site == TODAYSPLAN) { + if (site == DROPBOX || site == STRAVA || site == CYCLING_ANALYTICS || site == POLAR || site == SPORTTRACKS || site == GOOGLE_DRIVE || site == KENTUNI || site == TODAYSPLAN) { url = QUrl(urlstr); view->setUrl(url); @@ -453,7 +466,7 @@ void OAuthDialog::loadFinished(bool ok) { - if (site == GOOGLE_DRIVE) { + if (site == GOOGLE_DRIVE || site == KENTUNI) { if (ok && url.toString().startsWith("https://accounts.google.com/o/oauth2/auth")) { @@ -472,7 +485,7 @@ OAuthDialog::loadFinished(bool ok) QString urlstr = "https://www.googleapis.com/oauth2/v3/token?"; params.addQueryItem("client_id", GC_GOOGLE_DRIVE_CLIENT_ID); - if (site == GOOGLE_DRIVE) { + if (site == GOOGLE_DRIVE || site == KENTUNI) { params.addQueryItem("client_secret", GC_GOOGLE_DRIVE_CLIENT_SECRET); } @@ -571,7 +584,7 @@ OAuthDialog::networkRequestFinished(QNetworkReply *reply) // if we failed to extract then we have a big problem // google uses a refresh token so trap for them only - if ((site == GOOGLE_DRIVE && refresh_token == "") || access_token == "") { + if (((site == GOOGLE_DRIVE || site == KENTUNI) && refresh_token == "") || access_token == "") { // Something failed. // Only Google uses both refresh and access tokens. @@ -651,9 +664,17 @@ OAuthDialog::networkRequestFinished(QNetworkReply *reply) QMessageBox information(QMessageBox::Information, tr("Information"), info); information.exec(); + } else if (site == KENTUNI) { + + service->setSetting(GC_UOK_GOOGLE_DRIVE_REFRESH_TOKEN, refresh_token); + service->setSetting(GC_UOK_GOOGLE_DRIVE_ACCESS_TOKEN, access_token); + service->setSetting(GC_UOK_GOOGLE_DRIVE_LAST_ACCESS_TOKEN_REFRESH, QDateTime::currentDateTime()); + QString info = QString(tr("Kent University Google Drive authorization was successful.")); + QMessageBox information(QMessageBox::Information, tr("Information"), info); + information.exec(); + } else if (site == GOOGLE_DRIVE) { - // remove the Google Page first service->setSetting(GC_GOOGLE_DRIVE_REFRESH_TOKEN, refresh_token); service->setSetting(GC_GOOGLE_DRIVE_ACCESS_TOKEN, access_token); service->setSetting(GC_GOOGLE_DRIVE_LAST_ACCESS_TOKEN_REFRESH, QDateTime::currentDateTime()); diff --git a/src/Cloud/OAuthDialog.h b/src/Cloud/OAuthDialog.h index 611cfa26a..7223ed011 100644 --- a/src/Cloud/OAuthDialog.h +++ b/src/Cloud/OAuthDialog.h @@ -68,7 +68,8 @@ public: SPORTTRACKS, TODAYSPLAN, WITHINGS, - POLAR + POLAR, + KENTUNI } OAuthSite; // will work with old config via site and new via cloudservice (which is null for calendar and withings for now) diff --git a/src/Core/Settings.h b/src/Core/Settings.h index f5e847970..013027317 100644 --- a/src/Core/Settings.h +++ b/src/Core/Settings.h @@ -301,6 +301,16 @@ #define GC_GOOGLE_DRIVE_FOLDER "google-drive/folder" #define GC_GOOGLE_DRIVE_FOLDER_ID "google-drive/folder_id" + +//University of Kent (same as google drive) +#define GC_UOK_GOOGLE_DRIVE_AUTH_SCOPE "uok-google-drive/auth_scope" +#define GC_UOK_GOOGLE_DRIVE_ACCESS_TOKEN "uok-google-drive/access_token" +#define GC_UOK_GOOGLE_DRIVE_REFRESH_TOKEN "uok-google-drive/refresh_token" +#define GC_UOK_GOOGLE_DRIVE_LAST_ACCESS_TOKEN_REFRESH "uok-google-drive/last_access_token_refresh" + +#define GC_UOK_GOOGLE_DRIVE_FOLDER "uok-google-drive/folder" +#define GC_UOK_GOOGLE_DRIVE_FOLDER_ID "uok-google-drive/folder_id" + //Withings #define GC_WITHINGS_TOKEN "withings_token" #define GC_WITHINGS_SECRET "withings_secret" diff --git a/src/src.pro b/src/src.pro index 968d57d75..11ddf20c3 100644 --- a/src/src.pro +++ b/src/src.pro @@ -611,8 +611,8 @@ greaterThan(QT_MAJOR_VERSION, 4) { # Features that only work with QT5 or higher SOURCES += Cloud/Dropbox.cpp HEADERS += Cloud/Dropbox.h - SOURCES += Cloud/GoogleDrive.cpp - HEADERS += Cloud/GoogleDrive.h + SOURCES += Cloud/GoogleDrive.cpp Cloud/KentUniversity.cpp + HEADERS += Cloud/GoogleDrive.h Cloud/KentUniversity.h greaterThan(QT_MINOR_VERSION, 3) { SOURCES += Cloud/SixCycle.cpp