Add button_url[] and hypertext element to allow mods to open web pages (#13825)

Fixes #12500
This commit is contained in:
rubenwardy 2024-03-24 17:19:23 +00:00 committed by GitHub
parent 6c4a110679
commit 24cc33e704
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 530 additions and 37 deletions

@ -158,7 +158,7 @@ return {
"style[label_button;border=false]" .. "style[label_button;border=false]" ..
"button[0.1,3.4;5.3,0.5;label_button;" .. "button[0.1,3.4;5.3,0.5;label_button;" ..
core.formspec_escape(version.project .. " " .. version.string) .. "]" .. core.formspec_escape(version.project .. " " .. version.string) .. "]" ..
"button[1.5,4.1;2.5,0.8;homepage;minetest.net]" .. "button_url[1.5,4.1;2.5,0.8;homepage;minetest.net;https://www.minetest.net/]" ..
"hypertext[5.5,0.25;9.75,6.6;credits;" .. minetest.formspec_escape(hypertext) .. "]" "hypertext[5.5,0.25;9.75,6.6;credits;" .. minetest.formspec_escape(hypertext) .. "]"
-- Render information -- Render information
@ -188,10 +188,6 @@ return {
end, end,
cbf_button_handler = function(this, fields, name, tabdata) cbf_button_handler = function(this, fields, name, tabdata)
if fields.homepage then
core.open_url("https://www.minetest.net")
end
if fields.share_debug then if fields.share_debug then
local path = core.get_user_path() .. DIR_DELIM .. "debug.txt" local path = core.get_user_path() .. DIR_DELIM .. "debug.txt"
core.share_file(path) core.share_file(path)

@ -3017,6 +3017,16 @@ Elements
centered on `H`. With the new coordinate system, `H` will modify the height. centered on `H`. With the new coordinate system, `H` will modify the height.
* `label` is the text on the button * `label` is the text on the button
### `button_url[<X>,<Y>;<W>,<H>;<name>;<label>;<url>]`
* Clickable button. When clicked, fields will be sent and the user will be given the
option to open the URL in a browser.
* With the old coordinate system, buttons are a set height, but will be vertically
centered on `H`. With the new coordinate system, `H` will modify the height.
* To make this into an `image_button`, you can use formspec styling.
* `label` is the text on the button.
* `url` must be a valid web URL, starting with `http://` or `https://`.
### `image_button[<X>,<Y>;<W>,<H>;<texture name>;<name>;<label>]` ### `image_button[<X>,<Y>;<W>,<H>;<texture name>;<name>;<label>]`
* `texture name` is the filename of an image * `texture name` is the filename of an image
@ -3043,6 +3053,11 @@ Elements
* When clicked, fields will be sent and the form will quit. * When clicked, fields will be sent and the form will quit.
* Same as `button` in all other respects. * Same as `button` in all other respects.
### `button_url_exit[<X>,<Y>;<W>,<H>;<name>;<label>;<url>]`
* When clicked, fields will be sent and the form will quit.
* Same as `button_url` in all other respects.
### `image_button_exit[<X>,<Y>;<W>,<H>;<texture name>;<name>;<label>]` ### `image_button_exit[<X>,<Y>;<W>,<H>;<texture name>;<name>;<label>]`
* When clicked, fields will be sent and the form will quit. * When clicked, fields will be sent and the form will quit.
@ -3538,11 +3553,13 @@ Changes the style of the text.
Sets global style. Sets global style.
Global only styles: Global only styles:
* `background`: Text background, a `colorspec` or `none`. * `background`: Text background, a `colorspec` or `none`.
* `margin`: Page margins in pixel. * `margin`: Page margins in pixel.
* `valign`: Text vertical alignment (`top`, `middle`, `bottom`). * `valign`: Text vertical alignment (`top`, `middle`, `bottom`).
Inheriting styles (affects child elements): Inheriting styles (affects child elements):
* `color`: Default text color. Given color is a `colorspec`. * `color`: Default text color. Given color is a `colorspec`.
* `hovercolor`: Color of <action> tags when mouse is over. * `hovercolor`: Color of <action> tags when mouse is over.
* `size`: Default text size. * `size`: Default text size.
@ -3556,6 +3573,7 @@ tags appear.
`<tag name=... color=... hovercolor=... font=... size=...>` `<tag name=... color=... hovercolor=... font=... size=...>`
Defines or redefines tag style. This can be used to define new tags. Defines or redefines tag style. This can be used to define new tags.
* `name`: Name of the tag to define or change. * `name`: Name of the tag to define or change.
* `color`: Text color. Given color is a `colorspec`. * `color`: Text color. Given color is a `colorspec`.
* `hovercolor`: Text color when element hovered (only for `action` tags). Given color is a `colorspec`. * `hovercolor`: Text color when element hovered (only for `action` tags). Given color is a `colorspec`.
@ -3588,6 +3606,7 @@ Other tags can be added using `<tag ...>` tag.
Make that text a clickable text triggering an action. Make that text a clickable text triggering an action.
* `name`: Name of the action (mandatory). * `name`: Name of the action (mandatory).
* `url`: URL to open when the action is triggered (optional).
When clicked, the formspec is send to the server. The value of the text field When clicked, the formspec is send to the server. The value of the text field
sent to `on_player_receive_fields` will be "action:" concatenated to the action sent to `on_player_receive_fields` will be "action:" concatenated to the action

@ -1,12 +1,15 @@
local color = minetest.colorize local color = minetest.colorize
-- \208\176 is a cyrillic small a
local unsafe_url = minetest.formspec_escape("https://u:p@wikipedi\208\176.org:1233/heIIoll?a=b#c")
local clip_fs = [[ local clip_fs = [[
style_type[label,button,image_button,item_image_button, style_type[label,button,image_button,item_image_button,
tabheader,scrollbar,table,animated_image tabheader,scrollbar,table,animated_image
,field,textarea,checkbox,dropdown;noclip=%c] ,field,textarea,checkbox,dropdown;noclip=%c]
label[0,0;A clipping test] label[0,0;A clipping test]
button[0,1;3,0.8;clip_button;A clipping test] button_url[0,1;3,0.8;clip_button;A clipping test;]] .. unsafe_url .. [[]
image_button[0,2;3,0.8;testformspec_button_image.png;clip_image_button;A clipping test] image_button[0,2;3,0.8;testformspec_button_image.png;clip_image_button;A clipping test]
item_image_button[0,3;3,0.8;testformspec:item;clip_item_image_button;A clipping test] item_image_button[0,3;3,0.8;testformspec:item;clip_item_image_button;A clipping test]
tabheader[0,4.7;3,0.63;clip_tabheader;Clip,Test,Text,Tabs;1;false;false] tabheader[0,4.7;3,0.63;clip_tabheader;Clip,Test,Text,Tabs;1;false;false]
@ -92,6 +95,7 @@ This is a normal text.
<t_green>color=green</t_green> <t_green>color=green</t_green>
Action: <action name=color><t_green>color=green</t_green></action> Action: <action name=color><t_green>color=green</t_green></action>
Action: <action name=hovercolor><t_hover>hovercolor=yellow</t_hover></action> Action: <action name=hovercolor><t_hover>hovercolor=yellow</t_hover></action>
Action URL: <action name=open url=https://example.com/?a=b#c>open URL</action>
<t_size>size=24</t_size> <t_size>size=24</t_size>
<t_mono>font=mono</t_mono> <t_mono>font=mono</t_mono>
<t_multi>color=green font=mono size=24</t_multi> <t_multi>color=green font=mono size=24</t_multi>
@ -145,7 +149,7 @@ local hypertext_fs = "hypertext[0,0;11,9;hypertext;"..minetest.formspec_escape(h
local style_fs = [[ local style_fs = [[
style[one_btn1;bgcolor=red;textcolor=yellow;bgcolor_hovered=orange; style[one_btn1;bgcolor=red;textcolor=yellow;bgcolor_hovered=orange;
bgcolor_pressed=purple] bgcolor_pressed=purple]
button[0,0;2.5,0.8;one_btn1;Button] button_url_exit[0,0;2.5,0.8;one_btn1;Button;]] .. unsafe_url .. [[]
style[one_btn2;border=false;textcolor=cyan] ]].. style[one_btn2;border=false;textcolor=cyan] ]]..
"button[0,1.05;2.5,0.8;one_btn2;Text " .. color("#FF0", "Yellow") .. [[] "button[0,1.05;2.5,0.8;one_btn2;Text " .. color("#FF0", "Yellow") .. [[]

@ -50,6 +50,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "gui/guiFormSpecMenu.h" #include "gui/guiFormSpecMenu.h"
#include "gui/guiKeyChangeMenu.h" #include "gui/guiKeyChangeMenu.h"
#include "gui/guiPasswordChange.h" #include "gui/guiPasswordChange.h"
#include "gui/guiOpenURL.h"
#include "gui/guiVolumeChange.h" #include "gui/guiVolumeChange.h"
#include "gui/mainmenumanager.h" #include "gui/mainmenumanager.h"
#include "gui/profilergraph.h" #include "gui/profilergraph.h"
@ -1815,6 +1816,12 @@ inline bool Game::handleCallbacks()
g_gamecallback->keyconfig_requested = false; g_gamecallback->keyconfig_requested = false;
} }
if (!g_gamecallback->show_open_url_dialog.empty()) {
(new GUIOpenURLMenu(guienv, guiroot, -1,
&g_menumgr, texture_src, g_gamecallback->show_open_url_dialog))->drop();
g_gamecallback->show_open_url_dialog.clear();
}
if (g_gamecallback->keyconfig_changed) { if (g_gamecallback->keyconfig_changed) {
input->keycache.populate(); // update the cache with new settings input->keycache.populate(); // update the cache with new settings
g_gamecallback->keyconfig_changed = false; g_gamecallback->keyconfig_changed = false;

@ -13,6 +13,7 @@ set(gui_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/guiInventoryList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiInventoryList.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiItemImage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiItemImage.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiKeyChangeMenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiKeyChangeMenu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiOpenURL.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiPasswordChange.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiPasswordChange.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiPathSelectMenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiPathSelectMenu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/guiScene.cpp ${CMAKE_CURRENT_SOURCE_DIR}/guiScene.cpp

@ -976,14 +976,18 @@ void GUIFormSpecMenu::parseItemImage(parserData* data, const std::string &elemen
void GUIFormSpecMenu::parseButton(parserData* data, const std::string &element, void GUIFormSpecMenu::parseButton(parserData* data, const std::string &element,
const std::string &type) const std::string &type)
{ {
int expected_parts = (type == "button_url" || type == "button_url_exit") ? 5 : 4;
std::vector<std::string> parts; std::vector<std::string> parts;
if (!precheckElement("button", element, 4, 4, parts)) if (!precheckElement("button", element, expected_parts, expected_parts, parts))
return; return;
std::vector<std::string> v_pos = split(parts[0],','); std::vector<std::string> v_pos = split(parts[0],',');
std::vector<std::string> v_geom = split(parts[1],','); std::vector<std::string> v_geom = split(parts[1],',');
std::string name = parts[2]; std::string name = parts[2];
std::string label = parts[3]; std::string label = parts[3];
std::string url;
if (type == "button_url" || type == "button_url_exit")
url = parts[4];
MY_CHECKPOS("button",0); MY_CHECKPOS("button",0);
MY_CHECKGEOM("button",1); MY_CHECKGEOM("button",1);
@ -1018,8 +1022,10 @@ void GUIFormSpecMenu::parseButton(parserData* data, const std::string &element,
258 + m_fields.size() 258 + m_fields.size()
); );
spec.ftype = f_Button; spec.ftype = f_Button;
if(type == "button_exit") if (type == "button_exit" || type == "button_url_exit")
spec.is_exit = true; spec.is_exit = true;
if (type == "button_url" || type == "button_url_exit")
spec.url = url;
GUIButton *e = GUIButton::addButton(Environment, rect, m_tsrc, GUIButton *e = GUIButton::addButton(Environment, rect, m_tsrc,
data->current_parent, spec.fid, spec.flabel.c_str()); data->current_parent, spec.fid, spec.flabel.c_str());
@ -2897,7 +2903,7 @@ void GUIFormSpecMenu::parseElement(parserData* data, const std::string &element)
return; return;
} }
if (type == "button" || type == "button_exit") { if (type == "button" || type == "button_exit" || type == "button_url" || type == "button_url_exit") {
parseButton(data, description, type); parseButton(data, description, type);
return; return;
} }
@ -4968,6 +4974,17 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event)
m_sound_manager->playSound(0, SoundSpec(s.sound, 1.0f)); m_sound_manager->playSound(0, SoundSpec(s.sound, 1.0f));
s.send = true; s.send = true;
if (!s.url.empty()) {
if (m_client) {
// in game
g_gamecallback->showOpenURLDialog(s.url);
} else {
// main menu
porting::open_url(s.url);
}
}
if (s.is_exit) { if (s.is_exit) {
if (m_allowclose) { if (m_allowclose) {
acceptInput(quit_mode_accept); acceptInput(quit_mode_accept);

@ -137,6 +137,7 @@ class GUIFormSpecMenu : public GUIModalMenu
std::string fname; std::string fname;
std::wstring flabel; std::wstring flabel;
std::wstring fdefault; std::wstring fdefault;
std::string url;
s32 fid; s32 fid;
bool send; bool send;
FormspecFieldType ftype; FormspecFieldType ftype;

@ -27,6 +27,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "inventory.h" #include "inventory.h"
#include "util/string.h" #include "util/string.h"
#include "irrlicht_changes/CGUITTFont.h" #include "irrlicht_changes/CGUITTFont.h"
#include "mainmenumanager.h"
#include "porting.h"
using namespace irr::gui; using namespace irr::gui;
@ -1106,6 +1108,18 @@ bool GUIHyperText::OnEvent(const SEvent &event)
newEvent.GUIEvent.EventType = EGET_BUTTON_CLICKED; newEvent.GUIEvent.EventType = EGET_BUTTON_CLICKED;
Parent->OnEvent(newEvent); Parent->OnEvent(newEvent);
} }
auto url_it = tag->attrs.find("url");
if (url_it != tag->attrs.end()) {
if (g_gamecallback) {
// in game
g_gamecallback->showOpenURLDialog(url_it->second);
} else {
// main menu
porting::open_url(url_it->second);
}
}
break; break;
} }
} }

196
src/gui/guiOpenURL.cpp Normal file

@ -0,0 +1,196 @@
/*
Part of Minetest
Copyright (C) 2023-24 rubenwardy
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "guiOpenURL.h"
#include "guiButton.h"
#include "guiEditBoxWithScrollbar.h"
#include <IGUIEditBox.h>
#include <IGUIFont.h>
#include "client/renderingengine.h"
#include "porting.h"
#include "gettext.h"
#include "util/colorize.h"
namespace {
constexpr int ID_url = 256;
constexpr int ID_open = 259;
constexpr int ID_cancel = 261;
}
GUIOpenURLMenu::GUIOpenURLMenu(gui::IGUIEnvironment* env,
gui::IGUIElement* parent, s32 id,
IMenuManager *menumgr,
ISimpleTextureSource *tsrc, const std::string &url
):
GUIModalMenu(env, parent, id, menumgr),
m_tsrc(tsrc),
url(url)
{
}
void GUIOpenURLMenu::regenerateGui(v2u32 screensize)
{
/*
Remove stuff
*/
removeAllChildren();
/*
Calculate new sizes and positions
*/
const float s = m_gui_scale;
DesiredRect = core::rect<s32>(
screensize.X / 2 - 580 * s / 2,
screensize.Y / 2 - 250 * s / 2,
screensize.X / 2 + 580 * s / 2,
screensize.Y / 2 + 250 * s / 2
);
recalculateAbsolutePosition(false);
v2s32 size = DesiredRect.getSize();
v2s32 topleft_client(40 * s, 0);
/*
Get URL text
*/
bool ok = true;
std::string text;
#ifdef USE_CURL
try {
text = colorize_url(url);
} catch (const std::exception &e) {
text = std::string(e.what()) + " (url = " + url + ")";
ok = false;
}
#else
text = url;
#endif
/*
Add stuff
*/
s32 ypos = 40 * s;
{
core::rect<s32> rect(0, 0, 500 * s, 20 * s);
rect += topleft_client + v2s32(20 * s, ypos);
std::wstring title = ok
? wstrgettext("Open URL?")
: wstrgettext("Unable to open URL");
gui::StaticText::add(Environment, title, rect,
false, true, this, -1);
}
ypos += 50 * s;
{
core::rect<s32> rect(0, 0, 440 * s, 60 * s);
auto font = g_fontengine->getFont(FONT_SIZE_UNSPECIFIED,
ok ? FM_Mono : FM_Standard);
int scrollbar_width = Environment->getSkin()->getSize(gui::EGDS_SCROLLBAR_SIZE);
int max_cols = (rect.getWidth() - scrollbar_width - 10) / font->getDimension(L"x").Width;
text = wrap_rows(text, max_cols, true);
rect += topleft_client + v2s32(20 * s, ypos);
IGUIEditBox *e = new GUIEditBoxWithScrollBar(utf8_to_wide(text).c_str(), true, Environment,
this, ID_url, rect, m_tsrc, false, true);
e->setMultiLine(true);
e->setWordWrap(true);
e->setTextAlignment(gui::EGUIA_UPPERLEFT, gui::EGUIA_UPPERLEFT);
e->setDrawBorder(true);
e->setDrawBackground(true);
e->setOverrideFont(font);
e->drop();
}
ypos += 80 * s;
if (ok) {
core::rect<s32> rect(0, 0, 100 * s, 40 * s);
rect = rect + v2s32(size.X / 2 - 150 * s, ypos);
GUIButton::addButton(Environment, rect, m_tsrc, this, ID_open,
wstrgettext("Open").c_str());
}
{
core::rect<s32> rect(0, 0, 100 * s, 40 * s);
rect = rect + v2s32(size.X / 2 + 50 * s, ypos);
GUIButton::addButton(Environment, rect, m_tsrc, this, ID_cancel,
wstrgettext("Cancel").c_str());
}
}
void GUIOpenURLMenu::drawMenu()
{
gui::IGUISkin *skin = Environment->getSkin();
if (!skin)
return;
video::IVideoDriver *driver = Environment->getVideoDriver();
video::SColor bgcolor(140, 0, 0, 0);
driver->draw2DRectangle(bgcolor, AbsoluteRect, &AbsoluteClippingRect);
gui::IGUIElement::draw();
#ifdef __ANDROID__
getAndroidUIInput();
#endif
}
bool GUIOpenURLMenu::OnEvent(const SEvent &event)
{
if (event.EventType == EET_KEY_INPUT_EVENT) {
if ((event.KeyInput.Key == KEY_ESCAPE ||
event.KeyInput.Key == KEY_CANCEL) &&
event.KeyInput.PressedDown) {
quitMenu();
return true;
}
if (event.KeyInput.Key == KEY_RETURN && event.KeyInput.PressedDown) {
porting::open_url(url);
quitMenu();
return true;
}
}
if (event.EventType == EET_GUI_EVENT) {
if (event.GUIEvent.EventType == gui::EGET_ELEMENT_FOCUS_LOST &&
isVisible()) {
if (!canTakeFocus(event.GUIEvent.Element)) {
infostream << "GUIOpenURLMenu: Not allowing focus change."
<< std::endl;
// Returning true disables focus change
return true;
}
}
if (event.GUIEvent.EventType == gui::EGET_BUTTON_CLICKED) {
switch (event.GUIEvent.Caller->getID()) {
case ID_open:
porting::open_url(url);
quitMenu();
return true;
case ID_cancel:
quitMenu();
return true;
}
}
}
return Parent != nullptr && Parent->OnEvent(event);
}

50
src/gui/guiOpenURL.h Normal file

@ -0,0 +1,50 @@
/*
Part of Minetest
Copyright (C) 2023 rubenwardy
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#include "irrlichttypes_extrabloated.h"
#include "modalMenu.h"
#include <string>
class Client;
class ISimpleTextureSource;
class GUIOpenURLMenu : public GUIModalMenu
{
public:
GUIOpenURLMenu(gui::IGUIEnvironment *env, gui::IGUIElement *parent, s32 id,
IMenuManager *menumgr,
ISimpleTextureSource *tsrc, const std::string &url);
/*
Remove and re-add (or reposition) stuff
*/
void regenerateGui(v2u32 screensize);
void drawMenu();
bool OnEvent(const SEvent &event);
protected:
std::wstring getLabelByID(s32 id) { return L""; }
std::string getNameByID(s32 id) { return ""; }
private:
ISimpleTextureSource *m_tsrc;
std::string url;
};

@ -34,7 +34,7 @@ public:
virtual void disconnect() = 0; virtual void disconnect() = 0;
virtual void changePassword() = 0; virtual void changePassword() = 0;
virtual void changeVolume() = 0; virtual void changeVolume() = 0;
virtual void showOpenURLDialog(const std::string &url) = 0;
virtual void signalKeyConfigChange() = 0; virtual void signalKeyConfigChange() = 0;
}; };
@ -108,44 +108,47 @@ public:
MainGameCallback() = default; MainGameCallback() = default;
virtual ~MainGameCallback() = default; virtual ~MainGameCallback() = default;
virtual void exitToOS() void exitToOS() override
{ {
shutdown_requested = true; shutdown_requested = true;
} }
virtual void disconnect() void disconnect() override
{ {
disconnect_requested = true; disconnect_requested = true;
} }
virtual void changePassword() void changePassword() override
{ {
changepassword_requested = true; changepassword_requested = true;
} }
virtual void changeVolume() void changeVolume() override
{ {
changevolume_requested = true; changevolume_requested = true;
} }
virtual void keyConfig() void keyConfig() override
{ {
keyconfig_requested = true; keyconfig_requested = true;
} }
virtual void signalKeyConfigChange() void signalKeyConfigChange() override
{ {
keyconfig_changed = true; keyconfig_changed = true;
} }
void showOpenURLDialog(const std::string &url) override {
show_open_url_dialog = url;
}
bool disconnect_requested = false; bool disconnect_requested = false;
bool changepassword_requested = false; bool changepassword_requested = false;
bool changevolume_requested = false; bool changevolume_requested = false;
bool keyconfig_requested = false; bool keyconfig_requested = false;
bool shutdown_requested = false; bool shutdown_requested = false;
bool keyconfig_changed = false; bool keyconfig_changed = false;
std::string show_open_url_dialog = "";
}; };
extern MainGameCallback *g_gamecallback; extern MainGameCallback *g_gamecallback;

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "util/numeric.h" #include "util/numeric.h"
#include "util/string.h" #include "util/string.h"
#include "util/base64.h" #include "util/base64.h"
#include "util/colorize.h"
class TestUtilities : public TestBase { class TestUtilities : public TestBase {
public: public:
@ -59,6 +60,7 @@ public:
void testBase64(); void testBase64();
void testSanitizeDirName(); void testSanitizeDirName();
void testIsBlockInSight(); void testIsBlockInSight();
void testColorizeURL();
}; };
static TestUtilities g_test_instance; static TestUtilities g_test_instance;
@ -92,6 +94,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
TEST(testBase64); TEST(testBase64);
TEST(testSanitizeDirName); TEST(testSanitizeDirName);
TEST(testIsBlockInSight); TEST(testIsBlockInSight);
TEST(testColorizeURL);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@ -710,3 +713,19 @@ void TestUtilities::testIsBlockInSight()
UASSERT(isBlockInSight({-1, 0, 0}, cam_pos, cam_dir, fov, range)); UASSERT(isBlockInSight({-1, 0, 0}, cam_pos, cam_dir, fov, range));
} }
} }
void TestUtilities::testColorizeURL()
{
#ifdef USE_CURL
#define RED COLOR_CODE("#faa")
#define GREY COLOR_CODE("#aaa")
#define WHITE COLOR_CODE("#fff")
std::string result = colorize_url("http://example.com/");
UASSERT(result == (GREY "http://" WHITE "example.com" GREY "/"));
result = colorize_url(u8"https://u:p@wikipedi\u0430.org:1234/heIIoll?a=b#c");
UASSERT(result ==
(GREY "https://u:p@" WHITE "wikipedi" RED "%d0%b0" WHITE ".org" GREY ":1234/heIIoll?a=b#c"));
#endif
}

