mirror of
https://github.com/huderlem/porymap.git
synced 2026-07-19 00:43:33 -05:00
Add script identification, require permission to r/w external files
This commit is contained in:
parent
6044a381c6
commit
26404168f5
|
|
@ -2062,6 +2062,8 @@ All utility functions are callable via the global ``utility`` object.
|
|||
|
||||
Reads the contents of the file specified by ``path``.
|
||||
|
||||
If ``path`` refers to a file outside the project directory the user will be warned about this and may choose to cancel the read.
|
||||
|
||||
:param path: The path of the file to read. The path may either be relative to the project directory or absolute.
|
||||
:type path: string
|
||||
:returns: If reading the file was successful ``content`` will contain the contents of the file and ``error`` will be empty. Otherwise ``content`` will be empty and ``error`` will contain an error message.
|
||||
|
|
@ -2071,6 +2073,8 @@ All utility functions are callable via the global ``utility`` object.
|
|||
|
||||
Writes ``content`` to the file specified by ``path``. The file is created if it does not already exist.
|
||||
|
||||
If ``path`` refers to a file outside the project directory the user will be warned about this and may choose to cancel the write.
|
||||
|
||||
:param path: The path of the file to write. The path to an existing file may either be relative to the project directory or absolute. If the file does not already exist the path must be absolute.
|
||||
:type path: string
|
||||
:param content: The text to write to the file.
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ public:
|
|||
QFont applicationFont;
|
||||
QFont mapListFont;
|
||||
int imageExportColorSpaceId;
|
||||
QMap<QString,QString> trustedScriptHashes;
|
||||
|
||||
protected:
|
||||
virtual void parseConfigKeyValue(QString key, QString value) override;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ namespace Util {
|
|||
void show(QWidget *w);
|
||||
QColorSpace toColorSpace(int colorSpaceInt);
|
||||
QString mkpath(const QString& dirPath);
|
||||
QString getFileHash(const QString &filepath);
|
||||
}
|
||||
|
||||
#endif // UTILITY_H
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
#define SCRIPTING_H
|
||||
|
||||
#include <QStringList>
|
||||
#include <QStack>
|
||||
#include <QFileInfo>
|
||||
#include "scriptutility.h"
|
||||
#include "utility.h"
|
||||
|
||||
class Block;
|
||||
class Tile;
|
||||
|
|
@ -73,17 +76,58 @@ public:
|
|||
static const QImage * getImage(const QString &filepath, bool useCache);
|
||||
static QJSValue dialogInput(QJSValue input, bool selectedOk);
|
||||
static QJSValue fileResponse(const QString &s, bool isError);
|
||||
static bool checkFilePermissions(const QString &filepath);
|
||||
|
||||
private:
|
||||
MainWindow *mainWindow;
|
||||
QJSEngine *engine;
|
||||
QStringList filepaths;
|
||||
QList<QJSValue> modules;
|
||||
|
||||
class Script {
|
||||
public:
|
||||
Script() {};
|
||||
|
||||
QString filepath() const { return m_filepath; }
|
||||
QString fileName() const {
|
||||
QFileInfo fileInfo(m_filepath);
|
||||
return fileInfo.fileName();
|
||||
}
|
||||
void setFilepath(const QString &filepath) { m_filepath = filepath; }
|
||||
|
||||
QJSValue module() const { return m_module; }
|
||||
void setModule(const QJSValue &module) { m_module = module; }
|
||||
|
||||
QString hash() {
|
||||
if (m_hash.isEmpty()) {
|
||||
// We won't need to check whether most scripts are trusted,
|
||||
// so only calculate the hash when it's first requested.
|
||||
m_hash = Util::getFileHash(m_filepath);
|
||||
}
|
||||
return m_hash;
|
||||
}
|
||||
private:
|
||||
QString m_filepath;
|
||||
QJSValue m_module;
|
||||
QString m_hash;
|
||||
};
|
||||
QList<QSharedPointer<Script>> scripts;
|
||||
|
||||
// Tracks the scripts that are currently being evaluated.
|
||||
// This is useful for identifying which script is responsible
|
||||
// for calling a function at any point in time.
|
||||
// Because executing a callback/action in one script may trigger
|
||||
// a callback/action in a different script we may need to keep
|
||||
// track of multiple scripts executing at once.
|
||||
QStack<QSharedPointer<Script>> scriptExecutionStack;
|
||||
|
||||
QMap<QString, const QImage*> imageCache;
|
||||
ScriptUtility *scriptUtility;
|
||||
|
||||
void loadModules(const QStringList &moduleFiles);
|
||||
void invokeCallback(CallbackType type, QJSValueList args);
|
||||
void loadScript(const QString &filepath);
|
||||
void invokeCallback(CallbackType type, const QJSValueList &args);
|
||||
void invokeCallback(QSharedPointer<const Script> script, CallbackType type, const QJSValueList &args);
|
||||
bool invokeAction(QSharedPointer<const Script> script, const QString &functionName);
|
||||
QSharedPointer<Script> getActiveScript() const;
|
||||
bool askForTrust(QSharedPointer<Script> script, const QString &reason);
|
||||
};
|
||||
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
class Message : public QMessageBox {
|
||||
public:
|
||||
Message(QMessageBox::Icon icon, const QString &text, QMessageBox::StandardButtons buttons, QWidget *parent);
|
||||
void addStandardButtons(QMessageBox::StandardButtons buttons) { setStandardButtons(standardButtons() | buttons); }
|
||||
};
|
||||
|
||||
// Basic error message with an 'Ok' button.
|
||||
|
|
|
|||
|
|
@ -386,6 +386,7 @@ void PorymapConfig::reset() {
|
|||
// so we export images without one and let them handle it.
|
||||
this->imageExportColorSpaceId = 0;
|
||||
#endif
|
||||
this->trustedScriptHashes.clear();
|
||||
}
|
||||
|
||||
void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
|
||||
|
|
@ -576,6 +577,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
|
|||
LogType type = static_cast<LogType>(getConfigInteger(key, typeString, 0, 2));
|
||||
this->statusBarLogTypes.insert(type);
|
||||
}
|
||||
} else if (key.startsWith("trusted_script_hash/")) {
|
||||
this->trustedScriptHashes.insert(key.mid(QStringLiteral("trusted_script_hash/").length()), value);
|
||||
} else if (key == "application_font") {
|
||||
this->applicationFont = QFont();
|
||||
this->applicationFont.fromString(value);
|
||||
|
|
@ -681,7 +684,10 @@ QMap<QString, QString> PorymapConfig::getKeyValueMap() {
|
|||
map.insert("application_font", this->applicationFont.toString());
|
||||
map.insert("map_list_font", this->mapListFont.toString());
|
||||
map.insert("image_export_color_space_id", QString::number(this->imageExportColorSpaceId));
|
||||
|
||||
for (auto it = this->trustedScriptHashes.constBegin(); it != this->trustedScriptHashes.constEnd(); it++) {
|
||||
if (it.value().isEmpty() || it.key().isEmpty()) continue;
|
||||
map.insert("trusted_script_hash/" + it.key(), it.value());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <QRegularExpression>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QCryptographicHash>
|
||||
|
||||
// Sometimes we want to sort names alphabetically to make them easier to find in large combo box lists.
|
||||
// QStringList::sort (as of writing) can only sort numbers in lexical order, which has an undesirable
|
||||
|
|
@ -161,3 +162,11 @@ QString Util::mkpath(const QString& dirPath) {
|
|||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString Util::getFileHash(const QString &filepath) {
|
||||
QFile file(filepath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return QString();
|
||||
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||
return hash.addData(&file) ? QString(hash.result().toHex()) : QString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,10 +144,16 @@ QString ScriptUtility::detectProjectPath(const QString &path) const {
|
|||
}
|
||||
|
||||
QJSValue ScriptUtility::readTextFile(const QString &path) const {
|
||||
QFile file(detectProjectPath(path));
|
||||
if (path.isEmpty()) return Scripting::fileResponse(QStringLiteral("Failed to open file for reading: No path specified."), true);
|
||||
|
||||
const QString fullPath = detectProjectPath(path);
|
||||
QFile file(fullPath);
|
||||
if (!file.exists()) {
|
||||
return Scripting::fileResponse(QString("Failed to open file '%1' for reading: No such file.").arg(path), true);
|
||||
}
|
||||
if (!Scripting::checkFilePermissions(fullPath)) {
|
||||
return Scripting::fileResponse(QString("Failed to open file '%1' for reading: Permission denied.").arg(path), true);
|
||||
}
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return Scripting::fileResponse(QString("Failed to open file '%1' for reading.").arg(path), true);
|
||||
}
|
||||
|
|
@ -155,7 +161,14 @@ QJSValue ScriptUtility::readTextFile(const QString &path) const {
|
|||
}
|
||||
|
||||
QString ScriptUtility::writeTextFile(const QString &path, const QString &content, bool append) const {
|
||||
QFile file(detectProjectPath(path));
|
||||
if (path.isEmpty()) return QStringLiteral("Failed to open file for writing: No path specified.");
|
||||
|
||||
const QString fullPath = detectProjectPath(path);
|
||||
if (!Scripting::checkFilePermissions(fullPath)) {
|
||||
return QString("Failed to open file '%1' for writing: Permission denied.").arg(path);
|
||||
}
|
||||
|
||||
QFile file(fullPath);
|
||||
QIODevice::OpenMode flags = QIODevice::WriteOnly | QIODevice::Text;
|
||||
if (append) flags |= QIODevice::Append;
|
||||
if (!file.open(flags)) {
|
||||
|
|
|
|||
|
|
@ -44,9 +44,8 @@ Scripting::Scripting(MainWindow *mainWindow) {
|
|||
const QList<bool> enabled = userConfig.getCustomScriptsEnabled();
|
||||
for (int i = 0; i < paths.length(); i++) {
|
||||
if (enabled.value(i, true))
|
||||
this->filepaths.append(paths.at(i));
|
||||
loadScript(paths.at(i));
|
||||
}
|
||||
this->loadModules(this->filepaths);
|
||||
this->scriptUtility = new ScriptUtility(mainWindow);
|
||||
}
|
||||
|
||||
|
|
@ -54,33 +53,35 @@ Scripting::~Scripting() {
|
|||
if (mainWindow) mainWindow->clearOverlay();
|
||||
this->engine->setInterrupted(true);
|
||||
qDeleteAll(this->imageCache);
|
||||
this->scripts.clear();
|
||||
this->scriptExecutionStack.clear();
|
||||
delete this->engine;
|
||||
delete this->scriptUtility;
|
||||
}
|
||||
|
||||
void Scripting::loadModules(const QStringList &moduleFiles) {
|
||||
for (const auto &filepath : moduleFiles) {
|
||||
if (filepath.isEmpty()) continue;
|
||||
QJSValue module;
|
||||
QString validPath = Project::getExistingFilepath(filepath);
|
||||
if (validPath.isEmpty()) {
|
||||
logError(QString("Failed to find script file '%1'.").arg(filepath));
|
||||
} else {
|
||||
module = this->engine->importModule(validPath);
|
||||
}
|
||||
if (validPath.isEmpty() || tryErrorJS(module)) {
|
||||
QMessageBox messageBox(this->mainWindow);
|
||||
messageBox.setText("Failed to load script");
|
||||
messageBox.setInformativeText(QString("An error occurred while loading custom script file '%1'").arg(filepath));
|
||||
messageBox.setDetailedText(getMostRecentError());
|
||||
messageBox.setIcon(QMessageBox::Warning);
|
||||
messageBox.addButton(QMessageBox::Ok);
|
||||
messageBox.exec();
|
||||
continue;
|
||||
}
|
||||
logInfo(QString("Successfully loaded custom script file '%1'").arg(filepath));
|
||||
this->modules.append(module);
|
||||
void Scripting::loadScript(const QString &filepath) {
|
||||
if (filepath.isEmpty()) return;
|
||||
|
||||
auto script = QSharedPointer<Script>(new Script());
|
||||
script->setFilepath(Project::getExistingFilepath(filepath));
|
||||
if (script->filepath().isEmpty()) {
|
||||
logError(QString("Failed to find script file '%1'.").arg(filepath));
|
||||
} else {
|
||||
script->setModule(this->engine->importModule(script->filepath()));
|
||||
}
|
||||
if (script->filepath().isEmpty() || tryErrorJS(script->module())) {
|
||||
QMessageBox messageBox(this->mainWindow);
|
||||
messageBox.setText("Failed to load script");
|
||||
messageBox.setInformativeText(QString("An error occurred while loading custom script file '%1'").arg(filepath));
|
||||
messageBox.setDetailedText(getMostRecentError());
|
||||
messageBox.setIcon(QMessageBox::Warning);
|
||||
messageBox.addButton(QMessageBox::Ok);
|
||||
messageBox.exec();
|
||||
return;
|
||||
}
|
||||
|
||||
logInfo(QString("Successfully loaded custom script file '%1'").arg(filepath));
|
||||
this->scripts.append(script);
|
||||
}
|
||||
|
||||
void Scripting::populateGlobalObject(MainWindow *mainWindow) {
|
||||
|
|
@ -158,32 +159,31 @@ bool Scripting::tryErrorJS(QJSValue js) {
|
|||
return true;
|
||||
}
|
||||
|
||||
void Scripting::invokeCallback(CallbackType type, QJSValueList args) {
|
||||
for (QJSValue module : this->modules) {
|
||||
QString functionName = callbackFunctions[type];
|
||||
QJSValue callbackFunction = module.property(functionName);
|
||||
if (tryErrorJS(callbackFunction)) continue;
|
||||
|
||||
QJSValue result = callbackFunction.call(args);
|
||||
if (tryErrorJS(result)) continue;
|
||||
void Scripting::invokeCallback(CallbackType type, const QJSValueList &args) {
|
||||
for (const auto& script : this->scripts) {
|
||||
this->scriptExecutionStack.push(script);
|
||||
invokeCallback(script, type, args);
|
||||
this->scriptExecutionStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void Scripting::invokeCallback(QSharedPointer<const Script> script, CallbackType type, const QJSValueList &args) {
|
||||
QString functionName = callbackFunctions[type];
|
||||
QJSValue callbackFunction = script->module().property(functionName);
|
||||
if (tryErrorJS(callbackFunction)) return;
|
||||
tryErrorJS(callbackFunction.call(args));
|
||||
}
|
||||
|
||||
void Scripting::invokeAction(int actionIndex) {
|
||||
if (!instance || !instance->scriptUtility) return;
|
||||
QString functionName = instance->scriptUtility->getActionFunctionName(actionIndex);
|
||||
if (functionName.isEmpty()) return;
|
||||
|
||||
bool foundFunction = false;
|
||||
for (QJSValue module : instance->modules) {
|
||||
QJSValue callbackFunction = module.property(functionName);
|
||||
if (callbackFunction.isUndefined() || !callbackFunction.isCallable())
|
||||
continue;
|
||||
foundFunction = true;
|
||||
if (tryErrorJS(callbackFunction)) continue;
|
||||
|
||||
QJSValue result = callbackFunction.call(QJSValueList());
|
||||
if (tryErrorJS(result)) continue;
|
||||
for (const auto& script : instance->scripts) {
|
||||
instance->scriptExecutionStack.push(script);
|
||||
if (instance->invokeAction(script, functionName)) foundFunction = true;
|
||||
instance->scriptExecutionStack.pop();
|
||||
}
|
||||
if (!foundFunction) {
|
||||
logError(QString("Unknown custom script function '%1'").arg(functionName));
|
||||
|
|
@ -196,6 +196,21 @@ void Scripting::invokeAction(int actionIndex) {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns true if the script had the specified function and tried to execute it (whether successful or not),
|
||||
// returns false if the script did not have the specified function.
|
||||
// TODO: Now that we can individually identify scripts, this should never happen.
|
||||
// We can always call the function using the script that registered this action.
|
||||
bool Scripting::invokeAction(QSharedPointer<const Script> script, const QString &functionName) {
|
||||
QJSValue callbackFunction = script->module().property(functionName);
|
||||
if (callbackFunction.isUndefined() || !callbackFunction.isCallable())
|
||||
return false;
|
||||
if (tryErrorJS(callbackFunction)) return true;
|
||||
|
||||
QJSValue result = callbackFunction.call(QJSValueList());
|
||||
tryErrorJS(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Scripting::cb_ProjectOpened(QString projectPath) {
|
||||
if (!instance) return;
|
||||
|
||||
|
|
@ -412,6 +427,39 @@ QJSValue Scripting::fileResponse(const QString &s, bool isError) {
|
|||
return obj;
|
||||
}
|
||||
|
||||
QSharedPointer<Scripting::Script> Scripting::getActiveScript() const {
|
||||
return this->scriptExecutionStack.isEmpty() ? nullptr : this->scriptExecutionStack.top();
|
||||
}
|
||||
|
||||
bool Scripting::checkFilePermissions(const QString &filepath) {
|
||||
// Scripts are allowed to read/write files inside the project directory without explicit permission.
|
||||
// Normal file permission rules will still apply.
|
||||
if (QDir::cleanPath(filepath).startsWith(QDir::cleanPath(projectConfig.root()))) return true;
|
||||
|
||||
if (!instance) return false;
|
||||
QSharedPointer<Script> script = instance->getActiveScript();
|
||||
if (!script || script->hash().isEmpty()) return false;
|
||||
|
||||
if (porymapConfig.trustedScriptHashes.contains(script->hash()))
|
||||
return true; // User has already opted to trust this script
|
||||
|
||||
QString reason = QString("'%1' would like to access files outside your project folder.").arg(script->filepath());
|
||||
return instance->askForTrust(script, reason);
|
||||
}
|
||||
|
||||
bool Scripting::askForTrust(QSharedPointer<Script> script, const QString &reason) {
|
||||
QuestionMessage messageBox(QString("Allow '%1' to continue?").arg(script->fileName()), this->mainWindow);
|
||||
messageBox.setInformativeText(reason);
|
||||
if (messageBox.exec() == QMessageBox::Yes) {
|
||||
// User has opted to trust this script. If this script had an old hash saved, remove that first.
|
||||
QList<QString> oldHashes = porymapConfig.trustedScriptHashes.keys(script->filepath());
|
||||
for (const auto& oldHash : oldHashes) porymapConfig.trustedScriptHashes.remove(oldHash);
|
||||
porymapConfig.trustedScriptHashes.insert(script->hash(), script->filepath());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QJSEngine *Scripting::getEngine() {
|
||||
return instance->engine;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user