Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plugin permissions and IO API #5231

Merged
merged 37 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bcdbcb8
Add basicest plugin permission parsing
Mm2PL Mar 1, 2024
a3ff7de
Add Plugin::hasFSPermissionFor utility function
Mm2PL Mar 3, 2024
4d1f827
Add wrappers for (almost all) IO functions
Mm2PL Mar 3, 2024
4313d0b
Disallow commas in file paths
Mm2PL Mar 3, 2024
4a01239
Unrelated: Ensure that lua::pop(lua_State*, T*) is always balanced,
Mm2PL Mar 3, 2024
923e3b6
Restrict plugins' io to only a data directory
Mm2PL Mar 4, 2024
6dde7bc
This is documented to be <algorithm> instead
Mm2PL Mar 4, 2024
c83ab5b
Add stubs for io.popen and io.tmpfile to ease porting of existing Lua…
Mm2PL Mar 4, 2024
2c5bbc7
Make open relative to data directory instead of plugin
Mm2PL Mar 4, 2024
b5d4e4e
Document relative file behavior
Mm2PL Mar 4, 2024
b77a93c
rewrite string - how did i write this
Mm2PL Mar 4, 2024
3983fc7
Rename PluginPermission::toHtmlEscaped to toHtml
Mm2PL Mar 4, 2024
a00382d
Reformat PluginPermission.hpp
Mm2PL Mar 4, 2024
891f3da
Actually use isValid
Mm2PL Mar 4, 2024
9e850c6
Document behavior of lua::pop()
Mm2PL Mar 5, 2024
47b3578
Reword error strings and validate the string in LuaFileMode constructor
Mm2PL Mar 5, 2024
0e526c5
Reformat IOWrapper.cpp
Mm2PL Mar 5, 2024
5241b80
Make errors conform better to how Lua does them
Mm2PL Mar 5, 2024
4e9ca37
i fucking hate prettier
Mm2PL Mar 5, 2024
98d9b7f
changelog
Mm2PL Mar 5, 2024
3c877d5
ShutUpOldGcc names an order.
Mm2PL Mar 5, 2024
e0c9be6
control DOES NOT REACH THE end of non-void function
Mm2PL Mar 5, 2024
2655251
ah old compilers what would we do without them
Mm2PL Mar 5, 2024
df7d2c8
Merge remote-tracking branch 'origin/master' into feature/plugin-perm…
Mm2PL Mar 5, 2024
708d273
Document permissions
Mm2PL Mar 7, 2024
5357be2
Document IO api
Mm2PL Mar 7, 2024
11c5bb3
Include header links inside of docs/wip-plugins.md
Mm2PL Mar 7, 2024
d6fa4d4
Unrelated: Make sure c2.Channel functions get a channel instead of any
Mm2PL Mar 7, 2024
c9ea3bc
shut up uglier
Mm2PL Mar 7, 2024
08ef872
Use "enum" instead of "examples"
Mm2PL Mar 7, 2024
e4bbedd
Unrelated: Clarify plugin schema license field.
Mm2PL Mar 7, 2024
115f23e
Add missing #pragma once and reformat includes
Mm2PL Mar 9, 2024
7b927f2
Only run error copy loop if there are errors
Mm2PL Mar 9, 2024
4ed9e77
Change permission example snippets to show the structure of info.json
Mm2PL Mar 9, 2024
555683f
Merge branch 'master' of github.com:Chatterino/chatterino2 into featu…
Mm2PL Mar 9, 2024
de45a05
s/err/perm
Mm2PL Mar 9, 2024
5a5e3ae
Do not load data as code :)
Mm2PL Mar 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/plugin-info.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@
"description": "A small description of your license.",
"examples": ["MIT", "GPL-2.0-or-later"]
},
"permissions": {
"type": "array",
"description": "The permissions the plugin needs to work.",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string",
"examples": ["FilesystemRead", "FilesystemWrite"]
}
}
}
},
"$schema": { "type": "string" }
},
"required": ["name", "description", "authors", "version", "license"]
Expand Down
4 changes: 4 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,12 @@ set(SOURCE_FILES

controllers/plugins/api/ChannelRef.cpp
controllers/plugins/api/ChannelRef.hpp
controllers/plugins/api/IOWrapper.cpp
controllers/plugins/api/IOWrapper.hpp
controllers/plugins/LuaAPI.cpp
controllers/plugins/LuaAPI.hpp
controllers/plugins/PluginPermission.cpp
controllers/plugins/PluginPermission.hpp
controllers/plugins/Plugin.cpp
controllers/plugins/Plugin.hpp
controllers/plugins/PluginController.hpp
Expand Down
9 changes: 3 additions & 6 deletions src/controllers/plugins/LuaUtilities.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,11 @@ bool pop(lua_State *L, T *out, StackIdx idx = -1)
{
StackGuard guard(L, -1);
auto ok = peek(L, out, idx);
if (ok)
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
if (idx < 0)
{
if (idx < 0)
{
idx = lua_gettop(L) + idx + 1;
}
lua_remove(L, idx);
idx = lua_gettop(L) + idx + 1;
}
lua_remove(L, idx);
return ok;
}

Expand Down
57 changes: 57 additions & 0 deletions src/controllers/plugins/Plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# include <QJsonArray>
# include <QJsonObject>

# include <algorithm>
# include <unordered_map>
# include <unordered_set>

Expand Down Expand Up @@ -111,6 +112,45 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
QString("version is not a string (its type is %1)").arg(type));
this->version = semver::version(0, 0, 0);
}
auto permsObj = obj.value("permissions");
if (!permsObj.isUndefined())
{
if (!permsObj.isArray())
{
QString type = magic_enum::enum_name(permsObj.type()).data();
this->errors.emplace_back(
QString("permissions is not an array (its type is %1)")
.arg(type));
return;
}

auto permsArr = permsObj.toArray();
for (int i = 0; i < permsArr.size(); i++)
{
const auto &t = permsArr.at(i);
if (!t.isObject())
{
QString type = magic_enum::enum_name(t.type()).data();
this->errors.push_back(QString("permissions element #%1 is not "
"an object (its type is %2)")
.arg(i)
.arg(type));
return;
}
auto parsed = PluginPermission(t.toObject());
for (const auto &err : parsed.errors)
{
this->errors.push_back(
QString("permissions element #%1: %2").arg(i).arg(err));
}
if (parsed.errors.empty())
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
{
// ensure no invalid permissions slip through this
this->permissions.push_back(parsed);
}
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
}
}

