File indexing completed on 2024-04-28 15:15:59

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 //
0003 // SPDX-FileCopyrightText: 2012 Dennis Nienhüser <nienhueser@kde.org>
0004 //
0005 
0006 #include "NewstuffModel.h"
0007 
0008 #include "MarbleDebug.h"
0009 #include "MarbleDirs.h"
0010 #include "MarbleZipReader.h"
0011 
0012 #include <QUrl>
0013 #include <QVector>
0014 #include <QTemporaryFile>
0015 #include <QDir>
0016 #include <QFuture>
0017 #include <QPair>
0018 #include <QFutureWatcher>
0019 #include <QtConcurrentRun>
0020 #include <QProcessEnvironment>
0021 #include <QMutexLocker>
0022 #include <QIcon>
0023 #include <QNetworkAccessManager>
0024 #include <QNetworkReply>
0025 #include <QDomDocument>
0026 
0027 namespace Marble
0028 {
0029 
0030 class NewstuffItem
0031 {
0032 public:
0033     QString m_category;
0034     QString m_name;
0035     QString m_author;
0036     QString m_license;
0037     QString m_summary;
0038     QString m_version;
0039     QString m_releaseDate;
0040     QUrl m_previewUrl;
0041     QIcon m_preview;
0042     QUrl m_payloadUrl;
0043     QDomNode m_registryNode;
0044     qint64 m_payloadSize;
0045     qint64 m_downloadedSize;
0046 
0047     NewstuffItem();
0048 
0049     QString installedVersion() const;
0050     QString installedReleaseDate() const;
0051     bool isUpgradable() const;
0052     QStringList installedFiles() const;
0053 
0054     static bool deeperThan( const QString &one, const QString &two );
0055 };
0056 
0057 class FetchPreviewJob;
0058 
0059 class NewstuffModelPrivate
0060 {
0061 public:
0062     enum NodeAction {
0063         Append,
0064         Replace
0065     };
0066 
0067     enum UserAction {
0068         Install,
0069         Uninstall
0070     };
0071 
0072     typedef QPair<int, UserAction> Action;
0073 
0074     NewstuffModel* m_parent;
0075 
0076     QVector<NewstuffItem> m_items;
0077 
0078     QNetworkAccessManager m_networkAccessManager;
0079 
0080     QString m_provider;
0081 
0082     QMap<QNetworkReply *, FetchPreviewJob *> m_networkJobs;
0083 
0084     QNetworkReply* m_currentReply;
0085 
0086     QTemporaryFile* m_currentFile;
0087 
0088     QString m_targetDirectory;
0089 
0090     QString m_registryFile;
0091 
0092     NewstuffModel::IdTag m_idTag;
0093 
0094     QDomDocument m_registryDocument;
0095 
0096     QDomElement m_root;
0097 
0098     Action m_currentAction;
0099 
0100     QProcess* m_unpackProcess;
0101 
0102     QMutex m_mutex;
0103 
0104     QList<Action> m_actionQueue;
0105 
0106     QHash<int, QByteArray> m_roleNames;
0107 
0108     explicit NewstuffModelPrivate( NewstuffModel* parent );
0109 
0110     QIcon preview( int index );
0111     void setPreview( int index, const QIcon &previewIcon );
0112 
0113     void handleProviderData( QNetworkReply* reply );
0114 
0115     static bool canExecute( const QString &executable );
0116 
0117     void installMap();
0118 
0119     void updateModel();
0120 
0121     void saveRegistry();
0122 
0123     void uninstall( int index );
0124 
0125     static void changeNode( QDomNode &node, QDomDocument &domDocument, const QString &key, const QString &value, NodeAction action );
0126 
0127     void readInstalledFiles( QStringList* target, const QDomNode &node );
0128 
0129     void processQueue();
0130 
0131     static NewstuffItem importNode( const QDomNode &node );
0132 
0133     bool isTransitioning( int index ) const;
0134 
0135     void unzip();
0136 
0137     void updateRegistry(const QStringList &files);
0138 
0139     template<class T>
0140     static void readValue( const QDomNode &node, const QString &key, T* target );
0141 };
0142 
0143 class FetchPreviewJob
0144 {
0145 public:
0146     FetchPreviewJob( NewstuffModelPrivate *modelPrivate, int index );
0147 
0148     void run( const QByteArray &data );
0149 
0150 private:
0151     NewstuffModelPrivate *const m_modelPrivate;
0152     const int m_index;
0153 };
0154 
0155 NewstuffItem::NewstuffItem() : m_payloadSize( -2 ), m_downloadedSize( 0 )
0156 {
0157     // nothing to do
0158 }
0159 
0160 QString NewstuffItem::installedVersion() const
0161 {
0162     QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName( "version" );
0163     if ( nodes.size() == 1 ) {
0164         return nodes.at( 0 ).toElement().text();
0165     }
0166 
0167     return QString();
0168 }
0169 
0170 QString NewstuffItem::installedReleaseDate() const
0171 {
0172     QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName( "releasedate" );
0173     if ( nodes.size() == 1 ) {
0174         return nodes.at( 0 ).toElement().text();
0175     }
0176 
0177     return QString();
0178 }
0179 
0180 bool NewstuffItem::isUpgradable() const
0181 {
0182     bool installedOk, remoteOk;
0183     double const installed = installedVersion().toDouble( &installedOk );
0184     double const remote= m_version.toDouble( &remoteOk );
0185     return installedOk && remoteOk && remote > installed;
0186 }
0187 
0188 QStringList NewstuffItem::installedFiles() const
0189 {
0190     QStringList result;
0191     QDomNodeList const nodes = m_registryNode.toElement().elementsByTagName( "installedfile" );
0192     for ( int i=0; i<nodes.count(); ++i ) {
0193         result << nodes.at( i ).toElement().text();
0194     }
0195     return result;
0196 }
0197 
0198 bool NewstuffItem::deeperThan(const QString &one, const QString &two)
0199 {
0200     return one.length() > two.length();
0201 }
0202 
0203 FetchPreviewJob::FetchPreviewJob( NewstuffModelPrivate *modelPrivate, int index ) :
0204     m_modelPrivate( modelPrivate ),
0205     m_index( index )
0206 {
0207 }
0208 
0209 void FetchPreviewJob::run( const QByteArray &data )
0210 {
0211     const QImage image = QImage::fromData( data );
0212 
0213     if ( image.isNull() )
0214         return;
0215 
0216     const QPixmap pixmap = QPixmap::fromImage( image );
0217     const QIcon previewIcon( pixmap );
0218     m_modelPrivate->setPreview( m_index, previewIcon );
0219 }
0220 
0221 NewstuffModelPrivate::NewstuffModelPrivate( NewstuffModel* parent ) : m_parent( parent ),
0222     m_networkAccessManager( nullptr ), m_currentReply( nullptr ), m_currentFile( nullptr ),
0223     m_idTag( NewstuffModel::PayloadTag ), m_currentAction( -1, Install ), m_unpackProcess( nullptr )
0224 {
0225     // nothing to do
0226 }
0227 
0228 QIcon NewstuffModelPrivate::preview( int index )
0229 {
0230     if ( m_items.at( index ).m_preview.isNull() ) {
0231         QPixmap dummyPixmap( 136, 136 );
0232         dummyPixmap.fill( Qt::transparent );
0233         setPreview( index, QIcon( dummyPixmap ) );
0234         QNetworkReply *reply = m_networkAccessManager.get( QNetworkRequest( m_items.at( index ).m_previewUrl ) );
0235         m_networkJobs.insert( reply, new FetchPreviewJob( this, index ) );
0236     }
0237 
0238     Q_ASSERT( !m_items.at( index ).m_preview.isNull() );
0239 
0240     return m_items.at( index ).m_preview;
0241 }
0242 
0243 void NewstuffModelPrivate::setPreview( int index, const QIcon &previewIcon )
0244 {
0245     NewstuffItem &item = m_items[index];
0246     item.m_preview = previewIcon;
0247     const QModelIndex affected = m_parent->index( index );
0248     emit m_parent->dataChanged( affected, affected );
0249 }
0250 
0251 void NewstuffModelPrivate::handleProviderData(QNetworkReply *reply)
0252 {
0253     if ( reply->operation() == QNetworkAccessManager::HeadOperation ) {
0254         const QVariant redirectionAttribute = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
0255         if ( !redirectionAttribute.isNull() ) {
0256             for ( int i=0; i<m_items.size(); ++i ) {
0257                 NewstuffItem &item = m_items[i];
0258                 if ( item.m_payloadUrl == reply->url() ) {
0259                     item.m_payloadUrl = redirectionAttribute.toUrl();
0260                 }
0261             }
0262             m_networkAccessManager.head( QNetworkRequest( redirectionAttribute.toUrl() ) );
0263             return;
0264         }
0265 
0266         QVariant const size = reply->header( QNetworkRequest::ContentLengthHeader );
0267         if ( size.isValid() ) {
0268             qint64 length = size.value<qint64>();
0269             for ( int i=0; i<m_items.size(); ++i ) {
0270                 NewstuffItem &item = m_items[i];
0271                 if ( item.m_payloadUrl == reply->url() ) {
0272                     item.m_payloadSize = length;
0273                     QModelIndex const affected = m_parent->index( i );
0274                     emit m_parent->dataChanged( affected, affected );
0275                 }
0276             }
0277         }
0278         return;
0279     }
0280 
0281     FetchPreviewJob *const job = m_networkJobs.take( reply );
0282 
0283     // check if we are redirected
0284     const QVariant redirectionAttribute = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
0285     if ( !redirectionAttribute.isNull() ) {
0286         QNetworkReply *redirectReply = m_networkAccessManager.get( QNetworkRequest( QUrl( redirectionAttribute.toUrl() ) ) );
0287         if ( job ) {
0288             m_networkJobs.insert( redirectReply, job );
0289         }
0290         return;
0291     }
0292 
0293     if ( job ) {
0294         job->run( reply->readAll() );
0295         delete job;
0296         return;
0297     }
0298 
0299     QDomDocument xml;
0300     if ( !xml.setContent( reply->readAll() ) ) {
0301         mDebug() << "Cannot parse newstuff xml data ";
0302         return;
0303     }
0304 
0305     m_items.clear();
0306 
0307     QDomElement root = xml.documentElement();
0308     QDomNodeList items = root.elementsByTagName( "stuff" );
0309     for (int i=0 ; i < items.length(); ++i ) {
0310         m_items << importNode( items.item( i ) );
0311     }
0312 
0313     updateModel();
0314 }
0315 
0316 bool NewstuffModelPrivate::canExecute( const QString &executable )
0317 {
0318     QString path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("PATH"), QStringLiteral("/usr/local/bin:/usr/bin:/bin"));
0319     for( const QString &dir: path.split( QLatin1Char( ':' ) ) ) {
0320         QFileInfo application( QDir( dir ), executable );
0321         if ( application.exists() ) {
0322             return true;
0323         }
0324     }
0325 
0326     return false;
0327 }
0328 
0329 void NewstuffModelPrivate::installMap()
0330 {
0331     if ( m_unpackProcess ) {
0332         m_unpackProcess->close();
0333         delete m_unpackProcess;
0334         m_unpackProcess = nullptr;
0335     } else if ( m_currentFile->fileName().endsWith( QLatin1String( "zip" ) ) ) {
0336         unzip();
0337     }
0338     else if ( m_currentFile->fileName().endsWith( QLatin1String( "tar.gz" ) ) && canExecute( "tar" ) ) {
0339         m_unpackProcess = new QProcess;
0340         QObject::connect( m_unpackProcess, SIGNAL(finished(int)),
0341                           m_parent, SLOT(contentsListed(int)) );
0342         QStringList arguments = QStringList() << "-t" << "-z" << "-f" << m_currentFile->fileName();
0343         m_unpackProcess->setWorkingDirectory( m_targetDirectory );
0344         m_unpackProcess->start( "tar", arguments );
0345     } else {
0346         if ( !m_currentFile->fileName().endsWith( QLatin1String( "tar.gz" ) ) ) {
0347             mDebug() << "Can only handle tar.gz files";
0348         } else {
0349             mDebug() << "Cannot extract archive: tar executable not found in PATH.";
0350         }
0351     }
0352 }
0353 
0354 void NewstuffModelPrivate::unzip()
0355 {
0356     MarbleZipReader zipReader(m_currentFile->fileName());
0357     QStringList files;
0358     for(const MarbleZipReader::FileInfo &fileInfo: zipReader.fileInfoList()) {
0359         files << fileInfo.filePath;
0360     }
0361     updateRegistry(files);
0362     zipReader.extractAll(m_targetDirectory);
0363     m_parent->mapInstalled(0);
0364 }
0365 
0366 void NewstuffModelPrivate::updateModel()
0367 {
0368     QDomNodeList items = m_root.elementsByTagName( "stuff" );
0369     for (int i=0 ; i < items.length(); ++i ) {
0370         QString const key = m_idTag == NewstuffModel::PayloadTag ? "payload" : "name";
0371         QDomNodeList matches = items.item( i ).toElement().elementsByTagName( key );
0372         if ( matches.size() == 1 ) {
0373             QString const value = matches.at( 0 ).toElement().text();
0374             bool found = false;
0375             for ( int j=0; j<m_items.size() && !found; ++j ) {
0376                 NewstuffItem &item = m_items[j];
0377                 if ( m_idTag == NewstuffModel::PayloadTag && item.m_payloadUrl.toString() == value ) {
0378                     item.m_registryNode = items.item( i );
0379                     found = true;
0380                 }
0381                 if ( m_idTag == NewstuffModel::NameTag && item.m_name == value ) {
0382                     item.m_registryNode = items.item( i );
0383                     found = true;
0384                 }
0385             }
0386 
0387             if ( !found ) {
0388                 // Not found in newstuff or newstuff not there yet
0389                 NewstuffItem item = importNode( items.item( i ) );
0390                 if ( m_idTag == NewstuffModel::PayloadTag ) {
0391                     item.m_registryNode = items.item( i );
0392                 } else if ( m_idTag == NewstuffModel::NameTag ) {
0393                     item.m_registryNode = items.item( i );
0394                 }
0395                 m_items << item;
0396             }
0397         }
0398     }
0399 
0400     m_parent->beginResetModel();
0401     m_parent->endResetModel();
0402 }
0403 
0404 void NewstuffModelPrivate::saveRegistry()
0405 {
0406     QFile output( m_registryFile );
0407     if ( !output.open( QFile::WriteOnly ) ) {
0408         mDebug() << "Cannot open " << m_registryFile << " for writing";
0409     } else {
0410         QTextStream outStream( &output );
0411         outStream << m_registryDocument.toString( 2 );
0412         outStream.flush();
0413         output.close();
0414     }
0415 }
0416 
0417 void NewstuffModelPrivate::uninstall( int index )
0418 {
0419     // Delete all files first, then directories (deeper ones first)
0420 
0421     QStringList directories;
0422     QStringList const files = m_items[index].installedFiles();
0423     for( const QString &file: files ) {
0424         if (file.endsWith(QLatin1Char('/'))) {
0425             directories << file;
0426         } else {
0427             QFile::remove( file );
0428         }
0429     }
0430 
0431     std::sort( directories.begin(), directories.end(), NewstuffItem::deeperThan );
0432     for( const QString &dir: directories ) {
0433         QDir::root().rmdir( dir );
0434     }
0435 
0436     m_items[index].m_registryNode.parentNode().removeChild( m_items[index].m_registryNode );
0437     m_items[index].m_registryNode.clear();
0438     saveRegistry();
0439 }
0440 
0441 void NewstuffModelPrivate::changeNode( QDomNode &node, QDomDocument &domDocument, const QString &key, const QString &value, NodeAction action )
0442 {
0443     if ( action == Append ) {
0444         QDomNode newNode = node.appendChild( domDocument.createElement( key ) );
0445         newNode.appendChild( domDocument.createTextNode( value ) );
0446     } else {
0447         QDomNode oldNode = node.namedItem( key );
0448         if ( !oldNode.isNull() ) {
0449             oldNode.removeChild( oldNode.firstChild() );
0450             oldNode.appendChild( domDocument.createTextNode( value ) );
0451         }
0452     }
0453 }
0454 
0455 template<class T>
0456 void NewstuffModelPrivate::readValue( const QDomNode &node, const QString &key, T* target )
0457 {
0458     QDomNodeList matches = node.toElement().elementsByTagName( key );
0459     if ( matches.size() == 1 ) {
0460         *target = T(matches.at( 0 ).toElement().text());
0461     } else {
0462         for ( int i=0; i<matches.size(); ++i ) {
0463             if ( matches.at( i ).attributes().contains(QStringLiteral("lang")) &&
0464                  matches.at( i ).attributes().namedItem(QStringLiteral("lang")).toAttr().value() == QLatin1String("en")) {
0465                 *target = T(matches.at( i ).toElement().text());
0466                 return;
0467             }
0468         }
0469     }
0470 }
0471 
0472 NewstuffModel::NewstuffModel( QObject *parent ) :
0473     QAbstractListModel( parent ), d( new NewstuffModelPrivate( this ) )
0474 {
0475     setTargetDirectory(MarbleDirs::localPath() + QLatin1String("/maps"));
0476     // no default registry file
0477 
0478     connect( &d->m_networkAccessManager, SIGNAL(finished(QNetworkReply*)),
0479              this, SLOT(handleProviderData(QNetworkReply*)) );
0480 
0481     QHash<int,QByteArray> roles;
0482     roles[Qt::DisplayRole] = "display";
0483     roles[Name] = "name";
0484     roles[Author] = "author";
0485     roles[License] = "license";
0486     roles[Summary] = "summary";
0487     roles[Version] = "version";
0488     roles[ReleaseDate] = "releasedate";
0489     roles[Preview] = "preview";
0490     roles[Payload] = "payload";
0491     roles[InstalledVersion] = "installedversion";
0492     roles[InstalledReleaseDate] = "installedreleasedate";
0493     roles[InstalledFiles] = "installedfiles";
0494     roles[IsInstalled] = "installed";
0495     roles[IsUpgradable] = "upgradable";
0496     roles[Category] = "category";
0497     roles[IsTransitioning] = "transitioning";
0498     roles[PayloadSize] = "size";
0499     roles[DownloadedSize] = "downloaded";
0500     d->m_roleNames = roles;
0501 }
0502 
0503 NewstuffModel::~NewstuffModel()
0504 {
0505     delete d;
0506 }
0507 
0508 int NewstuffModel::rowCount ( const QModelIndex &parent ) const
0509 {
0510     if ( !parent.isValid() ) {
0511         return d->m_items.size();
0512     }
0513 
0514     return 0;
0515 }
0516 
0517 QVariant NewstuffModel::data ( const QModelIndex &index, int role ) const
0518 {
0519     if ( index.isValid() && index.row() >= 0 && index.row() < d->m_items.size() ) {
0520         switch ( role ) {
0521         case Qt::DisplayRole: return d->m_items.at( index.row() ).m_name;
0522         case Qt::DecorationRole: return d->preview( index.row() );
0523         case Name: return d->m_items.at( index.row() ).m_name;
0524         case Author: return d->m_items.at( index.row() ).m_author;
0525         case License: return d->m_items.at( index.row() ).m_license;
0526         case Summary: return d->m_items.at( index.row() ).m_summary;
0527         case Version: return d->m_items.at( index.row() ).m_version;
0528         case ReleaseDate: return d->m_items.at( index.row() ).m_releaseDate;
0529         case Preview: return d->m_items.at( index.row() ).m_previewUrl;
0530         case Payload: return d->m_items.at( index.row() ).m_payloadUrl;
0531         case InstalledVersion: return d->m_items.at( index.row() ).installedVersion();
0532         case InstalledReleaseDate: return d->m_items.at( index.row() ).installedReleaseDate();
0533         case InstalledFiles: return d->m_items.at( index.row() ).installedFiles();
0534         case IsInstalled: return !d->m_items.at( index.row() ).m_registryNode.isNull();
0535         case IsUpgradable: return d->m_items.at( index.row() ).isUpgradable();
0536         case Category: return d->m_items.at( index.row() ).m_category;
0537         case IsTransitioning: return d->isTransitioning( index.row() );
0538         case PayloadSize: {
0539             qint64 const size = d->m_items.at( index.row() ).m_payloadSize;
0540             QUrl const url = d->m_items.at( index.row() ).m_payloadUrl;
0541             if ( size < -1 && !url.isEmpty() ) {
0542                 d->m_items[index.row()].m_payloadSize = -1; // prevent several head requests for the same item
0543                 d->m_networkAccessManager.head( QNetworkRequest( url ) );
0544             }
0545 
0546             return qMax<qint64>( -1, size );
0547         }
0548         case DownloadedSize: return d->m_items.at( index.row() ).m_downloadedSize;
0549         }
0550     }
0551 
0552     return QVariant();
0553 }
0554 
0555 QHash<int, QByteArray> NewstuffModel::roleNames() const
0556 {
0557     return d->m_roleNames;
0558 }
0559 
0560 
0561 int NewstuffModel::count() const
0562 {
0563     return rowCount();
0564 }
0565 
0566 void NewstuffModel::setProvider( const QString &downloadUrl )
0567 {
0568     if ( downloadUrl == d->m_provider ) {
0569         return;
0570     }
0571 
0572     d->m_provider = downloadUrl;
0573     emit providerChanged();
0574     d->m_networkAccessManager.get( QNetworkRequest( QUrl( downloadUrl ) ) );
0575 }
0576 
0577 QString NewstuffModel::provider() const
0578 {
0579     return d->m_provider;
0580 }
0581 
0582 void NewstuffModel::setTargetDirectory( const QString &targetDirectory )
0583 {
0584     if ( targetDirectory != d->m_targetDirectory ) {
0585         QFileInfo targetDir( targetDirectory );
0586         if ( !targetDir.exists() ) {
0587             if ( !QDir::root().mkpath( targetDir.absoluteFilePath() ) ) {
0588                 qDebug() << "Failed to create directory " << targetDirectory << ", newstuff installation might fail.";
0589             }
0590         }
0591 
0592         d->m_targetDirectory = targetDirectory;
0593         emit targetDirectoryChanged();
0594     }
0595 }
0596 
0597 QString NewstuffModel::targetDirectory() const
0598 {
0599     return d->m_targetDirectory;
0600 }
0601 
0602 void NewstuffModel::setRegistryFile( const QString &filename, IdTag idTag )
0603 {
0604     QString registryFile = filename;
0605     if (registryFile.startsWith(QLatin1Char('~')) && registryFile.length() > 1) {
0606         registryFile = QDir::homePath() + registryFile.mid( 1 );
0607     }
0608 
0609     if ( d->m_registryFile != registryFile ) {
0610         d->m_registryFile = registryFile;
0611         d->m_idTag = idTag;
0612         emit registryFileChanged();
0613 
0614         QFileInfo inputFile( registryFile );
0615         if ( !inputFile.exists() ) {
0616             QDir::root().mkpath( inputFile.absolutePath() );
0617             d->m_registryDocument = QDomDocument( "khotnewstuff3" );
0618             QDomProcessingInstruction header = d->m_registryDocument.createProcessingInstruction( "xml", "version=\"1.0\" encoding=\"utf-8\"" );
0619             d->m_registryDocument.appendChild( header );
0620             d->m_root = d->m_registryDocument.createElement( "hotnewstuffregistry" );
0621             d->m_registryDocument.appendChild( d->m_root );
0622         } else {
0623             QFile input( registryFile );
0624             if ( !input.open( QFile::ReadOnly ) ) {
0625                 mDebug() << "Cannot open newstuff registry " << registryFile;
0626                 return;
0627             }
0628 
0629             if ( !d->m_registryDocument.setContent( &input ) ) {
0630                 mDebug() << "Cannot parse newstuff registry " << registryFile;
0631                 return;
0632             }
0633             input.close();
0634             d->m_root = d->m_registryDocument.documentElement();
0635         }
0636 
0637         d->updateModel();
0638     }
0639 }
0640 
0641 QString NewstuffModel::registryFile() const
0642 {
0643     return d->m_registryFile;
0644 }
0645 
0646 void NewstuffModel::install( int index )
0647 {
0648     if ( index < 0 || index >= d->m_items.size() ) {
0649         return;
0650     }
0651 
0652     NewstuffModelPrivate::Action action( index, NewstuffModelPrivate::Install );
0653     { // <-- do not remove, mutex locker scope
0654         QMutexLocker locker( &d->m_mutex );
0655         if ( d->m_actionQueue.contains( action ) ) {
0656             return;
0657         }
0658         d->m_actionQueue << action;
0659     }
0660 
0661     d->processQueue();
0662 }
0663 
0664 void NewstuffModel::uninstall( int idx )
0665 {
0666     if ( idx < 0 || idx >= d->m_items.size() ) {
0667         return;
0668     }
0669 
0670     if ( d->m_items[idx].m_registryNode.isNull() ) {
0671         emit uninstallationFinished( idx );
0672     }
0673 
0674     NewstuffModelPrivate::Action action( idx, NewstuffModelPrivate::Uninstall );
0675     { // <-- do not remove, mutex locker scope
0676         QMutexLocker locker( &d->m_mutex );
0677         if ( d->m_actionQueue.contains( action ) ) {
0678             return;
0679         }
0680         d->m_actionQueue << action;
0681     }
0682 
0683     d->processQueue();
0684 }
0685 
0686 void NewstuffModel::cancel( int index )
0687 {
0688     if ( !d->isTransitioning( index ) ) {
0689         return;
0690     }
0691 
0692     { // <-- do not remove, mutex locker scope
0693         QMutexLocker locker( &d->m_mutex );
0694         if ( d->m_currentAction.first == index ) {
0695             if ( d->m_currentAction.second == NewstuffModelPrivate::Install ) {
0696                 if ( d->m_currentReply ) {
0697                     d->m_currentReply->abort();
0698                     d->m_currentReply->deleteLater();
0699                     d->m_currentReply = nullptr;
0700                 }
0701 
0702                 if ( d->m_unpackProcess ) {
0703                     d->m_unpackProcess->terminate();
0704                     d->m_unpackProcess->deleteLater();
0705                     d->m_unpackProcess = nullptr;
0706                 }
0707 
0708                 if ( d->m_currentFile ) {
0709                     d->m_currentFile->deleteLater();
0710                     d->m_currentFile = nullptr;
0711                 }
0712 
0713                 d->m_items[d->m_currentAction.first].m_downloadedSize = 0;
0714 
0715                 emit installationFailed( d->m_currentAction.first, tr( "Installation aborted by user." ) );
0716                 d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
0717             } else {
0718                 // Shall we interrupt this?
0719             }
0720         } else {
0721             if ( d->m_currentAction.second == NewstuffModelPrivate::Install ) {
0722                 NewstuffModelPrivate::Action install( index, NewstuffModelPrivate::Install );
0723                 d->m_actionQueue.removeAll( install );
0724                 emit installationFailed( index, tr( "Installation aborted by user." ) );
0725             } else {
0726                 NewstuffModelPrivate::Action uninstall( index, NewstuffModelPrivate::Uninstall );
0727                 d->m_actionQueue.removeAll( uninstall );
0728                 emit uninstallationFinished( index ); // do we need failed here?
0729             }
0730         }
0731     }
0732 
0733     d->processQueue();
0734 }
0735 
0736 void NewstuffModel::updateProgress( qint64 bytesReceived, qint64 bytesTotal )
0737 {
0738     qreal const progress = qBound<qreal>( 0.0, 0.9 * bytesReceived / qreal( bytesTotal ), 1.0 );
0739     emit installationProgressed( d->m_currentAction.first, progress );
0740     NewstuffItem &item = d->m_items[d->m_currentAction.first];
0741     item.m_payloadSize = bytesTotal;
0742     if ( qreal(bytesReceived-item.m_downloadedSize)/bytesTotal >= 0.01 || progress >= 0.9 ) {
0743         // Only consider download progress of 1% and more as a data change
0744         item.m_downloadedSize = bytesReceived;
0745         QModelIndex const affected = index( d->m_currentAction.first );
0746         emit dataChanged( affected, affected );
0747     }
0748 }
0749 
0750 void NewstuffModel::retrieveData()
0751 {
0752     if ( d->m_currentReply && d->m_currentReply->isReadable() ) {
0753         // check if we are redirected
0754         const QVariant redirectionAttribute = d->m_currentReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
0755         if ( !redirectionAttribute.isNull() ) {
0756             d->m_currentReply = d->m_networkAccessManager.get( QNetworkRequest( redirectionAttribute.toUrl() ) );
0757             QObject::connect( d->m_currentReply, SIGNAL(readyRead()), this, SLOT(retrieveData()) );
0758             QObject::connect( d->m_currentReply, SIGNAL(readChannelFinished()), this, SLOT(retrieveData()) );
0759             QObject::connect( d->m_currentReply, SIGNAL(downloadProgress(qint64,qint64)),
0760                               this, SLOT(updateProgress(qint64,qint64)) );
0761         } else {
0762             d->m_currentFile->write( d->m_currentReply->readAll() );
0763             if ( d->m_currentReply->isFinished() ) {
0764                 d->m_currentReply->deleteLater();
0765                 d->m_currentReply = nullptr;
0766                 d->m_currentFile->flush();
0767                 d->installMap();
0768             }
0769         }
0770     }
0771 }
0772 
0773 void NewstuffModel::mapInstalled( int exitStatus )
0774 {
0775     if ( d->m_unpackProcess ) {
0776         d->m_unpackProcess->deleteLater();
0777         d->m_unpackProcess = nullptr;
0778     }
0779 
0780     if ( d->m_currentFile ) {
0781         d->m_currentFile->deleteLater();
0782         d->m_currentFile = nullptr;
0783     }
0784 
0785     emit installationProgressed( d->m_currentAction.first, 1.0 );
0786     d->m_items[d->m_currentAction.first].m_downloadedSize = 0;
0787     if ( exitStatus == 0 ) {
0788         emit installationFinished( d->m_currentAction.first );
0789     } else {
0790         mDebug() << "Process exit status " << exitStatus << " indicates an error.";
0791         emit installationFailed( d->m_currentAction.first , QString( "Unable to unpack file. Process exited with status code %1." ).arg( exitStatus ) );
0792     }
0793     QModelIndex const affected = index( d->m_currentAction.first );
0794 
0795     { // <-- do not remove, mutex locker scope
0796         QMutexLocker locker( &d->m_mutex );
0797         d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
0798     }
0799     emit dataChanged( affected, affected );
0800     d->processQueue();
0801 }
0802 
0803 void NewstuffModel::mapUninstalled()
0804 {
0805     QModelIndex const affected = index( d->m_currentAction.first );
0806     emit uninstallationFinished( d->m_currentAction.first );
0807 
0808     { // <-- do not remove, mutex locker scope
0809         QMutexLocker locker( &d->m_mutex );
0810         d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
0811     }
0812     emit dataChanged( affected, affected );
0813     d->processQueue();
0814 }
0815 
0816 void NewstuffModel::contentsListed( int exitStatus )
0817 {
0818     if ( exitStatus == 0 ) {
0819         QStringList const files = QString(d->m_unpackProcess->readAllStandardOutput()).split(QLatin1Char('\n'), QString::SkipEmptyParts);
0820         d->updateRegistry(files);
0821 
0822         QObject::disconnect( d->m_unpackProcess, SIGNAL(finished(int)),
0823                              this, SLOT(contentsListed(int)) );
0824         QObject::connect( d->m_unpackProcess, SIGNAL(finished(int)),
0825                           this, SLOT(mapInstalled(int)) );
0826         QStringList arguments = QStringList() << "-x" << "-z" << "-f" << d->m_currentFile->fileName();
0827         d->m_unpackProcess->start( "tar", arguments );
0828     } else {
0829         mDebug() << "Process exit status " << exitStatus << " indicates an error.";
0830         emit installationFailed( d->m_currentAction.first , QString( "Unable to list file contents. Process exited with status code %1." ).arg( exitStatus ) );
0831 
0832         { // <-- do not remove, mutex locker scope
0833             QMutexLocker locker( &d->m_mutex );
0834             d->m_currentAction = NewstuffModelPrivate::Action( -1, NewstuffModelPrivate::Install );
0835         }
0836         d->processQueue();
0837     }
0838 }
0839 
0840 void NewstuffModelPrivate::updateRegistry(const QStringList &files)
0841 {
0842     emit m_parent->installationProgressed( m_currentAction.first, 0.92 );
0843     if ( !m_registryFile.isEmpty() ) {
0844         NewstuffItem &item = m_items[m_currentAction.first];
0845         QDomNode node = item.m_registryNode;
0846         NewstuffModelPrivate::NodeAction action = node.isNull() ? NewstuffModelPrivate::Append : NewstuffModelPrivate::Replace;
0847         if ( node.isNull() ) {
0848             node = m_root.appendChild( m_registryDocument.createElement( "stuff" ) );
0849         }
0850 
0851         node.toElement().setAttribute( "category", m_items[m_currentAction.first].m_category );
0852         changeNode( node, m_registryDocument, "name", item.m_name, action );
0853         changeNode( node, m_registryDocument, "providerid", m_provider, action );
0854         changeNode( node, m_registryDocument, "author", item.m_author, action );
0855         changeNode( node, m_registryDocument, "homepage", QString(), action );
0856         changeNode( node, m_registryDocument, "licence", item.m_license, action );
0857         changeNode( node, m_registryDocument, "version", item.m_version, action );
0858         QString const itemId = m_idTag == NewstuffModel::PayloadTag ? item.m_payloadUrl.toString() : item.m_name;
0859         changeNode( node, m_registryDocument, "id", itemId, action );
0860         changeNode( node, m_registryDocument, "releasedate", item.m_releaseDate, action );
0861         changeNode( node, m_registryDocument, "summary", item.m_summary, action );
0862         changeNode( node, m_registryDocument, "changelog", QString(), action );
0863         changeNode( node, m_registryDocument, "preview", item.m_previewUrl.toString(), action );
0864         changeNode( node, m_registryDocument, "previewBig", item.m_previewUrl.toString(), action );
0865         changeNode( node, m_registryDocument, "payload", item.m_payloadUrl.toString(), action );
0866         changeNode( node, m_registryDocument, "status", "installed", action );
0867         m_items[m_currentAction.first].m_registryNode = node;
0868 
0869         bool hasChildren = true;
0870         while ( hasChildren ) {
0871             /** @todo FIXME: fileList does not contain all elements opposed to what docs say */
0872             QDomNodeList fileList = node.toElement().elementsByTagName( "installedfile" );
0873             hasChildren = !fileList.isEmpty();
0874             for ( int i=0; i<fileList.count(); ++i ) {
0875                 node.removeChild( fileList.at( i ) );
0876             }
0877         }
0878 
0879         for( const QString &file: files ) {
0880             QDomNode fileNode = node.appendChild( m_registryDocument.createElement( "installedfile" ) );
0881             fileNode.appendChild(m_registryDocument.createTextNode(m_targetDirectory + QLatin1Char('/') + file));
0882         }
0883 
0884         saveRegistry();
0885     }
0886 }
0887 
0888 void NewstuffModelPrivate::processQueue()
0889 {
0890     if ( m_actionQueue.empty() || m_currentAction.first >= 0 ) {
0891         return;
0892     }
0893 
0894     { // <-- do not remove, mutex locker scope
0895         QMutexLocker locker( &m_mutex );
0896         m_currentAction = m_actionQueue.takeFirst();
0897     }
0898     if ( m_currentAction.second == Install ) {
0899         if ( !m_currentFile ) {
0900             QFileInfo const file = m_items.at( m_currentAction.first ).m_payloadUrl.path();
0901             m_currentFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/marble-XXXXXX-") + file.fileName());
0902         }
0903 
0904         if ( m_currentFile->open() ) {
0905             QUrl const payload = m_items.at( m_currentAction.first ).m_payloadUrl;
0906             m_currentReply = m_networkAccessManager.get( QNetworkRequest( payload ) );
0907             QObject::connect( m_currentReply, SIGNAL(readyRead()), m_parent, SLOT(retrieveData()) );
0908             QObject::connect( m_currentReply, SIGNAL(readChannelFinished()), m_parent, SLOT(retrieveData()) );
0909             QObject::connect( m_currentReply, SIGNAL(downloadProgress(qint64,qint64)),
0910                               m_parent, SLOT(updateProgress(qint64,qint64)) );
0911             /** @todo: handle download errors */
0912         } else {
0913             mDebug() << "Failed to write to " << m_currentFile->fileName();
0914         }
0915     } else {
0916         // Run in a separate thread to keep the ui responsive
0917         QFutureWatcher<void>* watcher = new QFutureWatcher<void>( m_parent );
0918         QObject::connect( watcher, SIGNAL(finished()), m_parent, SLOT(mapUninstalled()) );
0919         QObject::connect( watcher, SIGNAL(finished()), watcher, SLOT(deleteLater()) );
0920 
0921         QFuture<void> future = QtConcurrent::run( this, &NewstuffModelPrivate::uninstall, m_currentAction.first );
0922         watcher->setFuture( future );
0923     }
0924 }
0925 
0926 NewstuffItem NewstuffModelPrivate::importNode(const QDomNode &node)
0927 {
0928     NewstuffItem item;
0929     item.m_category = node.attributes().namedItem(QStringLiteral("category")).toAttr().value();
0930     readValue<QString>( node, "name", &item.m_name );
0931     readValue<QString>( node, "author", &item.m_author );
0932     readValue<QString>( node, "licence", &item.m_license );
0933     readValue<QString>( node, "summary", &item.m_summary );
0934     readValue<QString>( node, "version", &item.m_version );
0935     readValue<QString>( node, "releasedate", &item.m_releaseDate );
0936     readValue<QUrl>( node, "preview", &item.m_previewUrl );
0937     readValue<QUrl>( node, "payload", &item.m_payloadUrl );
0938     return item;
0939 }
0940 
0941 bool NewstuffModelPrivate::isTransitioning( int index ) const
0942 {
0943     if ( m_currentAction.first == index ) {
0944         return true;
0945     }
0946 
0947     for( const Action &action: m_actionQueue ) {
0948         if ( action.first == index ) {
0949             return true;
0950         }
0951     }
0952 
0953     return false;
0954 }
0955 
0956 }
0957 
0958 #include "moc_NewstuffModel.cpp"