1 /* This file is part of Clementine.
2    Copyright 2013-2014, John Maguire <john.maguire@gmail.com>
3    Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
4 
5    Clementine is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9 
10    Clementine is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14 
15    You should have received a copy of the GNU General Public License
16    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #include "boxservice.h"
20 
21 #include <QJsonDocument>
22 #include <QJsonObject>
23 #include <QJsonArray>
24 #include <QUrlQuery>
25 
26 #include "core/application.h"
27 #include "core/player.h"
28 #include "core/waitforsignal.h"
29 #include "internet/box/boxurlhandler.h"
30 #include "internet/core/oauthenticator.h"
31 #include "library/librarybackend.h"
32 #include "ui/iconloader.h"
33 
34 const char* BoxService::kServiceName = "Box";
35 const char* BoxService::kSettingsGroup = "Box";
36 
37 namespace {
38 
39 static const char* kClientId = "gbswb9wp7gjyldc3qrw68h2rk68jaf4h";
40 static const char* kClientSecret = "pZ6cUCQz5X0xaWoPVbCDg6GpmfTtz73s";
41 
42 static const char* kOAuthEndpoint = "https://api.box.com/oauth2/authorize";
43 static const char* kOAuthTokenEndpoint = "https://api.box.com/oauth2/token";
44 
45 static const char* kUserInfo = "https://api.box.com/2.0/users/me";
46 static const char* kFolderItems = "https://api.box.com/2.0/folders/%1/items";
47 static const int kRootFolderId = 0;
48 
49 static const char* kFileContent = "https://api.box.com/2.0/files/%1/content";
50 
51 static const char* kEvents = "https://api.box.com/2.0/events";
52 }  // namespace
53 
BoxService(Application * app,InternetModel * parent)54 BoxService::BoxService(Application* app, InternetModel* parent)
55     : CloudFileService(app, parent, kServiceName, kSettingsGroup,
56                        IconLoader::Load("box", IconLoader::Provider),
57                        SettingsDialog::Page_Box) {
58   app->player()->RegisterUrlHandler(new BoxUrlHandler(this, this));
59 }
60 
has_credentials() const61 bool BoxService::has_credentials() const { return !refresh_token().isEmpty(); }
62 
refresh_token() const63 QString BoxService::refresh_token() const {
64   QSettings s;
65   s.beginGroup(kSettingsGroup);
66 
67   return s.value("refresh_token").toString();
68 }
69 
is_authenticated() const70 bool BoxService::is_authenticated() const {
71   return !access_token_.isEmpty() &&
72          QDateTime::currentDateTime().secsTo(expiry_time_) > 0;
73 }
74 
EnsureConnected()75 void BoxService::EnsureConnected() {
76   if (is_authenticated()) {
77     return;
78   }
79 
80   Connect();
81   WaitForSignal(this, SIGNAL(Connected()));
82 }
83 
Connect()84 void BoxService::Connect() {
85   OAuthenticator* oauth = new OAuthenticator(
86       kClientId, kClientSecret, OAuthenticator::RedirectStyle::REMOTE, this);
87   if (!refresh_token().isEmpty()) {
88     oauth->RefreshAuthorisation(kOAuthTokenEndpoint, refresh_token());
89   } else {
90     oauth->StartAuthorisation(kOAuthEndpoint, kOAuthTokenEndpoint, QString());
91   }
92 
93   NewClosure(oauth, SIGNAL(Finished()), this,
94              SLOT(ConnectFinished(OAuthenticator*)), oauth);
95 }
96 
ConnectFinished(OAuthenticator * oauth)97 void BoxService::ConnectFinished(OAuthenticator* oauth) {
98   oauth->deleteLater();
99 
100   QSettings s;
101   s.beginGroup(kSettingsGroup);
102   s.setValue("refresh_token", oauth->refresh_token());
103 
104   access_token_ = oauth->access_token();
105   expiry_time_ = oauth->expiry_time();
106 
107   if (s.value("name").toString().isEmpty()) {
108     QUrl url(kUserInfo);
109     QNetworkRequest request(url);
110     AddAuthorizationHeader(&request);
111 
112     QNetworkReply* reply = network_->get(request);
113     NewClosure(reply, SIGNAL(finished()), this,
114                SLOT(FetchUserInfoFinished(QNetworkReply*)), reply);
115   } else {
116     emit Connected();
117   }
118   UpdateFiles();
119 }
120 
AddAuthorizationHeader(QNetworkRequest * request) const121 void BoxService::AddAuthorizationHeader(QNetworkRequest* request) const {
122   request->setRawHeader("Authorization",
123                         QString("Bearer %1").arg(access_token_).toUtf8());
124 }
125 
FetchUserInfoFinished(QNetworkReply * reply)126 void BoxService::FetchUserInfoFinished(QNetworkReply* reply) {
127   reply->deleteLater();
128 
129   QJsonObject json_response = QJsonDocument::fromJson(reply->readAll()).object();
130 
131   QString name = json_response["name"].toString();
132   if (!name.isEmpty()) {
133     QSettings s;
134     s.beginGroup(kSettingsGroup);
135     s.setValue("name", name);
136   }
137 
138   emit Connected();
139 }
140 
ForgetCredentials()141 void BoxService::ForgetCredentials() {
142   QSettings s;
143   s.beginGroup(kSettingsGroup);
144 
145   s.remove("refresh_token");
146   s.remove("name");
147 }
148 
UpdateFiles()149 void BoxService::UpdateFiles() {
150   QSettings s;
151   s.beginGroup(kSettingsGroup);
152 
153   if (!s.value("cursor").toString().isEmpty()) {
154     // Use events API to fetch changes.
155     UpdateFilesFromCursor(s.value("cursor").toString());
156     return;
157   }
158 
159   // First run we scan as events may not cover everything.
160   FetchRecursiveFolderItems(kRootFolderId);
161   InitialiseEventsCursor();
162 }
163 
InitialiseEventsCursor()164 void BoxService::InitialiseEventsCursor() {
165   QUrl url(kEvents);
166   QUrlQuery url_query;
167   url_query.addQueryItem("stream_position", "now");
168   url.setQuery(url_query);
169   QNetworkRequest request(url);
170   AddAuthorizationHeader(&request);
171   QNetworkReply* reply = network_->get(request);
172   NewClosure(reply, SIGNAL(finished()), this,
173              SLOT(InitialiseEventsFinished(QNetworkReply*)), reply);
174 }
175 
InitialiseEventsFinished(QNetworkReply * reply)176 void BoxService::InitialiseEventsFinished(QNetworkReply* reply) {
177   reply->deleteLater();
178   QJsonObject json_response = QJsonDocument::fromJson(reply->readAll()).object();
179   if (json_response.contains("next_stream_position")) {
180     QSettings s;
181     s.beginGroup(kSettingsGroup);
182     s.setValue("cursor", json_response["next_stream_position"].toString());
183   }
184 }
185 
FetchRecursiveFolderItems(const int folder_id,const int offset)186 void BoxService::FetchRecursiveFolderItems(const int folder_id,
187                                            const int offset) {
188   QUrl url(QString(kFolderItems).arg(folder_id));
189   QStringList fields;
190   fields << "etag"
191          << "size"
192          << "created_at"
193          << "modified_at"
194          << "name";
195   QString fields_list = fields.join(",");
196   QUrlQuery url_query (url);
197   url_query.addQueryItem("fields", fields_list);
198   url_query.addQueryItem("limit", "1000");  // Maximum according to API docs.
199   url_query.addQueryItem("offset", QString::number(offset));
200   url.setQuery(url_query);
201   QNetworkRequest request(url);
202   AddAuthorizationHeader(&request);
203   QNetworkReply* reply = network_->get(request);
204   NewClosure(reply, SIGNAL(finished()), this,
205              SLOT(FetchFolderItemsFinished(QNetworkReply*, int)), reply,
206              folder_id);
207 }
208 
FetchFolderItemsFinished(QNetworkReply * reply,const int folder_id)209 void BoxService::FetchFolderItemsFinished(QNetworkReply* reply,
210                                           const int folder_id) {
211   reply->deleteLater();
212 
213   QJsonObject json_response = QJsonDocument::fromJson(reply->readAll()).object();
214 
215   QJsonArray entries = json_response["entries"].toArray();
216   const int total_entries = json_response["total_count"].toInt();
217   const int offset = json_response["offset"].toInt();
218   if (entries.size() + offset < total_entries) {
219     // Fetch the next page if necessary.
220     FetchRecursiveFolderItems(folder_id, offset + entries.size());
221   }
222 
223   for (const QJsonValue& e : entries) {
224     QJsonObject entry = e.toObject();
225     if (entry["type"].toString() == "folder") {
226       FetchRecursiveFolderItems(entry["id"].toInt());
227     } else {
228       MaybeAddFileEntry(entry);
229     }
230   }
231 }
232 
MaybeAddFileEntry(const QJsonObject & entry)233 void BoxService::MaybeAddFileEntry(const QJsonObject &entry) {
234   QString mime_type = GuessMimeTypeForFile(entry["name"].toString());
235   QUrl url;
236   url.setScheme("box");
237   url.setPath("/" + entry["id"].toString());
238 
239   Song song;
240   song.set_url(url);
241   song.set_ctime(QDateTime::fromString(entry["created_at"].toString()).toTime_t());
242   song.set_mtime(QDateTime::fromString(entry["modified_at"].toString()).toTime_t());
243   song.set_filesize(entry["size"].toInt());
244   song.set_title(entry["name"].toString());
245 
246   // This is actually a redirect. Follow it now.
247   QNetworkReply* reply = FetchContentUrlForFile(entry["id"].toString());
248   NewClosure(reply, SIGNAL(finished()), this,
249              SLOT(RedirectFollowed(QNetworkReply*, Song, QString)), reply, song,
250              mime_type);
251 }
252 
FetchContentUrlForFile(const QString & file_id)253 QNetworkReply* BoxService::FetchContentUrlForFile(const QString& file_id) {
254   QUrl content_url(QString(kFileContent).arg(file_id));
255   QNetworkRequest request(content_url);
256   AddAuthorizationHeader(&request);
257   QNetworkReply* reply = network_->get(request);
258   return reply;
259 }
260 
RedirectFollowed(QNetworkReply * reply,const Song & song,const QString & mime_type)261 void BoxService::RedirectFollowed(QNetworkReply* reply, const Song& song,
262                                   const QString& mime_type) {
263   reply->deleteLater();
264   QVariant redirect =
265       reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
266   if (!redirect.isValid()) {
267     return;
268   }
269 
270   QUrl real_url = redirect.toUrl();
271   MaybeAddFileToDatabase(song, mime_type, real_url,
272                          QString("Bearer %1").arg(access_token_));
273 }
274 
UpdateFilesFromCursor(const QString & cursor)275 void BoxService::UpdateFilesFromCursor(const QString& cursor) {
276   QUrl url(kEvents);
277   QUrlQuery url_query;
278   url_query.addQueryItem("stream_position", cursor);
279   url_query.addQueryItem("limit", "5000");
280   url.setQuery(url_query);
281   QNetworkRequest request(url);
282   AddAuthorizationHeader(&request);
283   QNetworkReply* reply = network_->get(request);
284   NewClosure(reply, SIGNAL(finished()), this,
285              SLOT(FetchEventsFinished(QNetworkReply*)), reply);
286 }
287 
FetchEventsFinished(QNetworkReply * reply)288 void BoxService::FetchEventsFinished(QNetworkReply* reply) {
289   // TODO(John Maguire): Page through events.
290   reply->deleteLater();
291   QJsonObject json_response = QJsonDocument::fromJson(reply->readAll()).object();
292 
293   QSettings s;
294   s.beginGroup(kSettingsGroup);
295   s.setValue("cursor", json_response["next_stream_position"].toString());
296 
297   QJsonArray entries = json_response["entries"].toArray();
298   for (const QJsonValue& e : entries) {
299     QJsonObject event = e.toObject();
300     QString type = event["event_type"].toString();
301     QJsonObject source = event["source"].toObject();
302     if (source["type"] == "file") {
303       if (type == "ITEM_UPLOAD") {
304         // Add file.
305         MaybeAddFileEntry(source);
306       } else if (type == "ITEM_TRASH") {
307         // Delete file.
308         QUrl url;
309         url.setScheme("box");
310         url.setPath("/" + source["id"].toString());
311         Song song = library_backend_->GetSongByUrl(url);
312         if (song.is_valid()) {
313           library_backend_->DeleteSongs(SongList() << song);
314         }
315       }
316     }
317   }
318 }
319 
GetStreamingUrlFromSongId(const QString & id)320 QUrl BoxService::GetStreamingUrlFromSongId(const QString& id) {
321   EnsureConnected();
322   QNetworkReply* reply = FetchContentUrlForFile(id);
323   WaitForSignal(reply, SIGNAL(finished()));
324   reply->deleteLater();
325   QUrl real_url =
326       reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
327   return real_url;
328 }
329