Clipboard access check, content

This commit is contained in:
Christian Kaiser 2024-12-04 08:10:44 -03:00 committed by David Capello
parent 7331398203
commit 55b3e3c41c
6 changed files with 249 additions and 51 deletions

View File

@ -1667,6 +1667,7 @@ script_label = The following script:
file_label = wants to access to this file: file_label = wants to access to this file:
command_label = wants to execute the following command: command_label = wants to execute the following command:
websocket_label = wants to open a WebSocket connection to this URL: websocket_label = wants to open a WebSocket connection to this URL:
clipboard_label = wants to access the system clipboard
dont_show_for_this_access = Don't show this specific alert again for this script dont_show_for_this_access = Don't show this specific alert again for this script
dont_show_for_this_script = Give full trust to this script dont_show_for_this_script = Give full trust to this script
allow_execute_access = &Allow Execute Access allow_execute_access = &Allow Execute Access

View File

@ -6,7 +6,7 @@
<label text="@.script_label" /> <label text="@.script_label" />
<hbox border="4"><link id="script" /></hbox> <hbox border="4"><link id="script" /></hbox>
<label id="file_label" /> <label id="file_label" />
<hbox border="4"><link id="file" /></hbox> <hbox id="file_container" border="4"><link id="file" /></hbox>
<separator horizontal="true" /> <separator horizontal="true" />
<check id="dont_show" text="@.dont_show_for_this_access" /> <check id="dont_show" text="@.dont_show_for_this_access" />
<check id="full" text="@.dont_show_for_this_script" /> <check id="full" text="@.dont_show_for_this_script" />

View File

