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 }