File indexing completed on 2024-05-05 03:49:15

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 &timestamp )
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 &timestamp )
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 &timestamp )
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 &timestamp ) 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 &timestamp ) 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 &parameter ) 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 &timestamp )
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"