@ -1,6 +1,7 @@
set(UTIL_SRCS set(UTIL_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/areastore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/areastore.cpp
${CMAKE_CURRENT_SOURCE_DIR}/auth.cpp ${CMAKE_CURRENT_SOURCE_DIR}/auth.cpp
${CMAKE_CURRENT_SOURCE_DIR}/colorize.cpp
${CMAKE_CURRENT_SOURCE_DIR}/base64.cpp ${CMAKE_CURRENT_SOURCE_DIR}/base64.cpp
${CMAKE_CURRENT_SOURCE_DIR}/directiontables.cpp ${CMAKE_CURRENT_SOURCE_DIR}/directiontables.cpp
${CMAKE_CURRENT_SOURCE_DIR}/enriched_string.cpp ${CMAKE_CURRENT_SOURCE_DIR}/enriched_string.cpp

113
src/util/colorize.cpp Normal file

@ -0,0 +1,113 @@
/*
Part of Minetest
Copyright (C) 2024 rubenwardy
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "colorize.h"
#ifdef USE_CURL
#include <curl/urlapi.h>
#include "log.h"
#include "string.h"
#include <sstream>
std::string colorize_url(const std::string &url)
{
// Forbid escape codes in URL
if (url.find('\x1b') != std::string::npos) {
throw std::runtime_error("Unable to open URL as it contains escape codes");
}
auto urlHandleRAII = std::unique_ptr<CURLU, decltype(&curl_url_cleanup)>(
curl_url(), curl_url_cleanup);
CURLU *urlHandle = urlHandleRAII.get();
auto rc = curl_url_set(urlHandle, CURLUPART_URL, url.c_str(), 0);
if (rc != CURLUE_OK) {
throw std::runtime_error("Unable to open URL as it is not valid");
}
auto url_get = [&] (CURLUPart what) -> std::string {
char *tmp = nullptr;
curl_url_get(urlHandle, what, &tmp, 0);
std::string ret(tmp ? tmp : "");
curl_free(tmp);
return ret;
};
auto scheme = url_get(CURLUPART_SCHEME);
auto user = url_get(CURLUPART_USER);
auto password = url_get(CURLUPART_PASSWORD);
auto host = url_get(CURLUPART_HOST);
auto port = url_get(CURLUPART_PORT);
auto path = url_get(CURLUPART_PATH);
auto query = url_get(CURLUPART_QUERY);
auto fragment = url_get(CURLUPART_FRAGMENT);
auto zoneid = url_get(CURLUPART_ZONEID);
std::ostringstream os;
std::string_view red = COLOR_CODE("#faa");
std::string_view white = COLOR_CODE("#fff");
std::string_view grey = COLOR_CODE("#aaa");
os << grey << scheme << "://";
if (!user.empty())
os << user;
if (!password.empty())
os << ":" << password;
if (!(user.empty() && password.empty()))
os << "@";
// Print hostname, escaping unsafe characters
os << white;
bool was_alphanum = true;
std::string host_s = host;
for (size_t i = 0; i < host_s.size(); i++) {
char c = host_s[i];
bool is_alphanum = isalnum(c) || ispunct(c);
if (is_alphanum == was_alphanum) {
// skip
} else if (is_alphanum) {
os << white;
} else {
os << red;
}
was_alphanum = is_alphanum;
if (is_alphanum) {
os << c;
} else {
os << "%" << std::setfill('0') << std::setw(2) << std::hex
<< (static_cast<unsigned int>(c) & 0xff);
}
}
os << grey;
if (!zoneid.empty())
os << "%" << zoneid;
if (!port.empty())
os << ":" << port;
os << path;
if (!query.empty())
os << "?" << query;
if (!fragment.empty())
os << "#" << fragment;
return os.str();
}
#endif

34
src/util/colorize.h Normal file

@ -0,0 +1,34 @@
/*
Part of Minetest
Copyright (C) 2024 rubenwardy
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#pragma once
#include <string>
#include "config.h"
#define COLOR_CODE(color) "\x1b(c@" color ")"
#ifdef USE_CURL
/**
* Colorize URL to highlight the hostname and any unsafe characters
*
* Throws an exception if the url is invalid
*/
std::string colorize_url(const std::string &url);
#endif

@ -593,6 +593,40 @@ void str_replace(std::string &str, char from, char to)
std::replace(str.begin(), str.end(), from, to); std::replace(str.begin(), str.end(), from, to);
} }
std::string wrap_rows(std::string_view from, unsigned row_len, bool has_color_codes)
{
std::string to;
to.reserve(from.size());
std::string last_color_code;
unsigned character_idx = 0;
bool inside_colorize = false;
for (size_t i = 0; i < from.size(); i++) {
if (!IS_UTF8_MULTB_INNER(from[i])) {
if (inside_colorize) {
last_color_code += from[i];
if (from[i] == ')') {
inside_colorize = false;
} else {
// keep reading
}
} else if (has_color_codes && from[i] == '\x1b') {
inside_colorize = true;
last_color_code = "\x1b";
} else {
// Wrap string after last inner byte of char
if (character_idx > 0 && character_idx % row_len == 0) {
to += '\n' + last_color_code;
}
character_idx++;
}
}
to += from[i];
}
return to;
}
/* Translated strings have the following format: /* Translated strings have the following format:
* \x1bT marks the beginning of a translated string * \x1bT marks the beginning of a translated string
* \x1bE marks its end * \x1bE marks its end

@ -512,7 +512,7 @@ inline bool string_allowed_blacklist(std::string_view str,
* Create a string based on \p from where a newline is forcefully inserted * Create a string based on \p from where a newline is forcefully inserted
* every \p row_len characters. * every \p row_len characters.
* *
* @note This function does not honour word wraps and blindy inserts a newline * @note This function does not honour word wraps and blindly inserts a newline
* every \p row_len characters whether it breaks a word or not. It is * every \p row_len characters whether it breaks a word or not. It is
* intended to be used for, for example, showing paths in the GUI. * intended to be used for, for example, showing paths in the GUI.
* *
@ -521,26 +521,10 @@ inline bool string_allowed_blacklist(std::string_view str,
* *
* @param from The (utf-8) string to be wrapped into rows. * @param from The (utf-8) string to be wrapped into rows.
* @param row_len The row length (in characters). * @param row_len The row length (in characters).
* @param has_color_codes Whether the source string has colorize codes.
* @return A new string with the wrapping applied. * @return A new string with the wrapping applied.
*/ */
inline std::string wrap_rows(std::string_view from, unsigned row_len) std::string wrap_rows(std::string_view from, unsigned row_len, bool has_color_codes = false);
{
std::string to;
to.reserve(from.size());
unsigned character_idx = 0;
for (size_t i = 0; i < from.size(); i++) {
if (!IS_UTF8_MULTB_INNER(from[i])) {
// Wrap string after last inner byte of char
if (character_idx > 0 && character_idx % row_len == 0)
to += '\n';
character_idx++;
}
to += from[i];
}
return to;
}
/** /**