@ -12,44 +12,56 @@
#include "app/script/luacpp.h" #include "app/script/luacpp.h"
#include "app/util/clipboard.h" #include "app/util/clipboard.h"
#include "clip/clip.h" #include "clip/clip.h"
#include "doc/mask.h"
#include "doc/palette.h"
#include "doc/tileset.h"
#include "engine.h" #include "engine.h"
#include "docobj.h"
#include "security.h"
namespace app { namespace script { namespace app { namespace script {
namespace { namespace {
struct Clipboard {}; struct Clipboard {};
#define CLIPBOARD_GATE(mode) \
if (!ask_access(L, nullptr, mode, ResourceType::Clipboard)) \
return luaL_error(L, \
"the script doesn't have %s access to the clipboard", \
mode == FileAccessMode::Read ? "read" : "write");
int Clipboard_clear(lua_State* L) int Clipboard_clear(lua_State* L)
{ {
CLIPBOARD_GATE(FileAccessMode::Read);
if (!clip::clear()) if (!clip::clear())
return luaL_error(L, "failed to clear the clipboard"); return luaL_error(L, "failed to clear the clipboard");
return 1; return 0;
} }
int Clipboard_is_text(lua_State* L) int Clipboard_hasText(lua_State* L)
{ {
CLIPBOARD_GATE(FileAccessMode::Read);
lua_pushboolean(L, clip::has(clip::text_format())); lua_pushboolean(L, clip::has(clip::text_format()));
return 1; return 1;
} }
int Clipboard_is_image(lua_State* L) int Clipboard_hasImage(lua_State* L)
{ {
lua_pushboolean(L, clip::has(clip::image_format())); CLIPBOARD_GATE(FileAccessMode::Read);
return 1;
}
int Clipboard_is_empty(lua_State* L) lua_pushboolean(L, clip::has(clip::image_format()));
{
// Using clip::has(clip::empty_format()) had inconsistent results, might as well avoid false
// positives by just checking the two formats we support
lua_pushboolean(L, !clip::has(clip::image_format()) && !clip::has(clip::text_format()));
return 1; return 1;
} }
int Clipboard_get_image(lua_State* L) int Clipboard_get_image(lua_State* L)
{ {
CLIPBOARD_GATE(FileAccessMode::Read);
doc::Image* image = nullptr; doc::Image* image = nullptr;
doc::Mask* mask = nullptr; doc::Mask* mask = nullptr;
doc::Palette* palette = nullptr; doc::Palette* palette = nullptr;
@ -57,8 +69,12 @@ int Clipboard_get_image(lua_State* L)
const bool result = const bool result =
app::Clipboard::instance()->getNativeBitmap(&image, &mask, &palette, &tileset); app::Clipboard::instance()->getNativeBitmap(&image, &mask, &palette, &tileset);
// TODO: If we get a tileset, should we convert it to an image? if (image == nullptr) {
if (image == nullptr || !result) lua_pushnil(L);
return 1;
}
if (!result) // TODO: Can we have a false "nil" value without an error?
return luaL_error(L, "failed to get image from clipboard"); return luaL_error(L, "failed to get image from clipboard");
push_image(L, image); push_image(L, image);
@ -67,16 +83,21 @@ int Clipboard_get_image(lua_State* L)
int Clipboard_get_text(lua_State* L) int Clipboard_get_text(lua_State* L)
{ {
std::string str; CLIPBOARD_GATE(FileAccessMode::Read);
if (!clip::get_text(str))
return luaL_error(L, "failed to get text from clipboard"); std::string str;
if (clip::get_text(str))
lua_pushstring(L, str.c_str());
else
lua_pushnil(L);
lua_pushstring(L, str.c_str());
return 1; return 1;
} }
int Clipboard_set_image(lua_State* L) int Clipboard_set_image(lua_State* L)
{ {
CLIPBOARD_GATE(FileAccessMode::Read);
auto* image = may_get_image_from_arg(L, 2); auto* image = may_get_image_from_arg(L, 2);
if (!image) if (!image)
return luaL_error(L, "invalid image"); return luaL_error(L, "invalid image");
@ -84,7 +105,7 @@ int Clipboard_set_image(lua_State* L)
const bool result = app::Clipboard::instance()->setNativeBitmap( const bool result = app::Clipboard::instance()->setNativeBitmap(
image, image,
nullptr, nullptr,
get_current_palette(), // TODO: Not sure if there's any way to get the palette from the image get_current_palette(),
nullptr, nullptr,
image->maskColor() // TODO: Unsure if this is sufficient. image->maskColor() // TODO: Unsure if this is sufficient.
); );
@ -97,25 +118,131 @@ int Clipboard_set_image(lua_State* L)
int Clipboard_set_text(lua_State* L) int Clipboard_set_text(lua_State* L)
{ {
CLIPBOARD_GATE(FileAccessMode::Read);
const char* text = lua_tostring(L, 2); const char* text = lua_tostring(L, 2);
if (text != NULL && strlen(text) > 0 && !clip::set_text(text)) if (!clip::set_text(text ? text : ""))
return luaL_error(L, "failed to set the clipboard text to '%s'", text); return luaL_error(L, "failed to set the clipboard text to '%s'", text);
return 0; return 0;
} }
int Clipboard_get_content(lua_State* L)
{
CLIPBOARD_GATE(FileAccessMode::Read);
doc::Image* image = nullptr;
doc::Mask* mask = nullptr;
doc::Palette* palette = nullptr;
doc::Tileset* tileset = nullptr;
const bool bitmapResult =
app::Clipboard::instance()->getNativeBitmap(&image, &mask, &palette, &tileset);
std::string text;
const bool clipResult = clip::get_text(text);
lua_createtable(L, 0, 5);
if (bitmapResult && image)
push_image(L, image);
else
lua_pushnil(L);
lua_setfield(L, -2, "image");
if (bitmapResult && mask)
push_docobj<Mask>(L, mask);
else
lua_pushnil(L);
lua_setfield(L, -2, "mask");
if (bitmapResult && palette)
push_docobj<Palette>(L, palette);
else
lua_pushnil(L);
lua_setfield(L, -2, "palette");
if (bitmapResult && tileset)
push_docobj<Tileset>(L, tileset);
else
lua_pushnil(L);
lua_setfield(L, -2, "tileset");
if (clipResult)
lua_pushstring(L, text.c_str());
else
lua_pushnil(L);
lua_setfield(L, -2, "text");
return 1;
}
int Clipboard_set_content(lua_State* L)
{
CLIPBOARD_GATE(FileAccessMode::Read);
doc::Image* image = nullptr;
doc::Mask* mask = nullptr;
doc::Palette* palette = nullptr;
doc::Tileset* tileset = nullptr;
std::optional<std::string> text = std::nullopt;
if (!lua_istable(L, 2))
return luaL_error(L, "app.clipboard.content must be a table");
int type = lua_getfield(L, 2, "image");
if (type != LUA_TNIL)
image = may_get_image_from_arg(L, -1);
lua_pop(L, 1);
type = lua_getfield(L, 2, "mask");
if (type != LUA_TNIL)
mask = may_get_docobj<Mask>(L, -1);
lua_pop(L, 1);
type = lua_getfield(L, 2, "palette");
if (type != LUA_TNIL)
palette = may_get_docobj<Palette>(L, -1);
lua_pop(L, 1);
type = lua_getfield(L, 2, "tileset");
if (type != LUA_TNIL)
tileset = may_get_docobj<Tileset>(L, -1);
lua_pop(L, 1);
type = lua_getfield(L, 2, "text");
if (type != LUA_TNIL) {
const char* tableText = lua_tostring(L, -1);
if (tableText != nullptr && strlen(tableText) > 0)
text = std::string(tableText);
}
lua_pop(L, 1);
if (image &&
!app::Clipboard::instance()->setNativeBitmap(image,
mask,
palette ? palette : get_current_palette(),
tileset,
image ? image->maskColor() : -1))
return luaL_error(L, "failed to set data to clipboard");
if (text != std::nullopt && !clip::set_text(*text))
return luaL_error(L, "failed to set the clipboard text to '%s'", (*text).c_str());
return 0;
}
const luaL_Reg Clipboard_methods[] = { const luaL_Reg Clipboard_methods[] = {
{ "clear", Clipboard_clear }, { "clear", Clipboard_clear },
{ nullptr, nullptr } { nullptr, nullptr }
}; };
const Property Clipboard_properties[] = { const Property Clipboard_properties[] = {
{ "isText", Clipboard_is_text, nullptr }, { "hasText", Clipboard_hasText, nullptr },
{ "isImage", Clipboard_is_image, nullptr }, { "hasImage", Clipboard_hasImage, nullptr },
{ "isEmpty", Clipboard_is_empty, nullptr }, { "text", Clipboard_get_text, Clipboard_set_text },
{ "text", Clipboard_get_text, Clipboard_set_text }, { "image", Clipboard_get_image, Clipboard_set_image },
{ "image", Clipboard_get_image, Clipboard_set_image }, { "content", Clipboard_get_content, Clipboard_set_content },
{ nullptr, nullptr, nullptr } { nullptr, nullptr, nullptr }
}; };
} // anonymous namespace } // anonymous namespace

View File

@ -22,6 +22,7 @@
#include "base/fs.h" #include "base/fs.h"
#include "base/sha1.h" #include "base/sha1.h"
#include "fmt/format.h" #include "fmt/format.h"
#include "ui/widget_type.h"
#include "script_access.xml.h" #include "script_access.xml.h"
@ -91,7 +92,7 @@ std::string get_script_filename(lua_State* L)
const char* source = lua_tostring(L, -1); const char* source = lua_tostring(L, -1);
std::string script; std::string script;
if (source && *source) if (source && *source)
script = source + 1; script = source;
lua_pop(L, 2); lua_pop(L, 2);
return script; return script;
} }
@ -242,12 +243,25 @@ bool ask_access(lua_State* L,
if ((access & int(mode)) == int(mode)) if ((access & int(mode)) == int(mode))
return true; return true;
std::string allowButtonText = std::string allowButtonText;
(mode == FileAccessMode::LoadLib ? Strings::script_access_allow_load_lib_access() : switch (mode) {
mode == FileAccessMode::OpenSocket ? Strings::script_access_allow_open_conn_access() : case FileAccessMode::LoadLib:
mode == FileAccessMode::Execute ? Strings::script_access_allow_execute_access() : allowButtonText = Strings::script_access_allow_load_lib_access();
mode == FileAccessMode::Write ? Strings::script_access_allow_write_access() : break;
Strings::script_access_allow_read_access()); case FileAccessMode::OpenSocket:
allowButtonText = Strings::script_access_allow_open_conn_access();
break;
case FileAccessMode::Execute:
allowButtonText = Strings::script_access_allow_execute_access();
break;
case FileAccessMode::Write:
allowButtonText = Strings::script_access_allow_write_access();
break;
case FileAccessMode::Read:
allowButtonText = Strings::script_access_allow_read_access();
break;
default: return luaL_error(L, "invalid access request");
}
app::gen::ScriptAccess dlg; app::gen::ScriptAccess dlg;
dlg.script()->setText(script); dlg.script()->setText(script);
@ -258,15 +272,26 @@ bool ask_access(lua_State* L,
case ResourceType::File: label = Strings::script_access_file_label(); break; case ResourceType::File: label = Strings::script_access_file_label(); break;
case ResourceType::Command: label = Strings::script_access_command_label(); break; case ResourceType::Command: label = Strings::script_access_command_label(); break;
case ResourceType::WebSocket: label = Strings::script_access_websocket_label(); break; case ResourceType::WebSocket: label = Strings::script_access_websocket_label(); break;
case ResourceType::Clipboard: label = Strings::script_access_clipboard_label(); break;
} }
dlg.fileLabel()->setText(label); dlg.fileLabel()->setText(label);
} }
dlg.file()->setText(filename); if (filename && strlen(filename) > 0)
dlg.file()->setText(filename);
else
dlg.fileContainer()->setVisible(false);
dlg.allow()->setText(allowButtonText); dlg.allow()->setText(allowButtonText);
dlg.allow()->processMnemonicFromText(); dlg.allow()->processMnemonicFromText();
dlg.script()->Click.connect([&dlg] { app::launcher::open_folder(dlg.script()->text()); }); if (script == "internal") {
// Make it look like a normal label
dlg.script()->setType(ui::WidgetType::kLabelWidget);
dlg.script()->initTheme();
}
else
dlg.script()->Click.connect([&dlg] { app::launcher::open_folder(dlg.script()->text()); });
dlg.full()->Click.connect([&dlg, &allowButtonText]() { dlg.full()->Click.connect([&dlg, &allowButtonText]() {
if (dlg.full()->isSelected()) { if (dlg.full()->isSelected()) {

View File

@ -30,6 +30,7 @@ enum class ResourceType {
File, File,
Command, Command,
WebSocket, WebSocket,
Clipboard,
}; };
void overwrite_unsecure_functions(lua_State* L); void overwrite_unsecure_functions(lua_State* L);

View File

@ -5,46 +5,90 @@
dofile('./test_utils.lua') dofile('./test_utils.lua')
do -- Clipboard clearing do -- Text clearing
app.clipboard.text = "hello world" app.clipboard.text = "clear me"
expect_eq(false, app.clipboard.isEmpty) expect_eq(true, app.clipboard.hasText)
app.clipboard.clear() app.clipboard.clear()
expect_eq(true, app.clipboard.isEmpty) expect_eq(false, app.clipboard.hasText)
end
do -- Text clearing (with content)
app.clipboard.content = { text = "clear me 2" }
expect_eq(true, app.clipboard.hasText)
app.clipboard.clear()
expect_eq(false, app.clipboard.hasText)
end end
do -- Text copying and access do -- Text copying and access
app.clipboard.clear() app.clipboard.clear()
expect_eq(false, app.clipboard.isText) expect_eq(false, app.clipboard.hasText)
expect_eq(false, app.clipboard.isImage) expect_eq(false, app.clipboard.hasImage)
expect_eq(true, app.clipboard.isEmpty)
app.clipboard.text = "hello world" app.clipboard.text = "hello world"
expect_eq(true, app.clipboard.isText) expect_eq(true, app.clipboard.hasText)
expect_eq(false, app.clipboard.isImage) expect_eq(false, app.clipboard.hasImage)
expect_eq(false, app.clipboard.isEmpty)
expect_eq("hello world", app.clipboard.text) expect_eq("hello world", app.clipboard.text)
end end
do -- Text copying and access (with .content)
app.clipboard.clear()
expect_eq(false, app.clipboard.hasText)
expect_eq(false, app.clipboard.hasImage)
app.clipboard.content = { text = "hello world 2"}
expect_eq(true, app.clipboard.hasText)
expect_eq(false, app.clipboard.hasImage)
expect_eq("hello world 2", app.clipboard.content.text)
end
do -- Image copying and access do -- Image copying and access
local sprite = Sprite{ fromFile="sprites/abcd.aseprite" } local sprite = Sprite{ fromFile="sprites/abcd.aseprite" }
app.clipboard.clear() app.clipboard.clear()
expect_eq(false, app.clipboard.isText) expect_eq(false, app.clipboard.hasText)
expect_eq(false, app.clipboard.isImage) expect_eq(false, app.clipboard.hasImage)
expect_eq(true, app.clipboard.isEmpty)
assert(app.image ~= nil) assert(app.image ~= nil)
app.clipboard.image = app.image app.clipboard.image = app.image
expect_eq(false, app.clipboard.isText) expect_eq(false, app.clipboard.hasText)
expect_eq(true, app.clipboard.isImage) expect_eq(true, app.clipboard.hasImage)
expect_eq(false, app.clipboard.isEmpty)
expect_eq(app.image.width, app.clipboard.image.width) expect_eq(app.image.width, app.clipboard.image.width)
expect_eq(app.image.height, app.clipboard.image.height) expect_eq(app.image.height, app.clipboard.image.height)
expect_eq(app.image.bytes, app.clipboard.image.bytes) expect_eq(app.image.bytes, app.clipboard.image.bytes)
end end
do -- Image copying and access (with .content)
local sprite = Sprite{ fromFile="sprites/abcd.aseprite" }
app.clipboard.clear()
expect_eq(false, app.clipboard.hasText)
expect_eq(false, app.clipboard.hasImage)
assert(app.image ~= nil)
app.clipboard.content = {
image = app.image,
palettte = sprite.palettes[1],
mask = sprite.spec.transparentColor,
tileset = nil,
text = nil, -- TODO: Error when this happens
}
expect_eq(false, app.clipboard.hasText)
expect_eq(true, app.clipboard.hasImage)
local result = app.clipboard.content
assert(result ~= nil)
expect_eq(sprite.image.bytes, c.image.bytes)
-- TODO: the rest
end