1 /* This file is part of Clementine.
2    Copyright 2012-2014, John Maguire <john.maguire@gmail.com>
3    Copyright 2013, Martin Brodbeck <martin@brodbeck-online.de>
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 "dropboxservice.h"
21 
22 #include <QFileInfo>
23 #include <QTimer>
24 #include <QJsonDocument>
25 #include <QJsonObject>
26 #include <QJsonArray>
27 
28 #include "core/application.h"
29 #include "core/logging.h"
30 #include "core/network.h"
31 #include "core/player.h"
32 #include "core/utilities.h"
33 #include "core/waitforsignal.h"
34 #include "internet/core/oauthenticator.h"
35 #include "internet/dropbox/dropboxurlhandler.h"
36 #include "library/librarybackend.h"
37 #include "ui/iconloader.h"
38 
39 using Utilities::ParseRFC822DateTime;
40 
41 const char* DropboxService::kServiceName = "Dropbox";
42 const char* DropboxService::kSettingsGroup = "Dropbox";
43 
44 namespace {
45 
46 static const char* kServiceId = "dropbox";
47 
48 static const char* kMediaEndpoint =
49     "https://api.dropboxapi.com/2/files/get_temporary_link";
50 static const char* kListFolderEndpoint =
51     "https://api.dropboxapi.com/2/files/list_folder";
52 static const char* kListFolderContinueEndpoint =
53     "https://api.dropboxapi.com/2/files/list_folder/continue";
54 static const char* kLongPollEndpoint =
55     "https://notify.dropboxapi.com/2/files/list_folder/longpoll";
56 
57 }  // namespace
58 
DropboxService(Application * app,InternetModel * parent)59 DropboxService::DropboxService(Application* app, InternetModel* parent)
60     : CloudFileService(app, parent, kServiceName, kServiceId,
61                        IconLoader::Load("dropbox", IconLoader::Provider),
62                        SettingsDialog::Page_Dropbox),
63       network_(new NetworkAccessManager(this)) {
64   QSettings settings;
65   settings.beginGroup(kSettingsGroup);
66   // OAuth2 version of dropbox auth token.
67   access_token_ = settings.value("access_token2").toString();
68   app->player()->RegisterUrlHandler(new DropboxUrlHandler(this, this));
69 }
70 
has_credentials() const71 bool DropboxService::has_credentials() const {
72   return !access_token_.isEmpty();
73 }
74 
Connect()75 void DropboxService::Connect() {
76   if (has_credentials()) {
77     RequestFileList();
78   } else {
79     ShowSettingsDialog();
80   }
81 }
82 
AuthenticationFinished(OAuthenticator * authenticator)83 void DropboxService::AuthenticationFinished(OAuthenticator* authenticator) {
84   authenticator->deleteLater();
85 
86   access_token_ = authenticator->access_token();
87 
88   QSettings settings;
89   settings.beginGroup(kSettingsGroup);
90   settings.setValue("access_token2", access_token_);
91 
92   emit Connected();
93 
94   RequestFileList();
95 }
96 
GenerateAuthorisationHeader()97 QByteArray DropboxService::GenerateAuthorisationHeader() {
98   return QString("Bearer %1").arg(access_token_).toUtf8();
99 }
100 
RequestFileList()101 void DropboxService::RequestFileList() {
102   QSettings s;
103   s.beginGroup(kSettingsGroup);
104 
105   QString cursor = s.value("cursor", "").toString();
106 
107   if (cursor.isEmpty()) {
108     QUrl url = QUrl(QString(kListFolderEndpoint));
109 
110     QJsonObject json;
111     json.insert("path", "");
112     json.insert("recursive", true);
113     json.insert("include_deleted", true);
114 
115     QNetworkRequest request(url);
116     request.setRawHeader("Authorization", GenerateAuthorisationHeader());
117     request.setRawHeader("Content-Type", "application/json; charset=utf-8");
118 
119     QJsonDocument document(json);
120     QNetworkReply* reply = network_->post(request, document.toJson());
121     NewClosure(reply, SIGNAL(finished()), this,
122                SLOT(RequestFileListFinished(QNetworkReply*)), reply);
123   } else {
124     QUrl url = QUrl(kListFolderContinueEndpoint);
125     QJsonObject json;
126     json.insert("cursor", cursor);
127     QJsonDocument document(json);
128     QNetworkRequest request(url);
129     request.setRawHeader("Authorization", GenerateAuthorisationHeader());
130     request.setRawHeader("Content-Type", "application/json; charset=utf-8");
131     QNetworkReply* reply = network_->post(request, document.toJson());
132     NewClosure(reply, SIGNAL(finished()), this,
133                SLOT(RequestFileListFinished(QNetworkReply*)), reply);
134   }
135 }
136 
RequestFileListFinished(QNetworkReply * reply)137 void DropboxService::RequestFileListFinished(QNetworkReply* reply) {
138   reply->deleteLater();
139 
140   QJsonDocument document = QJsonDocument::fromBinaryData(reply->readAll());
141   QJsonObject json_response = document.object();
142 
143   if (json_response.contains("reset") && json_response["reset"].toBool()) {
144     qLog(Debug) << "Resetting Dropbox DB";
145     library_backend_->DeleteAll();
146   }
147 
148   QSettings settings;
149   settings.beginGroup(kSettingsGroup);
150   settings.setValue("cursor", json_response["cursor"].toString());
151 
152   QJsonArray contents = json_response["entries"].toArray();
153   qLog(Debug) << "File list found:" << contents.size();
154   for (const QJsonValue& c : contents) {
155     QJsonObject item = c.toObject();
156     QString path = item["path_lower"].toString();
157 
158     QUrl url;
159     url.setScheme("dropbox");
160     url.setPath(path);
161 
162     if (item[".tag"].toString() == "deleted") {
163       qLog(Debug) << "Deleting:" << url;
164       Song song = library_backend_->GetSongByUrl(url);
165       if (song.is_valid()) {
166         library_backend_->DeleteSongs(SongList() << song);
167       }
168       continue;
169     }
170 
171     if (item[".tag"].toString() == "folder") {
172       continue;
173     }
174 
175     if (ShouldIndexFile(url, GuessMimeTypeForFile(url.toString()))) {
176       QNetworkReply* reply = FetchContentUrl(url);
177       NewClosure(reply, SIGNAL(finished()), this,
178                  SLOT(FetchContentUrlFinished(QNetworkReply*, QVariantMap)),
179                  reply, item);
180     }
181   }
182 
183   if (json_response.contains("has_more") && json_response["has_more"].toBool()) {
184     QSettings s;
185     s.beginGroup(kSettingsGroup);
186     s.setValue("cursor", json_response["cursor"].toVariant());
187     RequestFileList();
188   } else {
189     // Long-poll wait for changes.
190     LongPollDelta();
191   }
192 }
193 
LongPollDelta()194 void DropboxService::LongPollDelta() {
195   if (!has_credentials()) {
196     // Might have been signed out by the user.
197     return;
198   }
199   QSettings s;
200   s.beginGroup(kSettingsGroup);
201 
202   QUrl request_url = QUrl(QString(kLongPollEndpoint));
203   QJsonObject json;
204   json.insert("cursor", s.value("cursor").toString());
205   json.insert("timeout", 30);
206   QNetworkRequest request(request_url);
207   request.setRawHeader("Content-Type", "application/json; charset=utf-8");
208   QJsonDocument document(json);
209   QNetworkReply* reply = network_->post(request, document.toJson());
210   NewClosure(reply, SIGNAL(finished()), this,
211              SLOT(LongPollFinished(QNetworkReply*)), reply);
212 }
213 
LongPollFinished(QNetworkReply * reply)214 void DropboxService::LongPollFinished(QNetworkReply* reply) {
215   reply->deleteLater();
216   QJsonObject json_response = QJsonDocument::fromBinaryData(reply->readAll()).object();
217   if (json_response["changes"].toBool()) {
218     // New changes, we should request deltas again.
219     qLog(Debug) << "Detected new dropbox changes; fetching...";
220     RequestFileList();
221   } else {
222     bool ok = false;
223     int backoff_secs = json_response["backoff"].toString().toInt(&ok);
224     backoff_secs = ok ? backoff_secs : 0;
225 
226     QTimer::singleShot(backoff_secs * 1000, this, SLOT(LongPollDelta()));
227   }
228 }
229 
FetchContentUrl(const QUrl & url)230 QNetworkReply* DropboxService::FetchContentUrl(const QUrl& url) {
231   QUrl request_url(kMediaEndpoint);
232   QJsonObject json;
233   json.insert("path", url.path());
234   QJsonDocument document(json);
235   QNetworkRequest request(request_url);
236   request.setRawHeader("Authorization", GenerateAuthorisationHeader());
237   request.setRawHeader("Content-Type", "application/json; charset=utf-8");
238   return network_->post(request, document.toJson());
239 }
240 
FetchContentUrlFinished(QNetworkReply * reply,const QVariantMap & data)241 void DropboxService::FetchContentUrlFinished(QNetworkReply* reply,
242                                              const QVariantMap& data) {
243   reply->deleteLater();
244   QJsonObject json_response = QJsonDocument::fromBinaryData(reply->readAll()).object();
245   QFileInfo info(data["path_lower"].toString());
246 
247   QUrl url;
248   url.setScheme("dropbox");
249   url.setPath(data["path_lower"].toString());
250 
251   Song song;
252   song.set_url(url);
253   song.set_etag(data["rev"].toString());
254   song.set_mtime(QDateTime::fromString(data["server_modified"].toString(),
255                                        Qt::ISODate).toTime_t());
256   song.set_title(info.fileName());
257   song.set_filesize(data["size"].toInt());
258   song.set_ctime(0);
259 
260   MaybeAddFileToDatabase(
261       song, GuessMimeTypeForFile(url.toString()),
262       QUrl::fromEncoded(json_response["link"].toVariant().toByteArray()),
263       QString());
264 }
265 
GetStreamingUrlFromSongId(const QUrl & url)266 QUrl DropboxService::GetStreamingUrlFromSongId(const QUrl& url) {
267   QNetworkReply* reply = FetchContentUrl(url);
268   WaitForSignal(reply, SIGNAL(finished()));
269 
270   QJsonObject json_response = QJsonDocument::fromJson(reply->readAll()).object();
271   return QUrl::fromEncoded(json_response["link"].toVariant().toByteArray());
272 }
273