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