1 /* This file is part of Clementine.
2 Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
3 Copyright 2012, 2014, David Sansome <me@davidsansome.com>
4 Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
5
6 Clementine is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Clementine is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Clementine. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "googledriveclient.h"
21
22 #include <QUrlQuery>
23 #include <QJsonParseError>
24 #include <QJsonDocument>
25 #include <QJsonObject>
26 #include <QJsonArray>
27 #include <QJsonValue>
28
29 #include "internet/core/oauthenticator.h"
30 #include "core/closure.h"
31 #include "core/logging.h"
32 #include "core/network.h"
33
34 using namespace google_drive;
35
36 const char* File::kFolderMimeType = "application/vnd.google-apps.folder";
37
38 namespace {
39 static const char* kGoogleDriveFile =
40 "https://www.googleapis.com/drive/v2/files/%1";
41 static const char* kGoogleDriveChanges =
42 "https://www.googleapis.com/drive/v2/changes";
43 static const char* kGoogleOAuthUserInfoEndpoint =
44 "https://www.googleapis.com/oauth2/v1/userinfo";
45
46 static const char* kOAuthEndpoint = "https://accounts.google.com/o/oauth2/auth";
47 static const char* kOAuthTokenEndpoint =
48 "https://accounts.google.com/o/oauth2/token";
49 static const char* kOAuthScope =
50 "https://www.googleapis.com/auth/drive.readonly "
51 "https://www.googleapis.com/auth/userinfo.email";
52 static const char* kClientId = "679260893280.apps.googleusercontent.com";
53 static const char* kClientSecret = "l3cWb8efUZsrBI4wmY3uKl6i";
54 } // namespace
55
parent_ids() const56 QStringList File::parent_ids() const {
57 QStringList ret;
58
59 for (const QVariant& var : data_["parents"].toList()) {
60 QVariantMap map(var.toMap());
61
62 if (map["isRoot"].toBool()) {
63 ret << QString();
64 } else {
65 ret << map["id"].toString();
66 }
67 }
68
69 return ret;
70 }
71
ConnectResponse(QObject * parent)72 ConnectResponse::ConnectResponse(QObject* parent) : QObject(parent) {}
73
GetFileResponse(const QString & file_id,QObject * parent)74 GetFileResponse::GetFileResponse(const QString& file_id, QObject* parent)
75 : QObject(parent), file_id_(file_id) {}
76
ListChangesResponse(const QString & cursor,QObject * parent)77 ListChangesResponse::ListChangesResponse(const QString& cursor, QObject* parent)
78 : QObject(parent), cursor_(cursor) {}
79
Client(QObject * parent)80 Client::Client(QObject* parent)
81 : QObject(parent), network_(new NetworkAccessManager(this)) {}
82
Connect(const QString & refresh_token)83 ConnectResponse* Client::Connect(const QString& refresh_token) {
84 ConnectResponse* ret = new ConnectResponse(this);
85 OAuthenticator* oauth = new OAuthenticator(
86 kClientId, kClientSecret, OAuthenticator::RedirectStyle::LOCALHOST, this);
87
88 if (refresh_token.isEmpty()) {
89 oauth->StartAuthorisation(kOAuthEndpoint, kOAuthTokenEndpoint, kOAuthScope);
90 } else {
91 oauth->RefreshAuthorisation(kOAuthTokenEndpoint, refresh_token);
92 }
93
94 NewClosure(oauth, SIGNAL(Finished()), this,
95 SLOT(ConnectFinished(ConnectResponse*, OAuthenticator*)), ret,
96 oauth);
97 return ret;
98 }
99
ConnectFinished(ConnectResponse * response,OAuthenticator * oauth)100 void Client::ConnectFinished(ConnectResponse* response, OAuthenticator* oauth) {
101 oauth->deleteLater();
102 access_token_ = oauth->access_token();
103 expiry_time_ = oauth->expiry_time();
104 response->refresh_token_ = oauth->refresh_token();
105
106 // Fetch user email.
107 QUrl url(kGoogleOAuthUserInfoEndpoint);
108 QNetworkRequest request(url);
109 AddAuthorizationHeader(&request);
110 QNetworkReply* reply = network_->get(request);
111 NewClosure(reply, SIGNAL(finished()), this,
112 SLOT(FetchUserInfoFinished(ConnectResponse*, QNetworkReply*)),
113 response, reply);
114 }
115
FetchUserInfoFinished(ConnectResponse * response,QNetworkReply * reply)116 void Client::FetchUserInfoFinished(ConnectResponse* response,
117 QNetworkReply* reply) {
118 reply->deleteLater();
119 if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) != 200) {
120 qLog(Warning) << "Failed to get user info" << reply->readAll();
121 } else {
122 QJsonParseError error;
123 QJsonDocument document = QJsonDocument::fromJson(reply->readAll(), &error);
124 if (error.error != QJsonParseError::NoError) {
125 qLog(Error) << "Failed to parse user info reply";
126 return;
127 }
128
129 qLog(Debug) << document;
130 response->user_email_ = document.object()["email"].toString();
131 qLog(Debug) << response->user_email_;
132 }
133 emit response->Finished();
134 emit Authenticated();
135 }
136
AddAuthorizationHeader(QNetworkRequest * request) const137 void Client::AddAuthorizationHeader(QNetworkRequest* request) const {
138 request->setRawHeader("Authorization",
139 QString("Bearer %1").arg(access_token_).toUtf8());
140 }
141
GetFile(const QString & file_id)142 GetFileResponse* Client::GetFile(const QString& file_id) {
143 GetFileResponse* ret = new GetFileResponse(file_id, this);
144
145 QUrl url(QString(kGoogleDriveFile).arg(file_id));
146
147 QNetworkRequest request = QNetworkRequest(url);
148 AddAuthorizationHeader(&request);
149 // Never cache these requests as we will get out of date download URLs.
150 request.setAttribute(QNetworkRequest::CacheLoadControlAttribute,
151 QNetworkRequest::AlwaysNetwork);
152
153 QNetworkReply* reply = network_->get(request);
154 NewClosure(reply, SIGNAL(finished()), this,
155 SLOT(GetFileFinished(GetFileResponse*, QNetworkReply*)), ret,
156 reply);
157
158 return ret;
159 }
160
GetFileFinished(GetFileResponse * response,QNetworkReply * reply)161 void Client::GetFileFinished(GetFileResponse* response, QNetworkReply* reply) {
162 reply->deleteLater();
163
164 QJsonParseError error;
165 QJsonDocument document = QJsonDocument::fromJson(reply->readAll(), &error);
166 if (error.error != QJsonParseError::NoError) {
167 qLog(Error) << "Failed to fetch file with ID" << response->file_id_;
168 emit response->Finished();
169 return;
170 }
171
172 response->file_ = File(document.object().toVariantMap());
173 emit response->Finished();
174 }
175
ListChanges(const QString & cursor)176 ListChangesResponse* Client::ListChanges(const QString& cursor) {
177 ListChangesResponse* ret = new ListChangesResponse(cursor, this);
178 MakeListChangesRequest(ret);
179 return ret;
180 }
181
MakeListChangesRequest(ListChangesResponse * response,const QString & page_token)182 void Client::MakeListChangesRequest(ListChangesResponse* response,
183 const QString& page_token) {
184 QUrl url(kGoogleDriveChanges);
185 QUrlQuery url_query;
186 if (!response->cursor().isEmpty()) {
187 url_query.addQueryItem("startChangeId", response->cursor());
188 }
189 if (!page_token.isEmpty()) {
190 url_query.addQueryItem("pageToken", page_token);
191 }
192
193 url.setQuery(url_query);
194
195 qLog(Debug) << "Requesting changes at:" << response->cursor() << page_token;
196
197 QNetworkRequest request(url);
198 AddAuthorizationHeader(&request);
199
200 QNetworkReply* reply = network_->get(request);
201 NewClosure(reply, SIGNAL(finished()), this,
202 SLOT(ListChangesFinished(ListChangesResponse*, QNetworkReply*)),
203 response, reply);
204 }
205
ListChangesFinished(ListChangesResponse * response,QNetworkReply * reply)206 void Client::ListChangesFinished(ListChangesResponse* response,
207 QNetworkReply* reply) {
208 reply->deleteLater();
209
210 QJsonParseError error;
211 QJsonDocument document = QJsonDocument::fromJson(reply->readAll(), &error);
212 // TODO(John Maguire): Put this on a separate thread as the response could be large.
213 if (error.error != QJsonParseError::NoError) {
214 qLog(Error) << "Failed to fetch changes" << response->cursor();
215 emit response->Finished();
216 return;
217 }
218
219 QJsonObject json_result = document.object();
220 if (json_result.contains("largestChangeId")) {
221 response->next_cursor_ = json_result["largestChangeId"].toString();
222 }
223
224 // Emit the FilesFound signal for the files in the response.
225 FileList files;
226 QList<QUrl> files_deleted;
227 for (const QJsonValue & v : json_result["items"].toArray()) {
228 QJsonObject change = v.toObject();
229 if (change["deleted"].toBool() ||
230 change["file"].toObject()["labels"].toObject()["trashed"].toBool()) {
231 QUrl url;
232 url.setScheme("googledrive");
233 url.setPath("/" + change["fileId"].toString());
234 files_deleted << url;
235 } else {
236 files << File(change["file"].toObject().toVariantMap());
237 }
238 }
239
240 emit response->FilesFound(files);
241 emit response->FilesDeleted(files_deleted);
242
243 // Get the next page of results if there is one.
244 if (json_result.contains("nextPageToken")) {
245 MakeListChangesRequest(response, json_result["nextPageToken"].toString());
246 } else {
247 emit response->Finished();
248 }
249 }
250
is_authenticated() const251 bool Client::is_authenticated() const {
252 return !access_token_.isEmpty() &&
253 QDateTime::currentDateTime().secsTo(expiry_time_) > 0;
254 }
255
ForgetCredentials()256 void Client::ForgetCredentials() {
257 access_token_ = QString();
258 expiry_time_ = QDateTime();
259 }
260