File indexing completed on 2022-10-04 17:30:22

0001 // SPDX-License-Identifier: GPL-2.0-or-later
0002 // SPDX-FileCopyrightText: 2007 Dominik Seichter <domseichter@web.de>
0003 // SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
0004 
0005 #include "scriptplugin.h"
0006 
0007 #include <kconfiggroup.h>
0008 #include <kmessagebox.h>
0009 #include <KIO/StoredTransferJob>
0010 #include <KIO/StatJob>
0011 #include <KJobWidgets>
0012 #include <kio_version.h>
0013 
0014 #include <QTemporaryFile>
0015 #include <QFile>
0016 #include <QMenu>
0017 #include <QTextStream>
0018 #include <QVariant>
0019 #include <QFileDialog>
0020 #include <QDebug>
0021 #include <QThread>
0022 
0023 #include "ui_scriptplugindialog.h"
0024 #include "ui_scriptpluginwidget.h"
0025 #include "batchrenamer.h"
0026 #include "krenamefile.h"
0027 
0028 const char *ScriptPlugin::s_pszFileDialogLocation = "kfiledialog://krenamejscript";
0029 const char *ScriptPlugin::s_pszVarNameIndex       = "krename_index";
0030 const char *ScriptPlugin::s_pszVarNameUrl         = "krename_url";
0031 const char *ScriptPlugin::s_pszVarNameFilename    = "krename_filename";
0032 const char *ScriptPlugin::s_pszVarNameExtension   = "krename_extension";
0033 const char *ScriptPlugin::s_pszVarNameDirectory   = "krename_directory";
0034 
0035 enum EVarType {
0036     eVarType_String = 0,
0037     eVarType_Int,
0038     eVarType_Double,
0039     eVarType_Bool
0040 };
0041 
0042 // Wraps around QJSEngine::evaluate to force a timeout on it.
0043 // Doesn't require manual starting, result() takes care of that.
0044 // NB: Thy shalt not touch the engine after constructing the thread!
0045 class EvaluateThread : public QThread
0046 {
0047 public:
0048     EvaluateThread(QJSEngine *engine, const QString &script, QObject *parent = nullptr)
0049         : QThread(parent)
0050         , m_engine(engine)
0051         , m_script(script)
0052     {
0053         // Reset in case a previous eval was interrupted
0054         m_engine->setInterrupted(false);
0055     }
0056 
0057     void run() override
0058     {
0059         m_value = m_engine->evaluate(m_script);
0060     }
0061 
0062     // Start thread and gather a result from the engine, either because it finished or because
0063     // we forced an interrupt after timeout.
0064     QJSValue result()
0065     {
0066         start();
0067         wait(m_timeout);
0068         if (!isFinished()) {
0069             // This function is called by another thread and potentially races here as the run() may finish
0070             // between the condition and the interrupt call. This still doesn't require locking though.
0071             // m_value is only set once and only read once whether the engine is needlessly in interruption state
0072             // has no impact anymore if evaluate() returned already.
0073             m_engine->setInterrupted(true);
0074             wait(); // this is expected to return eventually!
0075         }
0076         return m_value;
0077     }
0078 
0079 private:
0080     QJSEngine *m_engine = nullptr;
0081     QString m_script;
0082     QJSValue m_value;
0083     const QDeadlineTimer m_timeout {30000 /* ms */};
0084 };
0085 
0086 ScriptPlugin::ScriptPlugin(PluginLoader *loader)
0087     : QObject(),
0088       Plugin(loader), m_parent(nullptr)
0089 {
0090     m_name = i18n("JavaScript Plugin");
0091     m_icon = "applications-development";
0092     m_menu   = new QMenu();
0093     m_widget = new Ui::ScriptPluginWidget();
0094 
0095     this->addSupportedToken("js;.*");
0096 
0097     m_help.append("[js;4+5];;" + i18n("Insert a snippet of JavaScript code (4+5 in this case)"));
0098 
0099     m_menu->addAction(i18n("Index of the current file"), this, &ScriptPlugin::slotInsertIndex);
0100     m_menu->addAction(i18n("URL of the current file"), this, &ScriptPlugin::slotInsertUrl);
0101     m_menu->addAction(i18n("Filename of the current file"), this, &ScriptPlugin::slotInsertFilename);
0102     m_menu->addAction(i18n("Extension of the current file"), this, &ScriptPlugin::slotInsertExtension);
0103     m_menu->addAction(i18n("Directory of the current file"), this, &ScriptPlugin::slotInsertDirectory);
0104 }
0105 
0106 ScriptPlugin::~ScriptPlugin()
0107 {
0108     delete m_widget;
0109     delete m_menu;
0110 }
0111 
0112 QString ScriptPlugin::processFile(BatchRenamer *b, int index,
0113                                   const QString &filenameOrToken, EPluginType)
0114 {
0115     QString token(filenameOrToken);
0116     QString script;
0117     QString definitions = m_widget->textCode->toPlainText();
0118 
0119     if (token.contains(";")) {
0120         script = token.section(';', 1);   // all sections from 1 to the last
0121         token  = token.section(';', 0, 0).toLower();
0122     } else {
0123         token = token.toLower();
0124     }
0125 
0126     if (token == "js") {
0127         // Setup interpreter
0128         const KRenameFile &file = b->files()->at(index);
0129         initKRenameVars(file, index);
0130 
0131         // Make sure definitions are executed first
0132         script = definitions + '\n' + script;
0133 
0134         EvaluateThread thread(&m_engine, script);
0135         const QJSValue result = thread.result();
0136         if (result.isError()) {
0137             qDebug() << "JavaScript Error:" << result.toString();
0138             return QString();
0139         }
0140 
0141         return result.toString();
0142     }
0143 
0144     return QString();
0145 }
0146 
0147 const QIcon ScriptPlugin::icon() const
0148 {
0149     return QIcon::fromTheme(m_icon);
0150 }
0151 
0152 void ScriptPlugin::createUI(QWidget *parent) const
0153 {
0154     QStringList labels;
0155     labels << i18n("Variable Name");
0156     labels << i18n("Initial Value");
0157 
0158     const_cast<ScriptPlugin *>(this)->m_parent = parent;
0159     m_widget->setupUi(parent);
0160     m_widget->listVariables->setColumnCount(2);
0161     m_widget->listVariables->setHeaderLabels(labels);
0162 
0163     connect(m_widget->listVariables, &QTreeWidget::itemSelectionChanged,
0164             this, &ScriptPlugin::slotEnableControls);
0165     connect(m_widget->buttonAdd, &QPushButton::clicked,
0166             this, &ScriptPlugin::slotAdd);
0167     connect(m_widget->buttonRemove, &QPushButton::clicked,
0168             this, &ScriptPlugin::slotRemove);
0169     connect(m_widget->buttonLoad, &QPushButton::clicked,
0170             this, &ScriptPlugin::slotLoad);
0171     connect(m_widget->buttonSave, &QPushButton::clicked,
0172             this, &ScriptPlugin::slotSave);
0173     connect(m_widget->textCode, &QTextEdit::textChanged,
0174             this, &ScriptPlugin::slotEnableControls);
0175 
0176     const_cast<ScriptPlugin *>(this)->slotEnableControls();
0177 
0178     m_widget->buttonLoad->setIcon(QIcon::fromTheme("document-open"));
0179     m_widget->buttonSave->setIcon(QIcon::fromTheme("document-save-as"));
0180     m_widget->buttonAdd->setIcon(QIcon::fromTheme("list-add"));
0181     m_widget->buttonRemove->setIcon(QIcon::fromTheme("list-remove"));
0182 
0183     m_widget->buttonInsert->setMenu(m_menu);
0184 }
0185 
0186 void ScriptPlugin::initKRenameVars(const KRenameFile &file, int index)
0187 {
0188     // KRename definitions
0189     m_engine.globalObject().setProperty(ScriptPlugin::s_pszVarNameIndex, index);
0190     m_engine.globalObject().setProperty(ScriptPlugin::s_pszVarNameUrl, file.srcUrl().url());
0191     m_engine.globalObject().setProperty(ScriptPlugin::s_pszVarNameFilename, file.srcFilename());
0192     m_engine.globalObject().setProperty(ScriptPlugin::s_pszVarNameExtension, file.srcExtension());
0193     m_engine.globalObject().setProperty(ScriptPlugin::s_pszVarNameDirectory, file.srcDirectory());
0194 
0195     // User definitions, set them only on first file
0196     if (index != 0) {
0197         return;
0198     }
0199 
0200     for (int i = 0; i < m_widget->listVariables->topLevelItemCount(); i++) {
0201         // TODO, we have to know the type of the variable!
0202         QTreeWidgetItem *item = m_widget->listVariables->topLevelItem(i);
0203         if (!item) {
0204             continue;
0205         }
0206 
0207         EVarType eVarType = static_cast<EVarType>(item->data(1, Qt::UserRole).toInt());
0208         const QString &name  = item->text(0);
0209         const QString &value = item->text(1);
0210         switch (eVarType) {
0211         default:
0212         case eVarType_String:
0213             m_engine.globalObject().setProperty(name, value);
0214             break;
0215         case eVarType_Int:
0216             m_engine.globalObject().setProperty(name, value.toInt());
0217             break;
0218         case eVarType_Double:
0219             m_engine.globalObject().setProperty(name, value.toDouble());
0220             break;
0221         case eVarType_Bool:
0222             m_engine.globalObject().setProperty(name, (value.toLower() == "true" ? true : false));
0223             break;
0224         }
0225     }
0226 }
0227 
0228 void ScriptPlugin::insertVariable(const char *name)
0229 {
0230     m_widget->textCode->insertPlainText(QString(name));
0231 }
0232 
0233 void ScriptPlugin::slotEnableControls()
0234 {
0235     bool bEnable = !(m_widget->listVariables->selectedItems().isEmpty());
0236     m_widget->buttonRemove->setEnabled(bEnable);
0237 
0238     bEnable = !m_widget->textCode->toPlainText().isEmpty();
0239     m_widget->buttonSave->setEnabled(bEnable);
0240 }
0241 
0242 void ScriptPlugin::slotAdd()
0243 {
0244     QDialog dialog;
0245     Ui::ScriptPluginDialog dlg;
0246 
0247     dlg.setupUi(&dialog);
0248     dlg.comboType->addItem(i18n("String"), eVarType_String);
0249     dlg.comboType->addItem(i18n("Int"), eVarType_Int);
0250     dlg.comboType->addItem(i18n("Double"), eVarType_Double);
0251     dlg.comboType->addItem(i18n("Boolean"), eVarType_Bool);
0252 
0253     if (dialog.exec() != QDialog::Accepted) {
0254         return;
0255     }
0256 
0257     QString name  = dlg.lineName->text();
0258     QString value = dlg.lineValue->text();
0259 
0260     // Build a Java script statement
0261     QString script = name + " = " + value + ';';
0262 
0263     EvaluateThread thread(&m_engine, script);
0264     const QJSValue result = thread.result();
0265     if (result.isError()) {
0266         KMessageBox::error(m_parent, i18n("A JavaScript error has occurred: ") + result.toString(), this->name());
0267     } else {
0268         QTreeWidgetItem *item = new QTreeWidgetItem();
0269         item->setText(0, name);
0270         item->setText(1, value);
0271         item->setData(1, Qt::UserRole, dlg.comboType->currentData());
0272 
0273         m_widget->listVariables->addTopLevelItem(item);
0274     }
0275 }
0276 
0277 void ScriptPlugin::slotRemove()
0278 {
0279     QTreeWidgetItem *item = m_widget->listVariables->currentItem();
0280     if (item) {
0281         m_widget->listVariables->invisibleRootItem()->removeChild(item);
0282         delete item;
0283     }
0284 }
0285 
0286 void ScriptPlugin::slotLoad()
0287 {
0288     if (!m_widget->textCode->toPlainText().isEmpty() &&
0289             KMessageBox::questionYesNo(m_parent,
0290                                        i18n("All currently entered definitions will be lost. Do you want to continue?"), {},
0291                                        KStandardGuiItem::cont(),
0292                                        KStandardGuiItem::cancel())
0293             == KMessageBox::No) {
0294         return;
0295     }
0296 
0297     QUrl url = QFileDialog::getOpenFileUrl(m_parent, i18n("Select file"),
0298                                            QUrl(ScriptPlugin::s_pszFileDialogLocation));
0299 
0300     if (!url.isEmpty()) {
0301         // Also support remote files
0302         KIO::StoredTransferJob *job = KIO::storedGet(url);
0303         KJobWidgets::setWindow(job, m_parent);
0304         if (job->exec()) {
0305             m_widget->textCode->setPlainText(QString::fromLocal8Bit(job->data()));
0306         } else {
0307             KMessageBox::error(m_parent, job->errorString());
0308         }
0309     }
0310 
0311     slotEnableControls();
0312 }
0313 
0314 void ScriptPlugin::slotSave()
0315 {
0316     QUrl url = QFileDialog::getSaveFileUrl(m_parent, i18n("Select file"),
0317                                            QUrl(ScriptPlugin::s_pszFileDialogLocation));
0318 
0319     if (!url.isEmpty()) {
0320 #if KIO_VERSION >= QT_VERSION_CHECK(5, 69, 0)
0321         KIO::StatJob *statJob = KIO::statDetails(url, KIO::StatJob::DestinationSide, KIO::StatNoDetails);
0322 #else
0323         KIO::StatJob *statJob = KIO::stat(url, KIO::StatJob::DestinationSide, 0);
0324 #endif
0325         statJob->exec();
0326         if (statJob->error() != KIO::ERR_DOES_NOT_EXIST) {
0327             int m = KMessageBox::warningYesNo(m_parent, i18n("The file %1 already exists. "
0328                                               "Do you want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)), {},
0329                                               KStandardGuiItem::overwrite(),
0330                                               KStandardGuiItem::cancel());
0331 
0332             if (m == KMessageBox::No) {
0333                 return;
0334             }
0335         }
0336 
0337         if (url.isLocalFile()) {
0338             QFile file(url.path());
0339             if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
0340                 QTextStream out(&file);
0341                 out << m_widget->textCode->toPlainText();
0342                 out.flush();
0343                 file.close();
0344             } else {
0345                 KMessageBox::error(m_parent, i18n("Unable to open %1 for writing.", url.path()));
0346             }
0347         } else {
0348             KIO::StoredTransferJob *job = KIO::storedPut(m_widget->textCode->toPlainText().toLocal8Bit(), url, -1);
0349             KJobWidgets::setWindow(job, m_parent);
0350             job->exec();
0351             if (job->error()) {
0352                 KMessageBox::error(m_parent, job->errorString());
0353             }
0354         }
0355     }
0356 
0357     slotEnableControls();
0358 }
0359 
0360 void ScriptPlugin::slotTest()
0361 {
0362 }
0363 
0364 void ScriptPlugin::slotInsertIndex()
0365 {
0366     this->insertVariable(ScriptPlugin::s_pszVarNameIndex);
0367 }
0368 
0369 void ScriptPlugin::slotInsertUrl()
0370 {
0371     this->insertVariable(ScriptPlugin::s_pszVarNameUrl);
0372 }
0373 
0374 void ScriptPlugin::slotInsertFilename()
0375 {
0376     this->insertVariable(ScriptPlugin::s_pszVarNameFilename);
0377 }
0378 
0379 void ScriptPlugin::slotInsertExtension()
0380 {
0381     this->insertVariable(ScriptPlugin::s_pszVarNameExtension);
0382 }
0383 
0384 void ScriptPlugin::slotInsertDirectory()
0385 {
0386     this->insertVariable(ScriptPlugin::s_pszVarNameDirectory);
0387 }
0388 
0389 void ScriptPlugin::loadConfig(KConfigGroup &group)
0390 {
0391     QStringList  variableNames;
0392     QStringList  variableValues;
0393     QVariantList variableTypes;
0394 
0395     variableNames  = group.readEntry("JavaScriptVariableNames",  variableNames);
0396     variableValues = group.readEntry("JavaScriptVariableValues", variableValues);
0397     variableTypes  = group.readEntry("JavaScriptVariableTypes", variableTypes);
0398 
0399     int min = qMin(variableNames.count(), variableValues.count());
0400     min = qMin(min, variableTypes.count());
0401 
0402     for (int i = 0; i < min; i++) {
0403         QTreeWidgetItem *item = new QTreeWidgetItem();
0404         item->setText(0, variableNames[i]);
0405         item->setText(1, variableValues[i]);
0406         item->setData(1, Qt::UserRole, variableTypes[i]);
0407 
0408         m_widget->listVariables->addTopLevelItem(item);
0409     }
0410 
0411     m_widget->textCode->setPlainText(group.readEntry("JavaScriptDefinitions", QString()));
0412 }
0413 
0414 void ScriptPlugin::saveConfig(KConfigGroup &group) const
0415 {
0416     QStringList  variableNames;
0417     QStringList  variableValues;
0418     QVariantList variableTypes;
0419 
0420     for (int i = 0; i < m_widget->listVariables->topLevelItemCount(); i++) {
0421         QTreeWidgetItem *item = m_widget->listVariables->topLevelItem(i);
0422         if (item) {
0423             variableNames  << item->text(0);
0424             variableValues << item->text(1);
0425             variableTypes  << item->data(1, Qt::UserRole);
0426         }
0427     }
0428 
0429     group.writeEntry("JavaScriptVariableNames",  variableNames);
0430     group.writeEntry("JavaScriptVariableValues", variableValues);
0431     group.writeEntry("JavaScriptVariableTypes",  variableTypes);
0432     group.writeEntry("JavaScriptDefinitions", m_widget->textCode->toPlainText());
0433 }
0434