auto tagsObj = obj.value("tags");
if (!tagsObj.isUndefined())
{
Expand Down Expand Up @@ -201,5 +241,22 @@ void Plugin::removeTimeout(QTimer *timer)
}
}

bool Plugin::hasFSPermissionFor(bool write, const QString &path)
{
auto canon = QUrl(this->loadDirectory().absolutePath() + "/");
if (!canon.isParentOf(path))
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
{
return false;
}

using PType = PluginPermission::Type;
auto typ = write ? PType::FilesystemWrite : PType::FilesystemRead;

return std::ranges::any_of(this->meta.permissions,
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
[typ](const auto &p) -> bool {
return p.type == typ;
});
}

} // namespace chatterino
#endif
10 changes: 10 additions & 0 deletions src/controllers/plugins/Plugin.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# include "Application.hpp"
# include "controllers/plugins/LuaAPI.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "controllers/plugins/PluginPermission.hpp"

# include <QDir>
# include <QString>
Expand Down Expand Up @@ -42,6 +43,8 @@ struct PluginMeta {
// optionally tags that might help in searching for the plugin
std::vector<QString> tags;

std::vector<PluginPermission> permissions;

// errors that occurred while parsing info.json
std::vector<QString> errors;

Expand Down Expand Up @@ -88,6 +91,11 @@ class Plugin
return this->loadDirectory_;
}

QDir dataDirectory() const
{
return this->loadDirectory_.absoluteFilePath("data");
}

// Note: The CallbackFunction object's destructor will remove the function from the lua stack
using LuaCompletionCallback =
lua::CallbackFunction<lua::api::CompletionList, QString, QString, int,
Expand Down Expand Up @@ -130,6 +138,8 @@ class Plugin
int addTimeout(QTimer *timer);
void removeTimeout(QTimer *timer);

bool hasFSPermissionFor(bool write, const QString &path);

