File indexing completed on 2024-06-02 04:52:44

0001 /****************************************************************************************
0002  * Copyright (c) 2004-2010 Mark Kretschmann <kretschmann@kde.org>                       *
0003  * Copyright (c) 2005-2007 Seb Ruiz <ruiz@kde.org>                                      *
0004  * Copyright (c) 2006 Alexandre Pereira de Oliveira <aleprj@gmail.com>                  *
0005  * Copyright (c) 2006 Martin Ellis <martin.ellis@kdemail.net>                           *
0006  * Copyright (c) 2007 Leo Franchi <lfranchi@gmail.com>                                  *
0007  * Copyright (c) 2008 Peter ZHOU <peterzhoulei@gmail.com>                               *
0008  * Copyright (c) 2009 Jakob Kummerow <jakob.kummerow@gmail.com>                         *
0009  *                                                                                      *
0010  * This program is free software; you can redistribute it and/or modify it under        *
0011  * the terms of the GNU General Public License as published by the Free Software        *
0012  * Foundation; either version 2 of the License, or (at your option) any later           *
0013  * version.                                                                             *
0014  *                                                                                      *
0015  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0016  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0017  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0018  *                                                                                      *
0019  * You should have received a copy of the GNU General Public License along with         *
0020  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0021  ****************************************************************************************/
0022 
0023 #define DEBUG_PREFIX "ScriptManager"
0024 
0025 #include "ScriptManager.h"
0026 
0027 #include "core/support/Amarok.h"
0028 #include "core/support/Debug.h"
0029 #include "core/support/Components.h"
0030 #include "core/logger/Logger.h"
0031 #include "MainWindow.h"
0032 #include "amarokconfig.h"
0033 #include <config.h> // for the compile flags
0034 #include "services/scriptable/ScriptableServiceManager.h"
0035 #include "ScriptItem.h"
0036 #include "ScriptUpdater.h"
0037 
0038 #include <KMessageBox>
0039 #include <KPluginInfo>
0040 #include <KPluginMetaData>
0041 
0042 #include <QFileInfo>
0043 #include <QJSEngine>
0044 #include <QJsonDocument>
0045 #include <QStandardPaths>
0046 #include <QTimer>
0047 #include <QDir>
0048 
0049 #include <sys/stat.h>
0050 #include <sys/types.h>
0051 
0052 
0053 ScriptManager* ScriptManager::s_instance = nullptr;
0054 
0055 ScriptManager::ScriptManager( QObject* parent )
0056     : QObject( parent )
0057 {
0058     DEBUG_BLOCK
0059     setObjectName( "ScriptManager" );
0060 
0061     s_instance = this;
0062 
0063     if( AmarokConfig::enableScripts() == false )
0064     {
0065         AmarokConfig::setEnableScripts( true );
0066     }
0067 
0068     // Delay this call via eventloop, because it's a bit slow and would block
0069     QTimer::singleShot( 0, this, &ScriptManager::updateAllScripts );
0070 }
0071 
0072 ScriptManager::~ScriptManager()
0073 {}
0074 
0075 void
0076 ScriptManager::destroy() {
0077     if (s_instance) {
0078         delete s_instance;
0079         s_instance = nullptr;
0080     }
0081 }
0082 
0083 ScriptManager*
0084 ScriptManager::instance()
0085 {
0086     return s_instance ? s_instance : new ScriptManager( The::mainWindow() );
0087 }
0088 
0089 ////////////////////////////////////////////////////////////////////////////////
0090 // public
0091 ////////////////////////////////////////////////////////////////////////////////
0092 
0093 bool
0094 ScriptManager::runScript( const QString& name, bool silent )
0095 {
0096     if( !m_scripts.contains( name ) )
0097         return false;
0098 
0099     return slotRunScript( name, silent );
0100 }
0101 
0102 bool
0103 ScriptManager::stopScript( const QString& name )
0104 {
0105     if( name.isEmpty() )
0106         return false;
0107     if( !m_scripts.contains( name ) )
0108         return false;
0109     m_scripts[name]->stop();
0110     return true;
0111 }
0112 
0113 QStringList
0114 ScriptManager::listRunningScripts() const
0115 {
0116     QStringList runningScripts;
0117     foreach( const ScriptItem *item, m_scripts )
0118     {
0119         if( item->running() )
0120             runningScripts << item->info().pluginName();
0121     }
0122     return runningScripts;
0123 }
0124 
0125 QString
0126 ScriptManager::specForScript( const QString& name ) const
0127 {
0128     if( !m_scripts.contains( name ) )
0129         return QString();
0130     return m_scripts[name]->specPath();
0131 }
0132 
0133 KPluginMetaData
0134 ScriptManager::createMetadaFromSpec( const QString &specPath )
0135 {
0136     // KPluginMetaData and KPluginInfo require file suffix to be .desktop. Thus create temporary file with suffix
0137     QFile specFile( specPath );
0138     QTemporaryFile desktopFile( QDir::tempPath() + "/XXXXXX.desktop" );
0139 
0140     if ( !specFile.open( QIODevice::ReadOnly ) ) {
0141         warning() << "Could not read from spec file: " << specPath;
0142         return KPluginMetaData();
0143     } else if ( !desktopFile.open() ) {
0144         warning() << "Could not create temporary .desktop file at " << QDir::tempPath();
0145         return KPluginMetaData();
0146     }
0147 
0148     QTextStream( &desktopFile ) << QTextStream( &specFile ).readAll();
0149     desktopFile.close();
0150 
0151     return KPluginMetaData( desktopFile.fileName() );
0152 }
0153 
0154 bool
0155 ScriptManager::lyricsScriptRunning() const
0156 {
0157     return !m_lyricsScript.isEmpty();
0158 }
0159 
0160 void
0161 ScriptManager::notifyFetchLyrics( const QString& artist, const QString& title, const QString& url, const Meta::TrackPtr &track )
0162 {
0163     DEBUG_BLOCK
0164     Q_EMIT fetchLyrics( artist, title, url, track );
0165 }
0166 
0167 ////////////////////////////////////////////////////////////////////////////////
0168 // private slots (script updater stuff)
0169 ////////////////////////////////////////////////////////////////////////////////
0170 
0171 void
0172 ScriptManager::updateAllScripts() // SLOT
0173 {
0174     DEBUG_BLOCK
0175     // find all scripts (both in $KDEHOME and /usr)
0176     QStringList foundScripts;
0177     QStringList locations = QStandardPaths::standardLocations( QStandardPaths::GenericDataLocation );
0178     for( const auto &location : locations )
0179     {
0180         QDir dir( location + "/amarok/scripts" );
0181 
0182         if( !dir.exists() )
0183             continue;
0184 
0185         for( const auto &scriptLocation : dir.entryList( QDir::NoDotAndDotDot | QDir::Dirs ) )
0186         {
0187             QDir scriptDir( dir.absoluteFilePath( scriptLocation ) );
0188             if( scriptDir.exists( QStringLiteral( "main.js" ) ) )
0189                 foundScripts << scriptDir.absoluteFilePath( QStringLiteral( "main.js" ) );
0190         }
0191     }
0192 
0193     // remove deleted scripts
0194     foreach( ScriptItem *item, m_scripts )
0195     {
0196         const QString specPath = QString( "%1/script.spec" ).arg( QFileInfo( item->url().path() ).path() );
0197         if( !QFile::exists( specPath ) )
0198         {
0199             debug() << "Removing script " << item->info().pluginName();
0200             item->uninstall();
0201             m_scripts.remove( item->info().pluginName() );
0202         }
0203     }
0204 
0205     m_nScripts = foundScripts.count();
0206 
0207     // get timestamp of the last update check
0208     KConfigGroup config = Amarok::config( "ScriptManager" );
0209     const uint lastCheck = config.readEntry( "LastUpdateCheck", QVariant( 0 ) ).toUInt();
0210     const uint now = QDateTime::currentDateTimeUtc().toSecsSinceEpoch();
0211     bool autoUpdateScripts = AmarokConfig::autoUpdateScripts();
0212     // note: we can't update scripts without the QtCryptoArchitecture, so don't even try
0213     #ifndef QCA2_FOUND
0214     autoUpdateScripts = false;
0215     #endif
0216 
0217     // last update was at least 7 days ago -> check now if auto update is enabled
0218     if( autoUpdateScripts && (now - lastCheck > 7*24*60*60) )
0219     {
0220         debug() << "ScriptUpdater: Performing script update check now!";
0221         for( int i = 0; i < m_nScripts; ++i )
0222         {
0223             ScriptUpdater *updater = new ScriptUpdater( this );
0224             // all the ScriptUpdaters are now started in parallel.
0225             // tell them which script to work on
0226             updater->setScriptPath( foundScripts.at( i ) );
0227             // tell them whom to signal when they're finished
0228             connect( updater, &ScriptUpdater::finished, this, &ScriptManager::updaterFinished );
0229             // and finally tell them to get to work
0230             QTimer::singleShot( 0, updater, &ScriptUpdater::updateScript );
0231         }
0232         // store current timestamp
0233         config.writeEntry( "LastUpdateCheck", QVariant( now ) );
0234         config.sync();
0235     }
0236     // last update was pretty recent, don't check again
0237     else
0238     {
0239         debug() << "ScriptUpdater: Skipping update check";
0240         for ( int i = 0; i < m_nScripts; i++ )
0241         {
0242             loadScript( foundScripts.at( i ) );
0243         }
0244         configChanged( true );
0245     }
0246 }
0247 
0248 void
0249 ScriptManager::updaterFinished( const QString &scriptPath ) // SLOT
0250 {
0251     DEBUG_BLOCK
0252     // count this event
0253     m_updateSemaphore.release();
0254     loadScript( scriptPath );
0255     if ( m_updateSemaphore.tryAcquire(m_nScripts) )
0256     {
0257         configChanged( true );
0258     }
0259     sender()->deleteLater();
0260 }
0261 
0262 ////////////////////////////////////////////////////////////////////////////////
0263 // private slots
0264 ////////////////////////////////////////////////////////////////////////////////
0265 
0266 bool
0267 ScriptManager::slotRunScript( const QString &name, bool silent )
0268 {
0269     ScriptItem *item = m_scripts.value( name );
0270     connect( item, &ScriptItem::signalHandlerException,
0271              this, &ScriptManager::handleException );
0272     if( item->info().category() == "Lyrics" )
0273     {
0274         m_lyricsScript = name;
0275         debug() << "lyrics script started:" << name;
0276         Q_EMIT lyricsScriptStarted();
0277     }
0278     return item->start( silent );
0279 }
0280 
0281 void
0282 ScriptManager::handleException(const QJSValue& value)
0283 {
0284     DEBUG_BLOCK
0285 
0286     Amarok::Logger::longMessage( i18n( "Script error reported by: %1\n%2", value.property("name").toString(), value.property("message").toString() ), Amarok::Logger::Error );
0287 }
0288 
0289 void
0290 ScriptManager::ServiceScriptPopulate( const QString &name, int level, int parent_id,
0291                                       const QString &path, const QString &filter )
0292 {
0293     if( m_scripts.value( name )->service() )
0294         m_scripts.value( name )->service()->slotPopulate( name, level, parent_id, path, filter );
0295 }
0296 
0297 void
0298 ScriptManager::ServiceScriptCustomize( const QString &name )
0299 {
0300     if( m_scripts.value( name )->service() )
0301         m_scripts.value( name )->service()->slotCustomize( name );
0302 }
0303 
0304 void
0305 ScriptManager::ServiceScriptRequestInfo( const QString &name, int level, const QString &callbackString )
0306 {
0307     if( m_scripts.value( name )->service() )
0308         m_scripts.value( name )->service()->slotRequestInfo( name, level, callbackString );
0309 }
0310 
0311 void
0312 ScriptManager::configChanged( bool changed )
0313 {
0314     Q_EMIT scriptsChanged();
0315     if( !changed )
0316         return;
0317     //evil scripts may prevent the config dialog from dismissing, delay execution
0318     QTimer::singleShot( 0, this, &ScriptManager::slotConfigChanged );
0319 }
0320 
0321 ////////////////////////////////////////////////////////////////////////////////
0322 // private
0323 ////////////////////////////////////////////////////////////////////////////////
0324 
0325 void
0326 ScriptManager::slotConfigChanged()
0327 {
0328     foreach( ScriptItem *item, m_scripts )
0329     {
0330         const QString name = item->info().pluginName();
0331         bool enabledByDefault = item->info().isPluginEnabledByDefault();
0332         bool enabled = Amarok::config( "Plugins" ).readEntry( name + "Enabled", enabledByDefault );
0333 
0334         if( !item->running() && enabled )
0335         {
0336             slotRunScript( name );
0337         }
0338         else if( item->running() && !enabled )
0339         {
0340             item->stop();
0341         }
0342     }
0343 }
0344 
0345 bool
0346 ScriptManager::loadScript( const QString& path )
0347 {
0348     if( path.isEmpty() )
0349         return false;
0350 
0351     QStringList SupportAPIVersion;
0352     SupportAPIVersion << QLatin1String("API V1.0.0") << QLatin1String("API V1.0.1");
0353     QString ScriptVersion;
0354     QFileInfo info( path );
0355     const QString jsonPath = QString( "%1/script.json" ).arg( info.path() );
0356     const QString specPath = QString( "%1/script.spec" ).arg( info.path() );
0357     KPluginMetaData pluginMetadata;
0358 
0359     if( QFile::exists( jsonPath ) )
0360     {
0361         pluginMetadata = KPluginMetaData( jsonPath );
0362     }
0363     else if( QFile::exists( specPath ) )
0364     {
0365         warning() << "Reading legacy spec file: " << specPath;
0366         pluginMetadata = createMetadaFromSpec( specPath );
0367     }
0368     else
0369     {
0370         error() << "script.json for "<< path << " is missing!";
0371         return false;
0372     }
0373 
0374     if( !pluginMetadata.isValid() )
0375     {
0376         error() << "PluginMetaData invalid for" << jsonPath;
0377         return false;
0378     }
0379 
0380     const QString pluginName = pluginMetadata.pluginId();
0381     const QString category   = pluginMetadata.category();
0382     const QString version    = pluginMetadata.version();
0383 
0384     if( pluginName.isEmpty() || category.isEmpty() || version.isEmpty() )
0385     {
0386         error() << "PluginMetaData has empty values for" << jsonPath;
0387         return false;
0388     }
0389 
0390     KPluginInfo pluginInfo( pluginMetadata );
0391 
0392     ScriptItem *item;
0393     if( !m_scripts.contains( pluginName ) )
0394     {
0395         item = new ScriptItem( this, pluginName, path, pluginInfo );
0396         m_scripts[ pluginName ] = item;
0397     }
0398     else if( m_scripts[pluginName]->info().version() < pluginInfo.version() )
0399     {
0400         m_scripts[ pluginName ]->deleteLater();
0401         item = new ScriptItem( this, pluginName, path, pluginInfo );
0402         m_scripts[ pluginName ] = item;
0403     }
0404     else
0405         item = m_scripts.value( pluginName );
0406 
0407     //assume it is API V1.0.0 if there is no "API V" prefix found
0408     if( !item->info().dependencies().at(0).startsWith("API V") )
0409         ScriptVersion = QLatin1String("API V1.0.0");
0410     else
0411         ScriptVersion = item->info().dependencies().at(0);
0412 
0413     if( !SupportAPIVersion.contains( ScriptVersion ) )
0414     {
0415         warning() << "script API version not compatible with Amarok.";
0416         return false;
0417     }
0418 
0419     debug() << "found script:" << category << pluginName << version << item->info().dependencies();
0420     return true;
0421 }
0422 
0423 KPluginInfo::List
0424 ScriptManager::scripts( const QString &category ) const
0425 {
0426     KPluginInfo::List scripts;
0427     foreach( const ScriptItem *script, m_scripts )
0428     {
0429         if( script->info().category() == category )
0430             scripts << script->info();
0431     }
0432     return scripts;
0433 }
0434 
0435 QString
0436 ScriptManager::scriptNameForEngine( const QJSEngine *engine ) const
0437 {
0438     foreach( const QString &name, m_scripts.keys() )
0439     {
0440         ScriptItem *script = m_scripts[name];
0441         if( script->engine() == engine )
0442             return name;
0443     }
0444 
0445     return QString();
0446 }