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