File indexing completed on 2024-05-12 15:29:08
0001 // SPDX-License-Identifier: LGPL-2.1-or-later 0002 // 0003 // SPDX-FileCopyrightText: 2013 Utku Aydın <utkuaydin34@gmail.com> 0004 // 0005 0006 #include "OwncloudSyncBackend.h" 0007 0008 #include "MarbleDirs.h" 0009 #include "MarbleModel.h" 0010 #include "MarbleDebug.h" 0011 #include "GeoDocument.h" 0012 #include "MarbleWidget.h" 0013 #include "RenderPlugin.h" 0014 #include "Route.h" 0015 #include "RoutingModel.h" 0016 #include "GeoDataParser.h" 0017 #include "GeoDataFolder.h" 0018 #include "RoutingManager.h" 0019 #include "RouteItem.h" 0020 #include "GeoDataDocument.h" 0021 #include "CloudRouteModel.h" 0022 #include "GeoDataPlacemark.h" 0023 #include "CloudSyncManager.h" 0024 #include "GeoDataExtendedData.h" 0025 #include "GeoDataData.h" 0026 0027 #include <QNetworkAccessManager> 0028 #include <QNetworkRequest> 0029 #include <QJsonDocument> 0030 #include <QJsonArray> 0031 #include <QJsonObject> 0032 #include <QFileInfo> 0033 #include <QBuffer> 0034 #include <QDir> 0035 0036 namespace Marble 0037 { 0038 0039 class Q_DECL_HIDDEN OwncloudSyncBackend::Private { 0040 0041 public: 0042 Private( CloudSyncManager* cloudSyncManager ); 0043 0044 QDir m_cacheDir; 0045 QNetworkAccessManager m_network; 0046 QNetworkReply *m_routeUploadReply; 0047 QNetworkReply *m_routeListReply; 0048 QNetworkReply *m_routeDownloadReply; 0049 QNetworkReply *m_routeDeleteReply; 0050 QNetworkReply *m_authReply; 0051 0052 QVector<RouteItem> m_routeList; 0053 0054 QString m_routeUploadEndpoint; 0055 QString m_routeListEndpoint; 0056 QString m_routeDownloadEndpoint; 0057 QString m_routeDeleteEndpoint; 0058 QString m_routePreviewEndpoint; 0059 0060 CloudSyncManager* m_cloudSyncManager; 0061 QUrl m_apiUrl; 0062 }; 0063 0064 OwncloudSyncBackend::Private::Private( CloudSyncManager* cloudSyncManager ) : 0065 m_cacheDir(MarbleDirs::localPath() + QLatin1String("/cloudsync/cache/routes/")), 0066 m_network(), 0067 m_routeUploadReply(), 0068 m_routeListReply(), 0069 m_routeDownloadReply(), 0070 m_routeDeleteReply(), 0071 m_authReply(), 0072 m_routeList(), 0073 // Route API endpoints 0074 m_routeUploadEndpoint( "routes/create" ), 0075 m_routeListEndpoint( "routes" ), 0076 m_routeDownloadEndpoint( "routes" ), 0077 m_routeDeleteEndpoint( "routes/delete" ), 0078 m_routePreviewEndpoint( "routes/preview" ), 0079 m_cloudSyncManager( cloudSyncManager ) 0080 { 0081 } 0082 0083 OwncloudSyncBackend::OwncloudSyncBackend( CloudSyncManager* cloudSyncManager ) : 0084 d( new Private( cloudSyncManager ) ) 0085 { 0086 connect(d->m_cloudSyncManager, SIGNAL(apiUrlChanged(QUrl)), this, SLOT(validateSettings())); 0087 } 0088 0089 OwncloudSyncBackend::~OwncloudSyncBackend() 0090 { 0091 delete d; 0092 } 0093 0094 void OwncloudSyncBackend::uploadRoute( const QString ×tamp ) 0095 { 0096 QString word = "----MarbleCloudBoundary"; 0097 QString boundary = QString( "--%0" ).arg( word ); 0098 QNetworkRequest request( endpointUrl( d->m_routeUploadEndpoint ) ); 0099 request.setHeader( QNetworkRequest::ContentTypeHeader, QString( "multipart/form-data; boundary=%0" ).arg( word ) ); 0100 0101 QByteArray data; 0102 data.append( QString( boundary + "\r\n" ).toUtf8() ); 0103 0104 // Timestamp part 0105 data.append( "Content-Disposition: form-data; name=\"timestamp\"" ); 0106 data.append( "\r\n\r\n" ); 0107 data.append( QString( timestamp + "\r\n" ).toUtf8() ); 0108 data.append( QString( boundary + "\r\n" ).toUtf8() ); 0109 0110 // Name part 0111 data.append( "Content-Disposition: form-data; name=\"name\"" ); 0112 data.append( "\r\n\r\n" ); 0113 data.append( routeName( timestamp ).toUtf8() ); 0114 data.append( "\r\n" ); 0115 data.append( QString( boundary + "\r\n" ).toUtf8() ); 0116 0117 QFile kmlFile( d->m_cacheDir.absolutePath() + QString( "/%0.kml" ).arg( timestamp ) ); 0118 0119 if( !kmlFile.open( QFile::ReadOnly ) ) { 0120 mDebug() << "Could not open " << timestamp << ".kml. Either it has not been saved" << 0121 " to cache for upload or another application removed it from there."; 0122 return; 0123 } 0124 0125 GeoDataParser parser(GeoData_KML); 0126 if (!parser.read(&kmlFile)) { 0127 mDebug() << "[OwncloudSyncBackend] KML file" << kmlFile.fileName() 0128 << "is broken so I can't fill required properties"; 0129 return; 0130 } 0131 0132 GeoDataDocument *root = dynamic_cast<GeoDataDocument*>(parser.releaseDocument()); 0133 if (!root || root->size() < 2) { 0134 mDebug() << "[OwncloudSyncBackend] Root document is broken"; 0135 return; 0136 } 0137 0138 GeoDataDocument *doc = geodata_cast<GeoDataDocument>(root->child(1)); 0139 if (!doc || doc->size() < 1) { 0140 mDebug() << "[OwncloudSyncBackend] Tracking document is broken"; 0141 return; 0142 } 0143 0144 GeoDataPlacemark *placemark = geodata_cast<GeoDataPlacemark>(doc->child(0)); 0145 if (!placemark) { 0146 mDebug() << "[OwncloudSyncBackend] Placemark is broken"; 0147 return; 0148 } 0149 0150 // Duration part 0151 double duration = 0152 QTime().secsTo(QTime::fromString(placemark->extendedData().value(QStringLiteral("duration")).value().toString(), Qt::ISODate)) / 60.0; 0153 mDebug() << "[Owncloud] Duration on write is" << duration; 0154 data.append( "Content-Disposition: form-data; name=\"duration\"" ); 0155 data.append( "\r\n\r\n" ); 0156 data.append( QString::number(duration).toUtf8() ); 0157 data.append( "\r\n" ); 0158 data.append( QString( boundary + "\r\n" ).toUtf8() ); 0159 0160 // Distance part 0161 double distance = 0162 placemark->extendedData().value(QStringLiteral("length")).value().toDouble(); 0163 mDebug() << "[Owncloud] Distance on write is" << distance; 0164 data.append( "Content-Disposition: form-data; name=\"distance\"" ); 0165 data.append( "\r\n\r\n" ); 0166 data.append( QString::number(distance).toUtf8() ); 0167 data.append( "\r\n" ); 0168 data.append( QString( boundary + "\r\n" ).toUtf8() ); 0169 0170 // KML part 0171 data.append( QString( "Content-Disposition: form-data; name=\"kml\"; filename=\"%0.kml\"" ).arg( timestamp ).toUtf8() ); 0172 data.append( "\r\n" ); 0173 data.append( "Content-Type: application/vnd.google-earth.kml+xml" ); 0174 data.append( "\r\n\r\n" ); 0175 0176 kmlFile.seek(0); // just to be sure 0177 data.append( kmlFile.readAll() ); 0178 data.append( "\r\n" ); 0179 data.append( QString( boundary + "\r\n" ).toUtf8() ); 0180 0181 kmlFile.close(); 0182 0183 // Preview part 0184 data.append( QString( "Content-Disposition: form-data; name=\"preview\"; filename=\"%0.jpg\"" ).arg( timestamp ).toUtf8() ); 0185 data.append( "\r\n" ); 0186 data.append( "Content-Type: image/jpg" ); 0187 data.append( "\r\n\r\n" ); 0188 0189 QByteArray previewBytes; 0190 QBuffer previewBuffer( &previewBytes ); 0191 QPixmap preview = createPreview( timestamp ); 0192 preview.save( &previewBuffer, "JPG" ); 0193 0194 data.append( previewBytes ); 0195 data.append( "\r\n" ); 0196 data.append( QString( boundary + "\r\n" ).toUtf8() ); 0197 0198 d->m_routeUploadReply = d->m_network.post( request, data ); 0199 connect( d->m_routeUploadReply, SIGNAL(uploadProgress(qint64,qint64)), this, SIGNAL(routeUploadProgress(qint64,qint64)) ); 0200 } 0201 0202 void OwncloudSyncBackend::downloadRouteList() 0203 { 0204 QNetworkRequest request( endpointUrl( d->m_routeListEndpoint ) ); 0205 d->m_routeListReply = d->m_network.get( request ); 0206 connect( d->m_routeListReply, SIGNAL(downloadProgress(qint64,qint64)), this, SIGNAL(routeListDownloadProgress(qint64,qint64)) ); 0207 connect( d->m_routeListReply, SIGNAL(finished()), this, SLOT(prepareRouteList()) ); 0208 } 0209 0210 void OwncloudSyncBackend::downloadRoute( const QString ×tamp ) 0211 { 0212 QNetworkRequest routeRequest( endpointUrl( d->m_routeDownloadEndpoint, timestamp ) ); 0213 d->m_routeDownloadReply = d->m_network.get( routeRequest ); 0214 connect( d->m_routeDownloadReply, SIGNAL(finished()), this, SLOT(saveDownloadedRoute()) ); 0215 connect( d->m_routeDownloadReply, SIGNAL(downloadProgress(qint64,qint64)), this, SIGNAL(routeDownloadProgress(qint64,qint64)) ); 0216 } 0217 0218 void OwncloudSyncBackend::deleteRoute( const QString ×tamp ) 0219 { 0220 QUrl url( endpointUrl( d->m_routeDeleteEndpoint, timestamp ) ); 0221 QNetworkRequest request( url ); 0222 d->m_routeDeleteReply = d->m_network.deleteResource( request ); 0223 connect( d->m_routeDeleteReply, SIGNAL(finished()), this, SIGNAL(routeDeleted()) ); 0224 } 0225 0226 QPixmap OwncloudSyncBackend::createPreview( const QString ×tamp ) const 0227 { 0228 MarbleWidget mapWidget; 0229 for( RenderPlugin* plugin: mapWidget.renderPlugins() ) { 0230 plugin->setEnabled( false ); 0231 } 0232 0233 mapWidget.setProjection( Mercator ); 0234 mapWidget.setMapThemeId(QStringLiteral("earth/openstreetmap/openstreetmap.dgml")); 0235 mapWidget.resize( 512, 512 ); 0236 0237 RoutingManager* manager = mapWidget.model()->routingManager(); 0238 manager->loadRoute( d->m_cacheDir.absolutePath() + QString( "/%0.kml" ).arg( timestamp ) ); 0239 GeoDataLatLonBox const bbox = manager->routingModel()->route().bounds(); 0240 0241 if ( !bbox.isEmpty() ) { 0242 mapWidget.centerOn( bbox ); 0243 } 0244 0245 QPixmap pixmap = mapWidget.grab(); 0246 QDir( d->m_cacheDir.absolutePath() ).mkpath( "preview" ); 0247 pixmap.save(d->m_cacheDir.absolutePath() + QLatin1String("/preview/") + timestamp + QLatin1String(".jpg")); 0248 0249 return pixmap; 0250 } 0251 0252 QString OwncloudSyncBackend::routeName( const QString ×tamp ) const 0253 { 0254 QFile file( d->m_cacheDir.absolutePath() + QString( "/%0.kml" ).arg( timestamp ) ); 0255 file.open( QFile::ReadOnly ); 0256 0257 GeoDataParser parser( GeoData_KML ); 0258 if( !parser.read( &file ) ) { 0259 mDebug() << "Could not read " << timestamp << ".kml. Timestamp will be used as " 0260 << "route name because of the problem"; 0261 return timestamp; 0262 } 0263 file.close(); 0264 0265 QString routeName; 0266 GeoDocument *geoDoc = parser.releaseDocument(); 0267 GeoDataDocument *container = dynamic_cast<GeoDataDocument*>( geoDoc ); 0268 if ( container && container->size() > 0 ) { 0269 GeoDataFolder *folder = container->folderList().at( 0 ); 0270 for ( GeoDataPlacemark *placemark: folder->placemarkList() ) { 0271 routeName.append( placemark->name() ); 0272 routeName.append( " - " ); 0273 } 0274 } 0275 0276 return routeName.left( routeName.length() - 3 ); 0277 } 0278 0279 void OwncloudSyncBackend::validateSettings() 0280 { 0281 if( d->m_cloudSyncManager->owncloudServer().size() > 0 0282 && d->m_cloudSyncManager->owncloudUsername().size() > 0 0283 && d->m_cloudSyncManager->owncloudPassword().size() > 0 ) 0284 { 0285 QNetworkRequest request( endpointUrl( d->m_routeListEndpoint ) ); 0286 d->m_authReply = d->m_network.get( request ); 0287 connect( d->m_authReply, SIGNAL(finished()), this, SLOT(checkAuthReply()) ); 0288 connect( d->m_authReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(checkAuthError(QNetworkReply::NetworkError)) ); 0289 } else { 0290 // no server, make the error field blank 0291 d->m_cloudSyncManager->setStatus("", CloudSyncManager::Success); 0292 } 0293 } 0294 0295 void OwncloudSyncBackend::checkAuthError(QNetworkReply::NetworkError error) 0296 { 0297 if ( error == QNetworkReply::HostNotFoundError ) { 0298 QString const status = tr( "Server '%1' could not be reached" ).arg( d->m_cloudSyncManager->owncloudServer() ); 0299 d->m_cloudSyncManager->setStatus( status , CloudSyncManager::Error ); 0300 } 0301 } 0302 0303 void OwncloudSyncBackend::checkAuthReply() 0304 { 0305 int statusCode = d->m_authReply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); 0306 0307 if ( statusCode == 0 ) // request was cancelled 0308 return; 0309 0310 QString result = d->m_authReply->readAll(); 0311 0312 if (!result.startsWith(QLatin1Char('{'))) { 0313 // not a JSON result 0314 if (result.contains(QLatin1String("http://owncloud.org"))) { 0315 // an owncloud login page was returned, marble app is not installed 0316 d->m_cloudSyncManager->setStatus( tr( "The Marble app is not installed on the ownCloud server" ), CloudSyncManager::Error); 0317 } else { 0318 d->m_cloudSyncManager->setStatus( tr( "The server is not an ownCloud server" ), CloudSyncManager::Error); 0319 } 0320 } else if (result == QLatin1String("{\"message\":\"Current user is not logged in\"}") && statusCode == 401) { 0321 // credentials were incorrect 0322 d->m_cloudSyncManager->setStatus( tr( "Username or password are incorrect" ), CloudSyncManager::Error); 0323 } else if ( result.contains("\"status\":\"success\"") && statusCode == 200 ) { 0324 // credentials were correct 0325 d->m_cloudSyncManager->setStatus( tr( "Login successful" ), CloudSyncManager::Success); 0326 } 0327 } 0328 0329 void OwncloudSyncBackend::cancelUpload() 0330 { 0331 d->m_routeUploadReply->abort(); 0332 } 0333 0334 void OwncloudSyncBackend::prepareRouteList() 0335 { 0336 QJsonDocument jsonDoc = QJsonDocument::fromJson(d->m_routeListReply->readAll()); 0337 QJsonValue dataValue = jsonDoc.object().value(QStringLiteral("data")); 0338 0339 d->m_routeList.clear(); 0340 0341 if (dataValue.isArray()) { 0342 QJsonArray dataArray = dataValue.toArray(); 0343 for (int index = 0; index < dataArray.size(); ++index) { 0344 QJsonObject dataObject = dataArray[index].toObject(); 0345 0346 RouteItem route; 0347 route.setIdentifier(dataObject.value(QStringLiteral("timestamp")).toString()); 0348 route.setName(dataObject.value(QStringLiteral("name")).toString() ); 0349 route.setDistance(dataObject.value(QStringLiteral("distance")).toString()); 0350 route.setDuration(dataObject.value(QStringLiteral("duration")).toString()); 0351 route.setPreviewUrl( endpointUrl( d->m_routePreviewEndpoint, route.identifier() ) ); 0352 route.setOnCloud( true ); 0353 0354 d->m_routeList.append( route ); 0355 } 0356 } 0357 0358 // FIXME Find why an empty item added to the end. 0359 if( !d->m_routeList.isEmpty() ) { 0360 d->m_routeList.remove( d->m_routeList.count() - 1 ); 0361 } 0362 0363 emit routeListDownloaded( d->m_routeList ); 0364 } 0365 0366 void OwncloudSyncBackend::saveDownloadedRoute() 0367 { 0368 QString timestamp = QFileInfo( d->m_routeDownloadReply->url().toString() ).fileName(); 0369 0370 bool pathCreated = d->m_cacheDir.mkpath( d->m_cacheDir.absolutePath() ); 0371 if ( !pathCreated ) { 0372 mDebug() << "Couldn't create the path " << d->m_cacheDir.absolutePath() << 0373 ". Check if your user has sufficient permissions for this operation."; 0374 } 0375 0376 QString kmlFilePath = QString( "%0/%1.kml").arg( d->m_cacheDir.absolutePath(), timestamp ); 0377 QFile kmlFile( kmlFilePath ); 0378 bool fileOpened = kmlFile.open( QFile::ReadWrite ); 0379 0380 if ( !fileOpened ) { 0381 mDebug() << "Failed to open file" << kmlFilePath << " for writing." 0382 << " Its directory either is missing or is not writable."; 0383 return; 0384 } 0385 0386 kmlFile.write( d->m_routeDownloadReply->readAll() ); 0387 kmlFile.close(); 0388 0389 QString previewPath = QString( "%0/preview/" ).arg( d->m_cacheDir.absolutePath() ); 0390 bool previewPathCreated = d->m_cacheDir.mkpath( previewPath ); 0391 if ( !previewPathCreated ) { 0392 mDebug() << "Couldn't create the path " << previewPath << 0393 ". Check if your user has sufficient permissions for this operation."; 0394 } 0395 0396 QString previewFilePath = QString( "%0/preview/%1.jpg").arg( d->m_cacheDir.absolutePath(), timestamp ); 0397 QFile previewFile( previewFilePath ); 0398 bool previewFileOpened = previewFile.open( QFile::ReadWrite ); 0399 0400 if ( !previewFileOpened ) { 0401 mDebug() << "Failed to open file" << previewFilePath << "for writing." 0402 << " Its directory either is missing or is not writable."; 0403 return; 0404 } 0405 0406 QPixmap preview = createPreview( timestamp ); 0407 preview.save( &previewFile, "JPG" ); 0408 previewFile.close(); 0409 0410 emit routeDownloaded(); 0411 } 0412 0413 QUrl OwncloudSyncBackend::endpointUrl( const QString &endpoint ) const 0414 { 0415 const QString endpointUrl = d->m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint; 0416 return QUrl( endpointUrl ); 0417 } 0418 0419 QUrl OwncloudSyncBackend::endpointUrl( const QString &endpoint, const QString ¶meter ) const 0420 { 0421 const QString endpointUrl = d->m_cloudSyncManager->apiUrl().toString() + QLatin1Char('/') + endpoint + QLatin1Char('/') + parameter; 0422 return QUrl( endpointUrl ); 0423 } 0424 0425 void OwncloudSyncBackend::removeFromCache( const QDir &cacheDir, const QString ×tamp ) 0426 { 0427 bool fileRemoved = QFile( QString( "%0/%1.kml" ).arg( cacheDir.absolutePath(), timestamp ) ).remove(); 0428 bool previewRemoved = QFile( QString( "%0/preview/%1.jpg" ).arg( cacheDir.absolutePath(), timestamp ) ).remove(); 0429 if ( !fileRemoved || !previewRemoved ) { 0430 mDebug() << "Failed to remove locally cached route " << timestamp << ". It might " 0431 "have been removed already, or its directory is missing / not writable."; 0432 } 0433 0434 emit removedFromCache( timestamp ); 0435 } 0436 0437 } 0438 0439 #include "moc_OwncloudSyncBackend.cpp"