private:
QDir loadDirectory_;
lua_State *state_;
Expand Down
50 changes: 49 additions & 1 deletion src/controllers/plugins/PluginController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# include "controllers/commands/CommandContext.hpp"
# include "controllers/commands/CommandController.hpp"
# include "controllers/plugins/api/ChannelRef.hpp"
# include "controllers/plugins/api/IOWrapper.hpp"
# include "controllers/plugins/LuaAPI.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "messages/MessageBuilder.hpp"
Expand Down Expand Up @@ -140,6 +141,8 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
luaL_requiref(L, reg.name, reg.func, int(true));
lua_pop(L, 1);
}
luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, int(false));
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);

// NOLINTNEXTLINE(*-avoid-c-arrays)
static const luaL_Reg c2Lib[] = {
Expand Down Expand Up @@ -234,8 +237,51 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
lua::push(L, QString(pluginDir.absolutePath()));
lua_pushcclosure(L, lua::api::searcherAbsolute, 1);
lua_seti(L, -2, 3);
lua_pop(L, 2); // remove package, package.searchers

lua_pop(L, 3); // remove gtable, package, package.searchers
// NOLINTNEXTLINE(*-avoid-c-arrays)
static const luaL_Reg ioLib[] = {
{"close", lua::api::io_close},
{"flush", lua::api::io_flush},
{"input", lua::api::io_input},
{"lines", lua::api::io_lines},
{"open", lua::api::io_open},
{"output", lua::api::io_output},
{"read", lua::api::io_read},
{"write", lua::api::io_write},
// type = realio.type
{nullptr, nullptr},
};
// TODO: io.popen stub
auto iolibIdx = lua::pushEmptyTable(L, 1);
luaL_setfuncs(L, ioLib, 0);

// set ourio.type = realio.type
lua_pushvalue(L, iolibIdx);
lua_getfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
lua_getfield(L, -1, "type");
lua_remove(L, -2); // remove realio
lua_setfield(L, iolibIdx, "type");
lua_pop(L, 1); // still have iolib on top of stack

lua_pushvalue(L, iolibIdx);
lua_setfield(L, gtable, "io");

lua_pushvalue(L, iolibIdx);
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_C2_IO_NAME);

luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
lua_pushvalue(L, iolibIdx);
lua_setfield(L, -2, "io");

lua_pop(L, 3); // remove gtable, iolib, LOADED

// Don't give plugins the option to shit into our stdio
lua_pushnil(L);
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_input");

lua_pushnil(L);
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_output");
}

void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
Expand Down Expand Up @@ -263,6 +309,8 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
<< meta.name << ") because it is disabled";
return;
}
temp->dataDirectory().mkpath(".");

qCDebug(chatterinoLua) << "Running lua file:" << index;
int err = luaL_dofile(l, index.absoluteFilePath().toStdString().c_str());
if (err != 0)
Expand Down
45 changes: 45 additions & 0 deletions src/controllers/plugins/PluginPermission.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/PluginPermission.hpp"

# include <magic_enum/magic_enum.hpp>
# include <QJsonObject>

namespace chatterino {

PluginPermission::PluginPermission(const QJsonObject &obj)
{
auto jsontype = obj.value("type");
if (!jsontype.isString())
{
QString tn = magic_enum::enum_name(jsontype.type()).data();
this->errors.emplace_back(QString("permission type is defined but is "
"not a string (its type is %1)")
.arg(tn));
}
auto strtype = jsontype.toString().toStdString();
auto opt = magic_enum::enum_cast<PluginPermission::Type>(
strtype, magic_enum::case_insensitive);
if (!opt.has_value())
{
this->errors.emplace_back(QString("permission type is an unknown (%1)")
.arg(jsontype.toString()));
return; // There is no more data to get, we don't know what to do
}
this->type = opt.value();
}

QString PluginPermission::toHtmlEscaped() const
{
switch (this->type)
{
case PluginPermission::Type::FilesystemRead:
return "In its data directory.";
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
case PluginPermission::Type::FilesystemWrite:
return "Write to or create files in its data directory";
default:
assert(false && "invalid PluginPermission type in toString()");
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
}
}

} // namespace chatterino
#endif
27 changes: 27 additions & 0 deletions src/controllers/plugins/PluginPermission.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS

# include <vector>

namespace chatterino {

struct PluginPermission {
enum class Type {
FilesystemRead,
FilesystemWrite,
};
Type type;

std::vector<QString> errors;

bool isValid() const
{
return this->errors.empty();
}
QString toHtmlEscaped() const;
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved

explicit PluginPermission(const QJsonObject &obj);
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
};

} // namespace chatterino
#endif
Loading
Loading