File indexing completed on 2024-04-21 15:55:47

0001 /**************************************************************************
0002 *   Copyright (C) 2006-2020 by Michel Ludwig (michel.ludwig@kdemail.net)  *
0003 ***************************************************************************/
0004 
0005 /**************************************************************************
0006 *                                                                         *
0007 *   This program is free software; you can redistribute it and/or modify  *
0008 *   it under the terms of the GNU General Public License as published by  *
0009 *   the Free Software Foundation; either version 2 of the License, or     *
0010 *   (at your option) any later version.                                   *
0011 *                                                                         *
0012 ***************************************************************************/
0013 
0014 #include "scriptmanager.h"
0015 
0016 #include <KConfig>
0017 #include <KLocalizedString>
0018 #include <KMessageBox>
0019 #include <KXMLGUIClient>
0020 #include <KXMLGUIFactory>
0021 
0022 #include <QEvent>
0023 #include <QDir>
0024 #include <QDirIterator>
0025 #include <QMap>
0026 
0027 #include "kiledebug.h"
0028 #include "kileconfig.h"
0029 #include "kileinfo.h"
0030 #include "kileversion.h"
0031 #include "kileviewmanager.h"
0032 #include "editorkeysequencemanager.h"
0033 #include "utilities.h"
0034 #include "scripting/script.h"
0035 
0036 namespace KileScript {
0037 
0038 ////////////////////////////// Manager //////////////////////////////
0039 
0040 Manager::Manager(KileInfo *kileInfo, KConfig *config, KActionCollection *actionCollection, QObject *parent, const char *name)
0041     : QObject(parent), m_jScriptDirWatch(Q_NULLPTR), m_kileInfo(kileInfo), m_config(config), m_actionCollection(actionCollection)
0042 {
0043     setObjectName(name);
0044 
0045     // create a local scripts directory if it doesn't exist yet
0046     m_localScriptDir = KileUtilities::writableLocation(QStandardPaths::AppDataLocation) + "/scripts/";
0047     QDir testDir(m_localScriptDir);
0048     if (!testDir.exists()) {
0049         testDir.mkpath(m_localScriptDir);
0050     }
0051 
0052     m_jScriptDirWatch = new KDirWatch(this);
0053     m_jScriptDirWatch->setObjectName("KileScript::Manager::ScriptDirWatch");
0054     connect(m_jScriptDirWatch, SIGNAL(dirty(QString)), this, SLOT(scanScriptDirectories()));
0055     connect(m_jScriptDirWatch, SIGNAL(created(QString)), this, SLOT(scanScriptDirectories()));
0056     connect(m_jScriptDirWatch, SIGNAL(deleted(QString)), this, SLOT(scanScriptDirectories()));
0057     m_jScriptDirWatch->startScan();
0058 
0059     // read plugin code for QScriptEngine
0060     readEnginePlugin();
0061     m_scriptActionMap = new QMap<QString,QAction *>;
0062 
0063     // init script objects
0064     m_kileScriptView = new KileScriptView(this, m_kileInfo->editorExtension());
0065     m_kileScriptDocument = new KileScriptDocument(this, m_kileInfo, m_kileInfo->editorExtension(), m_scriptActionMap);
0066     m_kileScriptObject = new KileScriptObject(this, m_kileInfo, m_scriptActionMap);
0067 }
0068 
0069 Manager::~Manager()
0070 {
0071     delete m_jScriptDirWatch;
0072     delete m_scriptActionMap;
0073 
0074     delete m_kileScriptView;
0075     delete m_kileScriptDocument;
0076     delete m_kileScriptObject;
0077 
0078     //still need to delete the scripts
0079     for(Script *script : qAsConst(m_jScriptList)) {
0080         delete script;
0081     }
0082     m_jScriptList.clear();
0083 }
0084 
0085 void Manager::executeScript(const Script *script)
0086 {
0087     KILE_DEBUG_MAIN << "execute script: " << script->getName();
0088 
0089     // compatibility check
0090     QString code = script->getCode();
0091     QRegExp endOfLineExp("(\r\n)|\n|\r");
0092     int i = code.indexOf(endOfLineExp);
0093     QString firstLine = (i >= 0 ? code.left(i) : code);
0094     QRegExp requiredVersionTagExp("(kile-version:\\s*)(\\d+\\.\\d+(.\\d+)?)");
0095     if(requiredVersionTagExp.indexIn(firstLine) != -1) {
0096         QString requiredKileVersion = requiredVersionTagExp.cap(2);
0097         if(compareVersionStrings(requiredKileVersion, kileFullVersion) > 0) {
0098             KMessageBox::error(m_kileInfo->mainWindow(), i18n("Version %1 of Kile is at least required to execute the script \"%2\". The execution has been aborted.",
0099                                                               requiredKileVersion, script->getName()), i18n("Version Error"));
0100             return;
0101         }
0102     }
0103 
0104     // TODO only scripts with a current view can be started at this moment
0105     KTextEditor::View *view = m_kileInfo->viewManager()->currentTextView();
0106     if(!view) {
0107         KMessageBox::error(m_kileInfo->mainWindow(), i18n("Cannot start the script: no view available"), i18n("Script Error"));
0108         return;
0109     }
0110 
0111     // TODO setup script objects (with existing views at this moment)
0112     m_kileScriptView->setView(view);
0113     m_kileScriptDocument->setView(view);
0114     m_kileScriptObject->setScriptname(script->getName());
0115 
0116     // create environment for QtScript engine
0117     ScriptEnvironment env(m_kileInfo, m_kileScriptView, m_kileScriptDocument, m_kileScriptObject,m_enginePlugin);
0118 
0119     env.execute(script);
0120 }
0121 
0122 void Manager::executeScript(unsigned int id)
0123 {
0124     QMap<unsigned int, Script*>::iterator i = m_idScriptMap.find(id);
0125     if(i != m_idScriptMap.end()) {
0126         executeScript(*i);
0127     }
0128 }
0129 
0130 const Script* Manager::getScript(unsigned int id)
0131 {
0132     QMap<unsigned int, Script*>::iterator i = m_idScriptMap.find(id);
0133     return ((i != m_idScriptMap.end()) ? (*i) : Q_NULLPTR);
0134 }
0135 
0136 void Manager::scanScriptDirectories()
0137 {
0138     if(!KileConfig::scriptingEnabled()) {
0139         return;
0140     }
0141     deleteScripts();
0142     populateDirWatch();
0143 
0144     KConfigGroup configGroup = m_config->group("Scripts");
0145     const QList<unsigned int> idList = configGroup.readEntry("IDs", QList<unsigned int>());
0146     unsigned int maxID = 0;
0147     QMap<QString, unsigned int> pathIDMap;
0148     QMap<unsigned int, bool> takenIDMap;
0149     for(const unsigned int i : idList) {
0150         // as of 12.07.2020, KConfigGroup::readPathEntry messes up the path if $HOME ends in /
0151         // for example, if HOME=/home/michel/, KConfigGroup::readPathEntry will return /home/michel//.local/share/kile/scripts/test.js,
0152         // resulting in the path /home/michel/.local/share/kile/scripts/test.js not being found;
0153         // we have used QDir:cleanPath to work around this
0154         QString fileName = QDir::cleanPath(configGroup.readPathEntry("Script" + QString::number(i), QString()));
0155         if(!fileName.isEmpty()) {
0156             unsigned int id = i;
0157             pathIDMap[fileName] = id;
0158             takenIDMap[id] = true;
0159             maxID = qMax(maxID, id);
0160         }
0161     }
0162 
0163     // scan for *.js files
0164     QSet<QString> scriptFileNamesSet;
0165     {
0166         QSet<QString> canonicalScriptFileNamesSet;
0167         const QStringList dirs = KileUtilities::locateAll(QStandardPaths::AppDataLocation, "scripts/", QStandardPaths::LocateDirectory);
0168         for(const QString &dir : dirs) {
0169             QDirIterator it(dir, QStringList() << QStringLiteral("*.js"), QDir::Files | QDir::Readable, QDirIterator::Subdirectories);
0170             while(it.hasNext()) {
0171                 const QString fileName = QDir::cleanPath(it.next());
0172                 const QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
0173 
0174                 // filter out file paths that point to the same file (via symbolic links, for example)
0175                 // but later on we work with the original file path, possibly containing symbolic links
0176                 if(canonicalFilePath.isEmpty() || canonicalScriptFileNamesSet.contains(canonicalFilePath)) {
0177                     continue;
0178                 }
0179                 canonicalScriptFileNamesSet.insert(canonicalFilePath);
0180 
0181                 scriptFileNamesSet.insert(fileName);
0182             }
0183         }
0184     }
0185 
0186     for(const QString &scriptFileName : qAsConst(scriptFileNamesSet)) {
0187         registerScript(scriptFileName, pathIDMap, takenIDMap, maxID);
0188     }
0189     //rewrite the IDs that are currently in use
0190     writeIDs();
0191     m_actionCollection->readSettings();
0192     emit scriptsChanged();
0193 }
0194 
0195 void Manager::deleteScripts()
0196 {
0197     QList<Script*> scriptList = m_jScriptList;
0198     m_jScriptList.clear(); // pretend that there are no scripts left
0199     QStringList keySequenceList;
0200     for(QList<Script*>::iterator it = scriptList.begin(); it != scriptList.end(); ++it) {
0201         keySequenceList.push_back((*it)->getKeySequence());
0202     }
0203     m_idScriptMap.clear();
0204     m_kileInfo->editorKeySequenceManager()->removeKeySequence(keySequenceList);
0205     for(QList<Script*>::iterator it = scriptList.begin(); it != scriptList.end(); ++it) {
0206         QAction *action = (*it)->getActionObject();
0207         if(action) {
0208             const QList<QWidget*> widgets = action->associatedWidgets();
0209             for(QWidget *w : widgets) {
0210                 w->removeAction(action);
0211             }
0212             m_actionCollection->takeAction(action);
0213             delete action;
0214         }
0215         delete *it;
0216     }
0217     emit scriptsChanged();
0218 }
0219 
0220 QList<Script*> Manager::getScripts()
0221 {
0222     return m_jScriptList;
0223 }
0224 
0225 void Manager::registerScript(const QString& fileName, QMap<QString, unsigned int>& pathIDMap, QMap<unsigned int, bool>& takenIDMap, unsigned int &maxID)
0226 {
0227     unsigned int id;
0228     QMap<QString, unsigned int>::iterator it = pathIDMap.find(fileName);
0229     if(it != pathIDMap.end()) {
0230         id = *it;
0231     }
0232     else {
0233         id = findFreeID(takenIDMap, maxID);
0234         pathIDMap[fileName] = id;
0235         takenIDMap[id] = true;
0236         maxID = qMax(maxID, id);
0237     }
0238     Script* script = new Script(id, fileName);
0239     m_jScriptList.push_back(script);
0240     m_idScriptMap[id] = script;
0241 
0242     // start with setting up the key sequence
0243     KConfigGroup configGroup = m_config->group("Scripts");
0244 
0245     int sequenceType = 0;
0246     QString editorKeySequence = QString();
0247     QString seq = configGroup.readEntry("Script" + QString::number(id) + "KeySequence");
0248     if(!seq.isEmpty()) {
0249         QRegExp re("(\\d+)-(.*)");
0250         if(re.exactMatch(seq))  {
0251             sequenceType = re.cap(1).toInt();
0252             if(sequenceType<Script::KEY_SEQUENCE || sequenceType>Script::KEY_SHORTCUT) {
0253                 sequenceType = Script::KEY_SEQUENCE;
0254             }
0255             editorKeySequence = re.cap(2);
0256         }
0257         else {
0258             sequenceType = Script::KEY_SEQUENCE;
0259             editorKeySequence = re.cap(1);
0260         }
0261     }
0262     KILE_DEBUG_MAIN << "script type=" << sequenceType << " seq=" << editorKeySequence;
0263 
0264     // now set up a regular action object
0265     ScriptExecutionAction *action = new ScriptExecutionAction(id, this, m_actionCollection);
0266 
0267     // add to action collection
0268     m_actionCollection->addAction("script" + QString::number(id) + "_execution", action);
0269     m_actionCollection->setDefaultShortcut(action, QString());
0270     script->setActionObject(action);
0271 
0272     // action with shortcut?
0273     if(!editorKeySequence.isEmpty()) {
0274         script->setSequenceType(sequenceType);
0275         script->setKeySequence(editorKeySequence);
0276         if(sequenceType == Script::KEY_SEQUENCE) {
0277             m_kileInfo->editorKeySequenceManager()->addAction(editorKeySequence, new KileEditorKeySequence::ExecuteScriptAction(script, this));
0278         }
0279         else {
0280             action->setShortcut(editorKeySequence);
0281         }
0282     }
0283 }
0284 
0285 void Manager::writeConfig()
0286 {
0287     // don't delete the key sequence settings if scripting has been disabled
0288     if(!KileConfig::scriptingEnabled()) {
0289         return;
0290     }
0291     m_config->deleteGroup("Scripts");
0292     writeIDs();
0293 
0294     // write the key sequences
0295     KConfigGroup configGroup = m_config->group("Scripts");
0296     for(const Script *script : qAsConst(m_jScriptList)) {
0297         QString seq = script->getKeySequence();
0298         QString sequenceEntry = (seq.isEmpty()) ? seq : QString("%1-%2").arg(QString::number(script->getSequenceType()), seq);
0299         configGroup.writeEntry("Script" + QString::number(script->getID()) + "KeySequence", sequenceEntry);
0300     }
0301 }
0302 
0303 void Manager::setEditorKeySequence(Script* script, int type, const QString& keySequence)
0304 {
0305     if(keySequence.isEmpty()) {
0306         return;
0307     }
0308     if(script) {
0309         int oldType = script->getSequenceType();
0310         QString oldSequence = script->getKeySequence();
0311         if(oldType == type && oldSequence == keySequence) {
0312             return;
0313         }
0314 
0315         if(oldType == KileScript::Script::KEY_SEQUENCE) {
0316             m_kileInfo->editorKeySequenceManager()->removeKeySequence(oldSequence);
0317         }
0318         else {
0319             script->getActionObject()->setShortcut(QKeySequence());
0320         }
0321         script->setSequenceType(type);
0322         script->setKeySequence(keySequence);
0323         if(type == KileScript::Script::KEY_SEQUENCE) {
0324             m_kileInfo->editorKeySequenceManager()->addAction(keySequence, new KileEditorKeySequence::ExecuteScriptAction(script, this));
0325         }
0326         else {
0327             script->getActionObject()->setShortcut(keySequence);
0328         }
0329     }
0330 }
0331 
0332 void Manager::setShortcut(Script* script, const QKeySequence& keySequence)
0333 {
0334     if(keySequence.isEmpty()) {
0335         return;
0336     }
0337     if(script) {
0338         if(script->getSequenceType() == KileScript::Script::KEY_SEQUENCE) {
0339             m_kileInfo->editorKeySequenceManager()->removeKeySequence(script->getKeySequence());
0340         }
0341 
0342         script->setSequenceType(KileScript::Script::KEY_SHORTCUT);
0343         script->setKeySequence(keySequence.toString(QKeySequence::PortableText));
0344         script->getActionObject()->setShortcut(keySequence);
0345     }
0346 }
0347 
0348 void Manager::removeEditorKeySequence(Script* script)
0349 {
0350     if(script) {
0351         QString keySequence = script->getKeySequence();
0352         if(keySequence.isEmpty()) {
0353             return;
0354         }
0355         script->setKeySequence(QString());
0356 
0357         int sequenceType = script->getSequenceType();
0358         if(sequenceType == Script::KEY_SEQUENCE) {
0359             m_kileInfo->editorKeySequenceManager()->removeKeySequence(keySequence);
0360         }
0361         else {
0362             script->getActionObject()->setShortcut(QString());
0363         }
0364 
0365         writeConfig();
0366     }
0367 }
0368 
0369 void Manager::populateDirWatch()
0370 {
0371     const QStringList jScriptDirectories = KileUtilities::locateAll(QStandardPaths::AppDataLocation, "scripts/", QStandardPaths::LocateDirectory);
0372     for(const QString& dir : jScriptDirectories) {
0373         // FIXME: future KDE versions could support the recursive
0374         //        watching of directories out of the box.
0375         addDirectoryToDirWatch(dir);
0376     }
0377     //we do not remove the directories that were once added as this apparently causes some strange
0378     //bugs (on KDE 3.5.x)
0379 }
0380 
0381 QString Manager::getLocalScriptDirectory() const
0382 {
0383     return m_localScriptDir;
0384 }
0385 
0386 void Manager::readConfig() {
0387     deleteScripts();
0388     scanScriptDirectories();
0389 }
0390 
0391 unsigned int Manager::findFreeID(const QMap<unsigned int, bool>& takenIDMap, unsigned int maxID)
0392 {
0393     if(takenIDMap.size() == 0) {
0394         return 0;
0395     }
0396     // maxID should have a real meaning now
0397     for(unsigned int i = 0; i < maxID; ++i) {
0398         if(takenIDMap.find(i) == takenIDMap.end()) {
0399             return i;
0400         }
0401     }
0402     return (maxID + 1);
0403 }
0404 
0405 void Manager::writeIDs()
0406 {
0407     KConfigGroup configGroup = m_config->group("Scripts");
0408     //delete old entries
0409     QList<unsigned int> idList = configGroup.readEntry("IDs", QList<unsigned int>());
0410     for(const int i : qAsConst(idList)) {
0411         configGroup.deleteEntry("Script" + QString::number(i));
0412     }
0413     //write new ones
0414     idList.clear();
0415     for(QMap<unsigned int, Script*>::iterator i = m_idScriptMap.begin(); i != m_idScriptMap.end(); ++i) {
0416         unsigned int id = i.key();
0417         idList.push_back(id);
0418         configGroup.writePathEntry("Script" + QString::number(id), (*i)->getFileName());
0419     }
0420     configGroup.writeEntry("IDs", idList);
0421 }
0422 
0423 void Manager::addDirectoryToDirWatch(const QString& dir)
0424 {
0425     //FIXME: no recursive watching and no watching of files as it isn't implemented
0426     //       yet
0427     //FIXME: check for KDE4
0428     if(!m_jScriptDirWatch->contains(dir)) {
0429         m_jScriptDirWatch->addDir(dir,  KDirWatch::WatchDirOnly);
0430     }
0431     QDir qDir(dir);
0432     const QStringList list = qDir.entryList(QDir::Dirs);
0433     for(const QString& subdir : list) {
0434         if(subdir != "." && subdir != "..") {
0435             addDirectoryToDirWatch(qDir.filePath(subdir));
0436         }
0437     }
0438 }
0439 
0440 void Manager::readEnginePlugin()
0441 {
0442     // TODO error message and disable scripting if not found
0443     QString pluginUrl = KileUtilities::locate(QStandardPaths::AppDataLocation, "script-plugins/cursor-range.js");
0444     m_enginePlugin = Script::readFile(pluginUrl);
0445 }
0446 
0447 void Manager::initScriptActions()
0448 {
0449     QStringList m_scriptActionList = QStringList()
0450                                      << "tag_chapter" << "tag_section" << "tag_subsection"
0451                                      << "tag_subsubsection" << "tag_paragraph" << "tag_subparagraph"
0452 
0453                                      << "tag_label" << "tag_ref" << "tag_pageref"
0454                                      << "tag_index" << "tag_footnote" << "tag_cite"
0455 
0456                                      << "tools_comment" << "tools_uncomment" << "tools_uppercase"
0457                                      << "tools_lowercase" << "tools_capitalize" << "tools_join_lines"
0458 
0459                                      << "wizard_tabular" << "wizard_array" << "wizard_tabbing"
0460                                      << "wizard_float" << "wizard_mathenv"
0461                                      << "wizard_postscript" << "wizard_pdf"
0462                                      ;
0463 
0464 
0465     const QList<KXMLGUIClient*> clients = m_kileInfo->mainWindow()->guiFactory()->clients();
0466     for(KXMLGUIClient *client : clients) {
0467         KILE_DEBUG_MAIN << "collection count: " << client->actionCollection()->count();
0468 
0469         const QList<QAction*> actions = client->actionCollection()->actions();
0470         for(QAction *action : actions) {
0471             QString objectname = action->objectName();
0472             if(m_scriptActionList.indexOf(objectname) >= 0) {
0473                 m_scriptActionMap->insert(objectname, action);
0474             }
0475         }
0476     }
0477 }
0478 
0479 
0480 
0481 ////////////////////////////// ScriptExecutionAction //////////////////////////////
0482 
0483 ScriptExecutionAction::ScriptExecutionAction(unsigned int id, KileScript::Manager *manager, QObject* parent) : QAction(parent), m_manager(manager), m_id(id)
0484 {
0485     const KileScript::Script *script = m_manager->getScript(m_id);
0486     Q_ASSERT(script);
0487     setText(i18n("Execution of %1", script->getName()));
0488     connect(this, SIGNAL(triggered()), this, SLOT(executeScript()));
0489 }
0490 
0491 ScriptExecutionAction::~ScriptExecutionAction()
0492 {
0493 }
0494 
0495 void ScriptExecutionAction::executeScript()
0496 {
0497     m_manager->executeScript(m_id);
0498 }
0499 
0500 
0501 
0502 }
0503