Marble

OwncloudSyncBackend.cpp
1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2013 Utku Aydın <[email protected]>
4 //
5 
6 #include "OwncloudSyncBackend.h"
7 
8 #include "MarbleDirs.h"
9 #include "MarbleModel.h"
10 #include "MarbleDebug.h"
11 #include "GeoDocument.h"
12 #include "MarbleWidget.h"
13 #include "RenderPlugin.h"
14 #include "Route.h"
15 #include "RoutingModel.h"
16 #include "GeoDataParser.h"
17 #include "GeoDataFolder.h"
18 #include "RoutingManager.h"
19 #include "RouteItem.h"
20 #include "GeoDataDocument.h"
21 #include "CloudRouteModel.h"
22 #include "GeoDataPlacemark.h"
23 #include "CloudSyncManager.h"
24 #include "GeoDataExtendedData.h"
25 #include "GeoDataData.h"
26 
27 #include <QNetworkAccessManager>
28 #include <QNetworkRequest>
29 #include <QJsonDocument>
30 #include <QJsonArray>
31 #include <QJsonObject>
32 #include <QFileInfo>
33 #include <QBuffer>
34 #include <QDir>
35 
36 namespace Marble
37 {
38 
39 class Q_DECL_HIDDEN OwncloudSyncBackend::Private {
40 
41  public:
42  Private( CloudSyncManager* cloudSyncManager );
43 
44  QDir m_cacheDir;
45  QNetworkAccessManager m_network;
46  QNetworkReply *m_routeUploadReply;
47  QNetworkReply *m_routeListReply;
48  QNetworkReply *m_routeDownloadReply;
49  QNetworkReply *m_routeDeleteReply;
50  QNetworkReply *m_authReply;
51 
52  QVector<RouteItem> m_routeList;
53 
54  QString m_routeUploadEndpoint;
55  QString m_routeListEndpoint;
56  QString m_routeDownloadEndpoint;
57  QString m_routeDeleteEndpoint;
58  QString m_routePreviewEndpoint;
59 
60  CloudSyncManager* m_cloudSyncManager;
61  QUrl m_apiUrl;
62 };
63 
64 OwncloudSyncBackend::Private::Private( CloudSyncManager* cloudSyncManager ) :
65  m_cacheDir(MarbleDirs::localPath() + QLatin1String("/cloudsync/cache/routes/")),
66  m_network(),
67  m_routeUploadReply(),
68  m_routeListReply(),
69  m_routeDownloadReply(),
70  m_routeDeleteReply(),
71  m_authReply(),
72  m_routeList(),
73  // Route API endpoints
74  m_routeUploadEndpoint( "routes/create" ),
75  m_routeListEndpoint( "routes" ),
76  m_routeDownloadEndpoint( "routes" ),
77  m_routeDeleteEndpoint( "routes/delete" ),
78  m_routePreviewEndpoint( "routes/preview" ),
79  m_cloudSyncManager( cloudSyncManager )
80 {
81 }
82 
83 OwncloudSyncBackend::OwncloudSyncBackend( CloudSyncManager* cloudSyncManager ) :
84  d( new Private( cloudSyncManager ) )
85 {
86  connect(d->m_cloudSyncManager, SIGNAL(apiUrlChanged(QUrl)), this, SLOT(validateSettings()));
87 }
88 
89 OwncloudSyncBackend::~OwncloudSyncBackend()
90 {
91  delete d;
92 }
93 
94 void OwncloudSyncBackend::uploadRoute( const QString &timestamp )
95 {
96  QString word = "----MarbleCloudBoundary";
97  QString boundary = QString( "--%0" ).arg( word );
98  QNetworkRequest request( endpointUrl( d->m_routeUploadEndpoint ) );
99  request.setHeader( QNetworkRequest::ContentTypeHeader, QString( "multipart/form-data; boundary=%0" ).arg( word ) );
100 
101  QByteArray data;
102  data.append( QString( boundary + "\r\n" ).toUtf8() );
103 
104  // Timestamp part
105  data.append( "Content-Disposition: form-data; name=\"timestamp\"" );
106  data.append( "\r\n\r\n" );
107  data.append( QString( timestamp + "\r\n" ).toUtf8() );
108  data.append( QString( boundary + "\r\n" ).toUtf8() );
109 
110  // Name part
111  data.append( "Content-Disposition: form-data; name=\"name\"" );
112  data.append( "\r\n\r\n" );
113  data.append( routeName( timestamp ).toUtf8() );
114  data.append( "\r\n" );
115  data.append( QString( boundary + "\r\n" ).toUtf8() );
116 
117  QFile kmlFile( d->m_cacheDir.absolutePath() + QString( "/%0.kml" ).arg( timestamp ) );
118 
119  if( !kmlFile.open( QFile::ReadOnly ) ) {
120  mDebug() << "Could not open " << timestamp << ".kml. Either it has not been saved" <<
121  " to cache for upload or another application removed it from there.";
122  return;
123  }
124 
125  GeoDataParser parser(GeoData_KML);
126  if (!parser.read(&kmlFile)) {
127  mDebug() << "[OwncloudSyncBackend] KML file" << kmlFile.fileName()
128  << "is broken so I can't fill required properties";
129  return;
130  }
131 
132  GeoDataDocument *root = dynamic_cast<GeoDataDocument*>(parser.releaseDocument());
133  if (!root || root->size() < 2) {
134  mDebug() << "[OwncloudSyncBackend] Root document is broken";
135  return;
136  }
137 
138  GeoDataDocument *doc = geodata_cast<GeoDataDocument>(root->child(1));
139  if (!doc || doc->size() < 1) {
140  mDebug() << "[OwncloudSyncBackend] Tracking document is broken";
141  return;
142  }
143 
144  GeoDataPlacemark *placemark = geodata_cast<GeoDataPlacemark>(doc->child(0));
145  if (!placemark) {
146  mDebug() << "[OwncloudSyncBackend] Placemark is broken";
147  return;
148  }
149 
150  // Duration part
151  double duration =
152  QTime().secsTo(QTime::fromString(placemark->extendedData().value(QStringLiteral("duration")).value().toString(), Qt::ISODate)) / 60.0;
153  mDebug() << "[Owncloud] Duration on write is" << duration;
154  data.append( "Content-Disposition: form-data; name=\"duration\"" );
155  data.append( "\r\n\r\n" );
156  data.append( QString::number(duration).toUtf8() );
157  data.append( "\r\n" );
158  data.append( QString( boundary + "\r\n" ).toUtf8() );
159 
160  // Distance part
161  double distance =
162  placemark->extendedData().value(QStringLiteral("length")).value().toDouble();
163  mDebug() << "[Owncloud] Distance on write is" << distance;
164  data.append( "Content-Disposition: form-data; name=\"distance\"" );
165  data.append( "\r\n\r\n" );
166  data.append( QString::number(distance).toUtf8() );
167  data.append( "\r\n" );
168  data.append( QString( boundary + "\r\n" ).toUtf8() );
169 
170  // KML part
171  data.append( QString( "Content-Disposition: form-data; name=\"kml\"; filename=\"%0.kml\"" ).arg( timestamp ).toUtf8() );
172  data.append( "\r\n" );
173  data.append( "Content-Type: application/vnd.google-earth.kml+xml" );
174  data.append( "\r\n\r\n" );
175 
176  kmlFile.seek(0); // just to be sure
177  data.append( kmlFile.readAll() );
178  data.append( "\r\n" );
179  data.append( QString( boundary + "\r\n" ).toUtf8() );
180 
181  kmlFile.close();
182 
183  // Preview part
184  data.append( QString( "Content-Disposition: form-data; name=\"preview\"; filename=\"%0.jpg\"" ).arg( timestamp ).toUtf8() );
185  data.append( "\r\n" );
186  data.append( "Content-Type: image/jpg" );
187  data.append( "\r\n\r\n" );
188 
189  QByteArray previewBytes;
190  QBuffer previewBuffer( &previewBytes );
191  QPixmap preview = createPreview( timestamp );
192  preview.save( &previewBuffer, "JPG" );
193 
194  data.append( previewBytes );
195  data.append( "\r\n" );
196  data.append( QString( boundary + "\r\n" ).toUtf8() );
197 
198  d->m_routeUploadReply = d->m_network.post( request, data );
199  connect( d->m_routeUploadReply, SIGNAL(uploadProgress(qint64,qint64)), this, SIGNAL(routeUploadProgress(qint64,qint64)) );
200 }
201 
202 void OwncloudSyncBackend::downloadRouteList()
203 {
204  QNetworkRequest request( endpointUrl( d->m_routeListEndpoint ) );
205  d->m_routeListReply = d->m_network.get( request );
206  connect( d->m_routeListReply, SIGNAL(downloadProgress(qint64,qint64)), this, SIGNAL(routeListDownloadProgress(qint64,qint64)) );
207  connect( d->m_routeListReply, SIGNAL(finished()), this, SLOT(prepareRouteList()) );
208 }
209 
210 void OwncloudSyncBackend::downloadRoute( const QString &timestamp )
211 {
212  QNetworkRequest routeRequest( endpointUrl( d->m_routeDownloadEndpoint, timestamp ) );
213  d->m_routeDownloadReply = d->m_network.get( routeRequest );
214  connect( d->m_routeDownloadReply, SIGNAL(finished()), this, SLOT(saveDownloadedRoute()) );
215  connect( d->m_routeDownloadReply, SIGNAL(downloadProgress(qint64,qint64)), this, SIGNAL(routeDownloadProgress(qint64,qint64)) );
216 }
217 
218 void OwncloudSyncBackend::deleteRoute( const QString &timestamp )
219 {
220  QUrl url( endpointUrl( d->m_routeDeleteEndpoint, timestamp ) );
221  QNetworkRequest request( url );
222  d->m_routeDeleteReply = d->m_network.deleteResource( request );
223  connect( d->m_routeDeleteReply, SIGNAL(finished()), this, SIGNAL(routeDeleted()) );
224 }
225 
226 QPixmap OwncloudSyncBackend::createPreview( const QString &timestamp ) const
227 {
228  MarbleWidget mapWidget;
229  for( RenderPlugin* plugin: mapWidget.renderPlugins() ) {
230  plugin->setEnabled( false );
231  }
232 
233  mapWidget.setProjection( Mercator );
234  mapWidget.setMapThemeId(QStringLiteral("earth/openstreetmap/openstreetmap.dgml"));
235  mapWidget.resize( 512, 512 );
236 
237  RoutingManager* manager = mapWidget.model()->routingManager();
238  manager->loadRoute( d->m_cacheDir.absolutePath() + QString( "/%0.kml" ).arg( timestamp ) );
239  GeoDataLatLonBox const bbox = manager->routingModel()->route().bounds();
240 
241  if ( !bbox.isEmpty() ) {
242  mapWidget.centerOn( bbox );
243  }
244 
245  QPixmap pixmap = mapWidget.grab();
246  QDir( d->m_cacheDir.absolutePath() ).mkpath( "preview" );
247  pixmap.save(d->m_cacheDir.absolutePath() + QLatin1String("/preview/") + timestamp + QLatin1String(".jpg"));
248 
249  return pixmap;
250 }
251 
252 QString OwncloudSyncBackend::routeName( const QString &timestamp ) const
253 {
254  QFile file( d->m_cacheDir.absolutePath() + QString( "/%0.kml" ).arg( timestamp ) );
255  file.open( QFile::ReadOnly );
256 
257  GeoDataParser parser( GeoData_KML );
258  if( !parser.read( &file ) ) {
259  mDebug() << "Could not read " << timestamp << ".kml. Timestamp will be used as "
260  << "route name because of the problem";
261  return timestamp;
262  }
263  file.close();
264 
265  QString routeName;
266  GeoDocument *geoDoc = parser.releaseDocument();
267  GeoDataDocument *container = dynamic_cast<GeoDataDocument*>( geoDoc );
268  if ( container && container->size() > 0 ) {
269  GeoDataFolder *folder = container->folderList().at( 0 );
270  for ( GeoDataPlacemark *placemark: folder->placemarkList() ) {
271  routeName.append( placemark->name() );
272  routeName.append( " - " );
273  }
274  }
275 
276  return routeName.left( routeName.length() - 3 );
277 }
278 
279 void OwncloudSyncBackend::validateSettings()
280 {
281  if( d->m_cloudSyncManager->owncloudServer().size() > 0
282  && d->m_cloudSyncManager->owncloudUsername().size() > 0
283  && d->m_cloudSyncManager->owncloudPassword().size() > 0 )
284  {
285  QNetworkRequest request( endpointUrl( d->m_routeListEndpoint ) );
286  d->m_authReply = d->m_network.get( request );
287  connect( d->m_authReply, SIGNAL(finished()), this, SLOT(checkAuthReply()) );
288  connect( d->m_authReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(checkAuthError(QNetworkReply::NetworkError)) );
289  } else {
290  // no server, make the error field blank
291  d->m_cloudSyncManager->setStatus("", CloudSyncManager::Success);
292  }
293 }
294 
295 void OwncloudSyncBackend::checkAuthError(QNetworkReply::NetworkError error)
296 {
297  if ( error == QNetworkReply::HostNotFoundError ) {
298  QString const status = tr( "Server '%1' could not be reached" ).arg( d->m_cloudSyncManager->owncloudServer() );
299  d->m_cloudSyncManager->setStatus( status , CloudSyncManager::Error );
300  }
301 }
302 
303 void OwncloudSyncBackend::checkAuthReply()
304 {
305  int statusCode = d->m_authReply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt();
306 
307  if ( statusCode == 0 ) // request was cancelled
308  return;
309 
310  QString result = d->m_authReply->readAll();
311 
312  if (!result.startsWith(QLatin1Char('{'))) {
313  // not a JSON result
314  if (result.contains(QLatin1String("http://owncloud.org"))) {
315  // an owncloud login page was returned, marble app is not installed
316  d->m_cloudSyncManager->setStatus( tr( "The Marble app is not installed on the ownCloud server" ), CloudSyncManager::Error);
317  } else {
318  d->m_cloudSyncManager->setStatus( tr( "The server is not an ownCloud server" ), CloudSyncManager::Error);
319  }
320  } else if (result == QLatin1String("{\"message\":\"Current user is not logged in\"}") && statusCode == 401) {
321  // credentials were incorrect
322  d->m_cloudSyncManager->setStatus( tr( "Username or password are incorrect" ), CloudSyncManager::Error);
323  } else if ( result.contains("\"status\":\"success\"") && statusCode == 200 ) {
324  // credentials were correct
325  d->m_cloudSyncManager->setStatus( tr( "Login successful" ), CloudSyncManager::Success);
326  }
327 }
328 
329 void OwncloudSyncBackend::cancelUpload()
330 {
331  d->m_routeUploadReply->abort();
332 }
333 
334 void OwncloudSyncBackend::prepareRouteList()
335 {
336  QJsonDocument jsonDoc = QJsonDocument::fromJson(d->m_routeListReply->readAll());
337  QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data"));
338 
339  d->m_routeList.clear();
340 
341  if (dataValue.isArray()) {
342  QJsonArray dataArray = dataValue.toArray();
343  for (int index = 0; index < dataArray.size(); ++index) {
344  QJsonObject dataObject = dataArray[index].toObject();
345 
346  RouteItem route;
347  route.setIdentifier(dataObject.value(QStringLiteral("timestamp")).toString());
348  route.setName(dataObject.value(QStringLiteral("name")).toString() );
349  route.setDistance(dataObject.value(QStringLiteral("distance")).toString());
350  route.setDuration(dataObject.value(QStringLiteral("duration")).toString());
351  route.setPreviewUrl( endpointUrl( d->m_routePreviewEndpoint, route.identifier() ) );
352  route.setOnCloud( true );
353 
354  d->m_routeList.append( route );
355  }
356  }
357 
358  // FIXME Find why an empty item added to the end.
359  if( !d->m_routeList.isEmpty() ) {
360  d->m_routeList.remove( d->m_routeList.count() - 1 );
361  }
362 
363  emit routeListDownloaded( d->m_routeList );
364 }
365 
366 void OwncloudSyncBackend::saveDownloadedRoute()
367 {
368  QString timestamp = QFileInfo( d->m_routeDownloadReply->url().toString() ).fileName();
369 
370  bool pathCreated = d->m_cacheDir.mkpath( d->m_cacheDir.absolutePath() );
371  if ( !pathCreated ) {
372  mDebug() << "Couldn't create the path " << d->m_cacheDir.absolutePath() <<
373  ". Check if your user has sufficient permissions for this operation.";
374  }
375 
376  QString kmlFilePath = QString( "%0/%1.kml").arg( d->m_cacheDir.absolutePath(), timestamp );
377  QFile kmlFile( kmlFilePath );
378  bool fileOpened = kmlFile.open( QFile::ReadWrite );
379 
380  if ( !fileOpened ) {
381  mDebug() << "Failed to open file" << kmlFilePath << " for writing."
382  << " Its directory either is missing or is not writable.";
383  return;
384  }
385 
386  kmlFile.write( d->m_routeDownloadReply->readAll() );
387  kmlFile.close();
388 
389  QString previewPath = QString( "%0/preview/" ).arg( d->m_cacheDir.absolutePath() );
390  bool previewPathCreated = d->m_cacheDir.mkpath( previewPath );
391  if ( !previewPathCreated ) {
392  mDebug() << "Couldn't create the path " << previewPath <<
393  ". Check if your user has sufficient permissions for this operation.";
394  }
395 
396  QString previewFilePath = QString( "%0/preview/%1.jpg").arg( d->m_cacheDir.absolutePath(), timestamp );
397  QFile previewFile( previewFilePath );
398  bool previewFileOpened = previewFile.open( QFile::ReadWrite );
399 
400  if ( !previewFileOpened ) {
401  mDebug() << "Failed to open file" << previewFilePath << "for writing."
402  << " Its directory either is missing or is not writable.";
403  return;
404  }
405 
406  QPixmap preview = createPreview( timestamp );
407  preview.save( &previewFile, "JPG" );
408  previewFile.close();
409 
410  emit routeDownloaded();
411 }
412 
413 QUrl OwncloudSyncBackend::endpointUrl( const QString &endpoint ) const
414 {
415  const QString endpointUrl = d->m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint;
416  return QUrl( endpointUrl );
417 }
418 
419 QUrl OwncloudSyncBackend::endpointUrl( const QString &endpoint, const QString &parameter ) const
420 {
421  const QString endpointUrl = d->m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint + QLatin1Char('/') + parameter;
422  return QUrl( endpointUrl );
423 }
424 
425 void OwncloudSyncBackend::removeFromCache( const QDir &cacheDir, const QString &timestamp )
426 {
427  bool fileRemoved = QFile( QString( "%0/%1.kml" ).arg( cacheDir.absolutePath(), timestamp ) ).remove();
428  bool previewRemoved = QFile( QString( "%0/preview/%1.jpg" ).arg( cacheDir.absolutePath(), timestamp ) ).remove();
429  if ( !fileRemoved || !previewRemoved ) {
430  mDebug() << "Failed to remove locally cached route " << timestamp << ". It might "
431  "have been removed already, or its directory is missing / not writable.";
432  }
433 
434  emit removedFromCache( timestamp );
435 }
436 
437 }
438 
439 #include "moc_OwncloudSyncBackend.cpp"
QJsonObject object() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QString number(int n, int base)
QTime fromString(const QString &string, Qt::DateFormat format)
bool remove()
QByteArray & append(char ch)
void error(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
int secsTo(const QTime &t) const const
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
char * toString(const T &value)
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
bool save(const QString &fileName, const char *format, int quality) const const
int length() const const
bool mkpath(const QString &dirPath) const const
QJsonValue value(const QString &key) const const
Binds a QML item to a specific geodetic location in screen coordinates.
QString absolutePath() const const
QString fileName() const const
QJsonArray toArray() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString left(int n) const const
const QChar at(int position) const const
@ Mercator
Mercator projection.
Definition: MarbleGlobal.h:44
void append(const QJsonValue &value)
bool isArray() const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString & append(QChar ch)
QDebug mDebug()
a function to replace qDebug() in Marble library code
Definition: MarbleDebug.cpp:31
int size() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Thu May 26 2022 04:07:50 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.