summaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 17:06:32 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 17:06:32 +0000
commit2dad5357405ad33cfa792f04b3ab62a5d188841e (patch)
treeb8f8893942060fe3cfb04ac374cda96fdfc8f453 /plugins
parentInitial commit. (diff)
downloadremmina-2dad5357405ad33cfa792f04b3ab62a5d188841e.tar.xz
remmina-2dad5357405ad33cfa792f04b3ab62a5d188841e.zip
Adding upstream version 1.4.34+dfsg.upstream/1.4.34+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'plugins')
-rw-r--r--plugins/CMakeLists.txt195
-rw-r--r--plugins/README.md29
-rw-r--r--plugins/common/remmina_plugin.h99
-rw-r--r--plugins/exec/CMakeLists.txt55
-rw-r--r--plugins/exec/exec_plugin.c340
-rw-r--r--plugins/exec/exec_plugin_config.h64
-rw-r--r--plugins/exec/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg85
-rw-r--r--plugins/gvnc/CMakeLists.txt54
-rw-r--r--plugins/gvnc/gvnc_plugin.c941
-rw-r--r--plugins/gvnc/gvnc_plugin.h131
-rw-r--r--plugins/gvnc/gvnc_plugin_config.h43
-rw-r--r--plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-ssh-symbolic.svg90
-rw-r--r--plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-symbolic.svg115
-rw-r--r--plugins/kwallet/CMakeLists.txt54
-rw-r--r--plugins/kwallet/src/kwallet_plugin.cpp89
-rw-r--r--plugins/kwallet/src/kwallet_plugin.h52
-rw-r--r--plugins/kwallet/src/kwallet_plugin_main.c146
-rw-r--r--plugins/python_wrapper/CMakeLists.txt68
-rw-r--r--plugins/python_wrapper/pygobject.h669
-rw-r--r--plugins/python_wrapper/python_wrapper_common.c327
-rw-r--r--plugins/python_wrapper/python_wrapper_common.h295
-rw-r--r--plugins/python_wrapper/python_wrapper_entry.c100
-rw-r--r--plugins/python_wrapper/python_wrapper_entry.h63
-rw-r--r--plugins/python_wrapper/python_wrapper_file.c160
-rw-r--r--plugins/python_wrapper/python_wrapper_file.h63
-rw-r--r--plugins/python_wrapper/python_wrapper_plugin.c220
-rw-r--r--plugins/python_wrapper/python_wrapper_plugin.h80
-rw-r--r--plugins/python_wrapper/python_wrapper_pref.c104
-rw-r--r--plugins/python_wrapper/python_wrapper_pref.h63
-rw-r--r--plugins/python_wrapper/python_wrapper_protocol.c315
-rw-r--r--plugins/python_wrapper/python_wrapper_protocol.h109
-rw-r--r--plugins/python_wrapper/python_wrapper_protocol_widget.c845
-rw-r--r--plugins/python_wrapper/python_wrapper_protocol_widget.h65
-rw-r--r--plugins/python_wrapper/python_wrapper_remmina.c1241
-rw-r--r--plugins/python_wrapper/python_wrapper_remmina.h93
-rw-r--r--plugins/python_wrapper/python_wrapper_remmina_file.c205
-rw-r--r--plugins/python_wrapper/python_wrapper_remmina_file.h63
-rw-r--r--plugins/python_wrapper/python_wrapper_secret.c138
-rw-r--r--plugins/python_wrapper/python_wrapper_secret.h68
-rw-r--r--plugins/python_wrapper/python_wrapper_tool.c84
-rw-r--r--plugins/python_wrapper/python_wrapper_tool.h69
-rw-r--r--plugins/rdp/CMakeLists.txt88
-rw-r--r--plugins/rdp/rdp_channels.c96
-rw-r--r--plugins/rdp/rdp_channels.h62
-rw-r--r--plugins/rdp/rdp_cliprdr.c1034
-rw-r--r--plugins/rdp/rdp_cliprdr.h51
-rw-r--r--plugins/rdp/rdp_event.c1494
-rw-r--r--plugins/rdp/rdp_event.h58
-rw-r--r--plugins/rdp/rdp_file.c299
-rw-r--r--plugins/rdp/rdp_file.h48
-rw-r--r--plugins/rdp/rdp_graphics.c163
-rw-r--r--plugins/rdp/rdp_graphics.h40
-rw-r--r--plugins/rdp/rdp_monitor.c231
-rw-r--r--plugins/rdp/rdp_monitor.h45
-rw-r--r--plugins/rdp/rdp_plugin.c3363
-rw-r--r--plugins/rdp/rdp_plugin.h400
-rw-r--r--plugins/rdp/rdp_settings.c759
-rw-r--r--plugins/rdp/rdp_settings.h49
-rw-r--r--plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-ssh-symbolic.svg89
-rw-r--r--plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-symbolic.svg112
-rw-r--r--plugins/secret/CMakeLists.txt50
-rw-r--r--plugins/secret/src/glibsecret_plugin.c217
-rw-r--r--plugins/secret/src/glibsecret_plugin.h40
-rw-r--r--plugins/spice/CMakeLists.txt55
-rw-r--r--plugins/spice/scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg97
-rw-r--r--plugins/spice/scalable/emblems/org.remmina.Remmina-spice-symbolic.svg100
-rw-r--r--plugins/spice/spice_plugin.c836
-rw-r--r--plugins/spice/spice_plugin.h93
-rw-r--r--plugins/spice/spice_plugin_file_transfer.c244
-rw-r--r--plugins/spice/spice_plugin_usb.c100
-rw-r--r--plugins/telepathy/CMakeLists.txt66
-rw-r--r--plugins/telepathy/Remmina.client8
-rw-r--r--plugins/telepathy/org.freedesktop.Telepathy.Client.Remmina.service.in3
-rw-r--r--plugins/telepathy/telepathy_channel_handler.c373
-rw-r--r--plugins/telepathy/telepathy_channel_handler.h43
-rw-r--r--plugins/telepathy/telepathy_handler.c128
-rw-r--r--plugins/telepathy/telepathy_handler.h57
-rw-r--r--plugins/telepathy/telepathy_plugin.c77
-rw-r--r--plugins/tool_hello_world/CMakeLists.txt55
-rw-r--r--plugins/tool_hello_world/plugin.c130
-rw-r--r--plugins/tool_hello_world/plugin_config.h43
-rw-r--r--plugins/tool_hello_world/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg85
-rw-r--r--plugins/vnc/CMakeLists.txt56
-rw-r--r--plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-ssh-symbolic.svg90
-rw-r--r--plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-symbolic.svg115
-rw-r--r--plugins/vnc/vnc_plugin.c2228
-rw-r--r--plugins/vnc/vnc_plugin.h158
-rw-r--r--plugins/www/CMakeLists.txt72
-rw-r--r--plugins/www/resources/js/.eslintrc.json13
-rw-r--r--plugins/www/resources/js/package.json11
-rw-r--r--plugins/www/resources/js/www-js.js98
-rw-r--r--plugins/www/scalable/emblems/org.remmina.Remmina-www-symbolic.svg117
-rw-r--r--plugins/www/www_config.h46
-rw-r--r--plugins/www/www_plugin.c987
-rw-r--r--plugins/www/www_plugin.h80
-rw-r--r--plugins/www/www_utils.c154
-rw-r--r--plugins/www/www_utils.h47
-rw-r--r--plugins/x2go/CMakeLists.txt66
-rw-r--r--plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg297
-rw-r--r--plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg297
-rw-r--r--plugins/x2go/x2go_plugin.c3463
-rw-r--r--plugins/x2go/x2go_plugin.h46
102 files changed, 27911 insertions, 0 deletions
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
new file mode 100644
index 0000000..acb93e0
--- /dev/null
+++ b/plugins/CMakeLists.txt
@@ -0,0 +1,195 @@
+# Remmina - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2011 Marc-Andre Moreau
+# Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. If you
+# do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+# Needed FreeRDP version to build freerdp plugin
+
+
+include_directories(${CMAKE_SOURCE_DIR}/plugins)
+
+set(APPICONSCALE_EMBLEMS_DIR "${REMMINA_DATADIR}/icons/hicolor/scalable/emblems")
+
+set(REMMINA_COMMON_INCLUDE_DIRS ${GTK3_INCLUDE_DIRS})
+set(REMMINA_COMMON_LIBRARIES ${GTK_LIBRARIES} ${Intl_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
+
+find_suggested_package(LIBSSH)
+if(LIBSSH_FOUND)
+ add_definitions(-DHAVE_LIBSSH)
+ include_directories(${SSH_INCLUDE_DIRS})
+ target_link_libraries(remmina ${SSH_LIBRARIES})
+endif()
+
+option(WITH_FREERDP3 "Compile the RDP plugin using symbols from FreeRDP3" OFF)
+if (WITH_FREERDP3)
+ set(FREERDP_REQUIRED_VERSION 3)
+ set(FREERDP_REQUIRED_MAJOR 3)
+ set(FREERDP_REQUIRED_MINOR 0)
+ set(FREERDP_REQUIRED_REVISION 0)
+else()
+ set(FREERDP_REQUIRED_VERSION 2)
+ set(FREERDP_REQUIRED_MAJOR 2)
+ set(FREERDP_REQUIRED_MINOR 0)
+ set(FREERDP_REQUIRED_REVISION 0)
+endif()
+
+find_package(WinPR ${FREERDP_REQUIRED_VERSION} )
+find_package(FreeRDP ${FREERDP_REQUIRED_VERSION} )
+find_package(FreeRDP-Client ${FREERDP_REQUIRED_VERSION} )
+
+
+if (WinPR_FOUND AND FreeRDP_FOUND AND FreeRDP-Client_FOUND)
+ message("Enabling RDP plugin")
+ add_subdirectory(rdp)
+else()
+ message(STATUS "Disabling RDP plugin.")
+endif()
+
+# X2Go
+option(WITH_X2GO "Build X2Go plugin" OFF)
+if(WITH_X2GO)
+ message(STATUS "Enabling X2Go plugin.")
+ message(AUTHOR_WARNING "pyhoca-cli is needed at runtime, be sure to install it")
+ add_subdirectory(x2go)
+else()
+ message(STATUS "Disabling X2Go plugin.")
+endif()
+
+find_suggested_package(LIBVNCSERVER)
+if(LIBVNCSERVER_FOUND)
+ add_subdirectory(vnc)
+endif()
+
+option(WITH_GVNC "Build GVNC plugin" OFF)
+if(WITH_GVNC)
+ message(STATUS "Enabling GVNC plugin.")
+ add_definitions(-DWITH_GVNC)
+ find_suggested_package(GTK-VNC)
+ if(GTK-VNC_FOUND)
+ add_subdirectory(gvnc)
+ endif()
+else()
+ message(STATUS "Disabling GVNC plugin.")
+endif()
+
+find_suggested_package(Spice)
+if(SPICE_FOUND)
+ add_subdirectory(spice)
+endif()
+
+option(WITH_WWW "Build WWW plugin" ON)
+if(WITH_WWW)
+ message(STATUS "Enabling WWW plugin.")
+ add_definitions(-DWITH_WWW)
+ find_suggested_package(WEBKIT2GTK)
+ if(WEBKIT2GTK_FOUND)
+ add_subdirectory(www)
+ endif()
+else()
+ message(STATUS "Disabling WWW plugin.")
+endif()
+
+if(WITH_EXAMPLES)
+ message(STATUS "Enabling examples and test plugins.")
+ add_subdirectory(tool_hello_world)
+endif()
+
+add_subdirectory(exec)
+
+option(WITH_KF5WALLET "Building KF5WALLET plugin" OFF)
+if(WITH_KF5WALLET)
+ CheckHasModule(KF5Wallet)
+ if(HAS_MODULE_KF5Wallet)
+ message(STATUS "Enabling KDE Wallet plugin.")
+ add_definitions(-DWITH_KF5WALLET)
+ add_subdirectory(kwallet)
+ else()
+ message(FATAL_ERROR "libKF5wallet not found but requested")
+ endif()
+endif()
+
+option(WITH_ST "Build Simple Terminal plugin" OFF)
+if(WITH_ST)
+ message(STATUS "Enabling ST plugin.")
+ add_definitions(-DWITH_ST)
+ get_filename_component(_st_fullpath "st" REALPATH)
+ if(EXISTS "${_st_fullpath}" AND EXISTS "${_st_fullpath}/CMakeLists.txt")
+ add_subdirectory(st)
+ else()
+ message(WARNING "Can't find ${_st_fullpath}/CMakeLists.txt")
+ endif()
+endif()
+
+option(WITH_XDMCP "Build XDMCP plugin" OFF)
+if(WITH_XDMCP)
+ add_definitions(-DWITH_XDMCP)
+ message(STATUS "Enabling XDMCP plugin.")
+ get_filename_component(_xdmcp_fullpath "xdmcp" REALPATH)
+ if(EXISTS "${_xdmcp_fullpath}" AND EXISTS "${_xdmcp_fullpath}/CMakeLists.txt")
+ add_subdirectory(xdmcp)
+ else()
+ message(WARNING "Can't find ${_xdmcp_fullpath}/CMakeLists.txt")
+ endif()
+endif()
+
+option(WITH_NX "Build NX plugin" OFF)
+if(WITH_NX)
+ add_definitions(-DWITH_NX)
+ # After the plugin split only the NX plugin needs XKBfile.h
+ find_required_package(XKBFILE)
+ if(LIBSSH_FOUND AND XKBFILE_FOUND)
+ message(STATUS "Enabling NX plugin.")
+ get_filename_component(_nx_fullpath "nx" REALPATH)
+ if(EXISTS "${_nx_fullpath}" AND EXISTS "${_nx_fullpath}/CMakeLists.txt")
+ add_subdirectory(nx)
+ else()
+ message(WARNING "Can't find ${_nx_fullpath}/CMakeLists.txt")
+ endif()
+ endif()
+endif()
+
+option(WITH_PYTHONLIBS "Build Python Wrapper Plugin" ON)
+if(WITH_PYTHONLIBS)
+ message(STATUS "Enabling Python Wrapper Plugin.")
+ add_definitions(-DWITH_PYTHONLIBS)
+ find_suggested_package(PythonLibs)
+ if(PythonLibs_FOUND)
+ add_subdirectory(python_wrapper)
+ endif()
+else()
+ message(STATUS "Disabling Python Wrapper plugin.")
+endif()
+
+#find_suggested_package(TELEPATHY)
+#if(TELEPATHY_FOUND)
+#add_subdirectory(telepathy)
+#endif()
diff --git a/plugins/README.md b/plugins/README.md
new file mode 100644
index 0000000..162a407
--- /dev/null
+++ b/plugins/README.md
@@ -0,0 +1,29 @@
+## Removed plugins
+
+We have removed the folders xdmcp, nx and st due to the fact that Gtk have deprecated the functions
+using the XEmbed protocol.
+
+These folders have been split in a new repository as shown in the following example
+
+```
+# Split of the folder plugins/st in a new branch
+git subtree split -P plugins/st -b plugins/st
+# We add the remote of the new repository
+git remote add plugins/st git@gitlab.com:Remmina/remmina-plugins.git
+# We push to the new repository
+git push plugins/st plugins/st
+# We change to the location of the local copy of git@gitlab.com:Remmina/remmina-plugins.git
+cd ../remmina-plugins/
+# The branch plugins/st does not have the folder plugins, the st source is in the root
+# The following subtree command recreate the folder structure (../Remmina is the main repo)
+git subtree add -P plugins/st ../Remmina plugins/st
+git push
+```
+
+The next step is to `git rm -r plugins/st` and if we need again `st`, it's enough to add it as a submodule:
+
+```
+git submodule add git@gitlab.com:Remmina/remmina-plugins.git plugins
+```
+
+The submodule part has to be tested as it'll cause issues with the existing files
diff --git a/plugins/common/remmina_plugin.h b/plugins/common/remmina_plugin.h
new file mode 100644
index 0000000..f76df5e
--- /dev/null
+++ b/plugins/common/remmina_plugin.h
@@ -0,0 +1,99 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#include "config.h"
+#include <gtk/gtk.h>
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+#include <remmina/plugin.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <signal.h>
+#include <pthread.h>
+#ifdef HAVE_NETDB_H
+#include <netdb.h>
+#endif
+#ifdef HAVE_ARPA_INET_H
+#include <arpa/inet.h>
+#endif
+#ifdef HAVE_NETINET_IN_H
+#include <netinet/in.h>
+#endif
+#ifdef HAVE_SYS_SOCKET_H
+#include <sys/socket.h>
+#endif
+#ifdef HAVE_FCNTL_H
+#include <fcntl.h>
+#endif
+#ifdef HAVE_ERRNO_H
+#include <errno.h>
+#endif
+#include "remmina/remmina_trace_calls.h"
+
+typedef void (*PThreadCleanupFunc)(void *);
+
+
+#define IDLE_ADD gdk_threads_add_idle
+#define TIMEOUT_ADD gdk_threads_add_timeout
+#define CANCEL_ASYNC pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); pthread_testcancel();
+#define CANCEL_DEFER pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
+
+#define THREADS_ENTER _Pragma("GCC error \"THREADS_ENTER has been deprecated in Remmina 1.2\"")
+#define THREADS_LEAVE _Pragma("GCC error \"THREADS_LEAVE has been deprecated in Remmina 1.2\"")
+
+#define MAX_X_DISPLAY_NUMBER 99
+#define X_UNIX_SOCKET "/tmp/.X11-unix/X%d"
+
+#define INCLUDE_GET_AVAILABLE_XDISPLAY static gint \
+ remmina_get_available_xdisplay(void) \
+ { \
+ gint i; \
+ gint display = 0; \
+ gchar fn[200]; \
+ for (i = 1; i < MAX_X_DISPLAY_NUMBER; i++) \
+ { \
+ g_snprintf(fn, sizeof(fn), X_UNIX_SOCKET, i); \
+ if (!g_file_test(fn, G_FILE_TEST_EXISTS)) \
+ { \
+ display = i; \
+ break; \
+ } \
+ } \
+ return display; \
+ }
diff --git a/plugins/exec/CMakeLists.txt b/plugins/exec/CMakeLists.txt
new file mode 100644
index 0000000..4906f3b
--- /dev/null
+++ b/plugins/exec/CMakeLists.txt
@@ -0,0 +1,55 @@
+# remmina-plugin-tool_hello_world - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. If you
+# do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+
+set(REMMINA_PLUGIN_EXEC_SRCS
+ exec_plugin_config.h
+ exec_plugin.c
+)
+
+add_library(remmina-plugin-exec MODULE ${REMMINA_PLUGIN_EXEC_SRCS})
+set_target_properties(remmina-plugin-exec PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-exec PROPERTIES NO_SONAME 1)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${GTK_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-exec ${REMMINA_COMMON_LIBRARIES})
+
+install(TARGETS remmina-plugin-exec DESTINATION ${REMMINA_PLUGINDIR})
+
+install(FILES
+ scalable/emblems/org.remmina.Remmina-tool-symbolic.svg
+ DESTINATION ${APPICONSCALE_EMBLEMS_DIR})
+
+if(WITH_ICON_CACHE)
+ gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor")
+endif()
diff --git a/plugins/exec/exec_plugin.c b/plugins/exec/exec_plugin.c
new file mode 100644
index 0000000..f22b535
--- /dev/null
+++ b/plugins/exec/exec_plugin.c
@@ -0,0 +1,340 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * Initially based on the plugin "Remmina Plugin EXEC", created and written by
+ * Fabio Castelli (Muflone) <muflone@vbsimple.net>.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "exec_plugin_config.h"
+
+#include "common/remmina_plugin.h"
+
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtk.h>
+#include <glib.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define GET_PLUGIN_DATA(gp) (RemminaPluginExecData*)g_object_get_data(G_OBJECT(gp), "plugin-data")
+
+typedef struct _RemminaPluginExecData {
+ GtkWidget *log_view;
+ GtkTextBuffer *log_buffer;
+ GtkTextBuffer *err;
+ GtkWidget *sw;
+ GPid pid;
+} RemminaPluginExecData;
+
+static RemminaPluginService *remmina_plugin_service = NULL;
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__)
+
+
+ static void
+cb_child_watch( GPid pid, gint status)
+{
+ /* Close pid */
+ g_spawn_close_pid( pid );
+}
+
+static void cb_child_setup(gpointer data){
+ int pid = getpid();
+ setpgid(pid, 0);
+}
+
+ static gboolean
+cb_out_watch (GIOChannel *channel, GIOCondition cond, RemminaProtocolWidget *gp)
+{
+ gchar *string;
+ gsize size;
+
+ RemminaPluginExecData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if( cond == G_IO_HUP )
+ {
+ g_io_channel_unref( channel );
+ return FALSE;
+ }
+
+ g_io_channel_read_line( channel, &string, &size, NULL, NULL );
+ gtk_text_buffer_insert_at_cursor( gpdata->log_buffer, string, -1 );
+ g_free( string );
+
+ return TRUE;
+}
+
+ static gboolean
+cb_err_watch (GIOChannel *channel, GIOCondition cond, RemminaProtocolWidget *gp)
+{
+ gchar *string;
+ gsize size;
+
+ RemminaPluginExecData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if( cond == G_IO_HUP )
+ {
+ g_io_channel_unref( channel );
+ return FALSE;
+ }
+
+ g_io_channel_read_line( channel, &string, &size, NULL, NULL );
+ gtk_text_buffer_insert_at_cursor( gpdata->err, string, -1 );
+ g_free( string );
+
+ return TRUE;
+}
+
+static void remmina_plugin_exec_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginExecData *gpdata;
+
+ REMMINA_PLUGIN_DEBUG("[%s] Plugin init", PLUGIN_NAME);
+
+ gpdata = g_new0(RemminaPluginExecData, 1);
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
+
+ gpdata->pid = 0;
+ gpdata->log_view = gtk_text_view_new();
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(gpdata->log_view), GTK_WRAP_CHAR);
+ gtk_text_view_set_editable(GTK_TEXT_VIEW(gpdata->log_view), FALSE);
+ gtk_text_view_set_left_margin (GTK_TEXT_VIEW (gpdata->log_view), 20);
+ gtk_text_view_set_right_margin (GTK_TEXT_VIEW (gpdata->log_view), 20);
+ gpdata->log_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (gpdata->log_view));
+ gpdata->sw = gtk_scrolled_window_new (NULL, NULL);
+ gtk_widget_set_size_request (gpdata->sw, 640, 480);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (gpdata->sw),
+ GTK_POLICY_AUTOMATIC,
+ GTK_POLICY_AUTOMATIC);
+ gtk_container_add(GTK_CONTAINER(gp), gpdata->sw);
+ gtk_container_add(GTK_CONTAINER(gpdata->sw), gpdata->log_view);
+ gtk_text_buffer_set_text (gpdata->log_buffer, "Remmina Exec Plugin Logger", -1);
+
+ gtk_widget_show_all(gpdata->sw);
+}
+
+static gboolean remmina_plugin_exec_run(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaFile* remminafile;
+ const gchar *cmd;
+ gchar *stdout_buffer;
+ gchar *stderr_buffer;
+ char **argv;
+ GError *error = NULL;
+ GPid child_pid;
+ gint child_stdout, child_stderr;
+ GtkDialog *dialog;
+ GIOChannel *out_ch, *err_ch;
+
+ REMMINA_PLUGIN_DEBUG("[%s] Plugin run", PLUGIN_NAME);
+ RemminaPluginExecData *gpdata = GET_PLUGIN_DATA(gp);
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ cmd = remmina_plugin_service->file_get_string(remminafile, "execcommand");
+ if (!cmd) {
+ gtk_text_buffer_set_text (gpdata->log_buffer,
+ _("You did not set any command to be executed"), -1);
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+ return TRUE;
+ }
+
+ g_shell_parse_argv(cmd, NULL, &argv, &error);
+ if (error) {
+ gtk_text_buffer_set_text (gpdata->log_buffer, error->message, -1);
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+ g_error_free(error);
+ return TRUE;
+ }
+
+ if (remmina_plugin_service->file_get_int(remminafile, "runasync", FALSE)) {
+ REMMINA_PLUGIN_DEBUG("[%s] Run Async", PLUGIN_NAME);
+ g_spawn_async_with_pipes( NULL,
+ argv,
+ NULL,
+ G_SPAWN_DO_NOT_REAP_CHILD |
+ G_SPAWN_SEARCH_PATH_FROM_ENVP |
+ G_SPAWN_SEARCH_PATH,
+ cb_child_setup,
+ NULL,
+ &child_pid,
+ NULL,
+ &child_stdout,
+ &child_stderr,
+ &error);
+ if (error != NULL) {
+ gtk_text_buffer_set_text (gpdata->log_buffer, error->message, -1);
+ g_error_free(error);
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+ return TRUE;
+ }
+ g_child_watch_add(child_pid, (GChildWatchFunc)cb_child_watch, gp );
+
+ gpdata->pid = child_pid;
+ /* Create channels that will be used to read data from pipes. */
+ out_ch = g_io_channel_unix_new(child_stdout);
+ err_ch = g_io_channel_unix_new(child_stderr);
+ /* Add watches to channels */
+ g_io_add_watch(out_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_out_watch, gp );
+ g_io_add_watch(err_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_err_watch, gp );
+
+ }else {
+ dialog = GTK_DIALOG(gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL,
+ GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
+ _("Warning: Running a command synchronously may cause Remmina not to respond.\nDo you really want to continue?")));
+ gint result = gtk_dialog_run (GTK_DIALOG (dialog));
+
+ switch (result)
+ {
+ case GTK_RESPONSE_YES:
+ break;
+ default:
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+ return FALSE;
+ break;
+ }
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+ REMMINA_PLUGIN_DEBUG("[%s] Run Sync", PLUGIN_NAME);
+ g_spawn_sync (NULL, // CWD or NULL
+ argv,
+ NULL, // ENVP or NULL
+ G_SPAWN_SEARCH_PATH |
+ G_SPAWN_SEARCH_PATH_FROM_ENVP,
+ NULL,
+ NULL,
+ &stdout_buffer, // STDOUT
+ &stderr_buffer, // STDERR
+ NULL, // Exit status
+ &error);
+ if (!error) {
+ REMMINA_PLUGIN_DEBUG("[%s] Command executed", PLUGIN_NAME);
+ gtk_text_buffer_set_text (gpdata->log_buffer, stdout_buffer, -1);
+ }else {
+ g_warning("Command %s exited with error: %s\n", cmd, error->message);
+ gtk_text_buffer_set_text (gpdata->log_buffer, error->message, -1);
+ g_error_free(error);
+ }
+ }
+
+ g_strfreev(argv);
+
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+ return TRUE;
+}
+
+static gboolean remmina_plugin_exec_close(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("[%s] Plugin close", PLUGIN_NAME);
+ RemminaPluginExecData *gpdata = GET_PLUGIN_DATA(gp);
+ //if async process was started, make sure it's dead if option is selected
+ RemminaFile* remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "kill_proc", FALSE)) {
+ if (gpdata->pid !=0 ){
+ int pgid = getpgid(gpdata->pid);
+ if (pgid != 0){
+ kill(-gpdata->pid, SIGHUP);
+ }
+ else{
+ kill(gpdata->pid, SIGHUP);
+ }
+
+ }
+ }
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ return FALSE;
+}
+
+/* Array of RemminaProtocolSetting for basic settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting tooltip
+ * g) Validation data pointer, will be passed to the validation callback method.
+ * h) Validation callback method (Can be NULL. Every entry will be valid then.)
+ * use following prototype:
+ * gboolean mysetting_validator_method(gpointer key, gpointer value,
+ * gpointer validator_data);
+ * gpointer key is a gchar* containing the setting's name,
+ * gpointer value contains the value which should be validated,
+ * gpointer validator_data contains your passed data.
+ */
+static const RemminaProtocolSetting remmina_plugin_exec_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "execcommand", N_("Command"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "runasync", N_("Asynchronous execution"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "kill_proc", N_("Kill process on disconnect"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL }
+};
+
+/* Protocol plugin definition and features */
+static RemminaProtocolPlugin remmina_plugin = {
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ PLUGIN_NAME, // Name
+ PLUGIN_DESCRIPTION, // Description
+ GETTEXT_PACKAGE, // Translation domain
+ PLUGIN_VERSION, // Version number
+ PLUGIN_APPICON, // Icon for normal connection
+ PLUGIN_APPICON, // Icon for SSH connection
+ remmina_plugin_exec_basic_settings, // Array for basic settings
+ NULL, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type
+ NULL, // Array for available features
+ remmina_plugin_exec_init, // Plugin initialization
+ remmina_plugin_exec_run, // Plugin open connection
+ remmina_plugin_exec_close, // Plugin close connection
+ NULL, // Query for available features
+ NULL, // Call a feature
+ NULL, // Send a keystroke
+ NULL, // No screenshot support available
+ NULL, // RCW map event
+ NULL // RCW unmap event
+};
+
+G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_service = service;
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+ if (!service->register_plugin((RemminaPlugin*)&remmina_plugin)) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/plugins/exec/exec_plugin_config.h b/plugins/exec/exec_plugin_config.h
new file mode 100644
index 0000000..d62a4fa
--- /dev/null
+++ b/plugins/exec/exec_plugin_config.h
@@ -0,0 +1,64 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * Initially based on the plugin "Remmina Plugin EXEC", created and written by
+ * Fabio Castelli (Muflone) <muflone@vbsimple.net>.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#define PLUGIN_NAME "EXEC"
+#define PLUGIN_DESCRIPTION N_("Execute a command")
+#define PLUGIN_VERSION "1.0"
+#define PLUGIN_APPICON "org.remmina.Remmina-tool-symbolic"
+
+#define REMMINA_PLUGIN_INFO(fmt, ...) \
+ remmina_plugin_service->_remmina_info(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_MESSAGE(fmt, ...) \
+ remmina_plugin_service->_remmina_message(__func, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) \
+ remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_WARNING(fmt, ...) \
+ remmina_plugin_service->_remmina_warning(__func__, fmt, ##__VA_ARGS__)
+
+/* This will intentionally crash Remmina */
+#define REMMINA_PLUGIN_ERROR(fmt, ...) \
+ remmina_plugin_service->_remmina_error(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_CRITICAL(fmt, ...) \
+ remmina_plugin_service->_remmina_critical(__func__, fmt, ##__VA_ARGS__)
+#define REMMINA_PLUGIN_AUDIT(fmt, ...) \
+ remmina_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__)
diff --git a/plugins/exec/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg b/plugins/exec/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg
new file mode 100644
index 0000000..f383629
--- /dev/null
+++ b/plugins/exec/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ version="1.1"
+ id="svg8"
+ inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
+ sodipodi:docname="org.remmina.Remmina-tool-symbolic.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="11.871974"
+ inkscape:cx="34.28242"
+ inkscape:cy="27.670209"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="995"
+ inkscape:window-x="1080"
+ inkscape:window-y="427"
+ inkscape:window-maximized="1"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:deskcolor="#d1d1d1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-171.36869"
+ originy="-105.18186" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-171.36866,-165.16877)">
+ <path
+ d="m 187.77406,165.16877 v 0.20572 l -7.85028,10.64488 c 0.22721,0.0656 0.45083,0.14294 0.66991,0.23189 -0.77754,-0.31322 -1.62348,-0.4954 -2.51368,-0.4954 -1.77942,0 -3.48494,0.70612 -4.74227,1.95807 -0.35468,0.35318 -0.66823,0.74465 -0.93515,1.16728 l 3.12261,3.10514 -2.0827,2.82424 c 0.4982,0.95633 1.28315,1.73475 2.24581,2.22754 l 2.83728,-2.06819 3.12548,3.10799 c 0.42429,-0.26637 0.817,-0.57912 1.17098,-0.93253 1.25898,-1.25029 1.96741,-2.94651 1.96908,-4.71574 0,-0.8619 -0.17842,-1.6798 -0.47762,-2.43611 0.0814,0.19645 0.15331,0.39646 0.21549,0.59958 l 10.70216,-8.009 z m -12.05944,0.3332 -4.21776,4.19702 5.47604,5.44278 4.21801,-4.19416 z m 13.91206,13.83421 -4.21801,4.19703 5.47628,5.44563 4.21801,-4.19726 z m -17.14253,6.67356 -1.11549,1.63082 1.49123,1.48312 1.52064,-1.10949 c -0.75315,-0.49516 -1.39819,-1.13659 -1.89638,-1.88553 z"
+ id="path32"
+ inkscape:connector-curvature="0"
+ style="isolation:isolate;fill:#171717;stroke-width:0.2385" />
+ </g>
+</svg>
diff --git a/plugins/gvnc/CMakeLists.txt b/plugins/gvnc/CMakeLists.txt
new file mode 100644
index 0000000..ca5c1cd
--- /dev/null
+++ b/plugins/gvnc/CMakeLists.txt
@@ -0,0 +1,54 @@
+# remmina-plugin-gvnc - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. If you
+# do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+set(REMMINA_PLUGIN_GVNC_SRCS
+ gvnc_plugin_config.h
+ gvnc_plugin.h
+ gvnc_plugin.c
+ )
+
+add_library(remmina-plugin-gvnc MODULE ${REMMINA_PLUGIN_GVNC_SRCS})
+set_target_properties(remmina-plugin-gvnc PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-gvnc PROPERTIES NO_SONAME 1)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${GTK-VNC_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-gvnc ${REMMINA_COMMON_LIBRARIES} ${GTK-VNC_LIBRARIES})
+
+install(TARGETS remmina-plugin-gvnc DESTINATION ${REMMINA_PLUGINDIR})
+
+install(FILES
+ scalable/emblems/org.remmina.Remmina-gvnc-symbolic.svg
+ DESTINATION ${APPICONSCALE_EMBLEMS_DIR})
+
+if(WITH_ICON_CACHE)
+ gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor")
+endif()
diff --git a/plugins/gvnc/gvnc_plugin.c b/plugins/gvnc/gvnc_plugin.c
new file mode 100644
index 0000000..14cead9
--- /dev/null
+++ b/plugins/gvnc/gvnc_plugin.c
@@ -0,0 +1,941 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "gvnc_plugin_config.h"
+#include "gvnc_plugin.h"
+
+#include <vncdisplay.h>
+#include <vncutil.h>
+#include <vncaudiopulse.h>
+
+#define GVNC_DEFAULT_PORT 5900
+
+#ifndef VNC_CHECK_VERSION
+# define VNC_CHECK_VERSION(a, b, c) 0
+#endif
+#if VNC_CHECK_VERSION(1, 2, 0)
+# define HAVE_VNC_REMOTE_RESIZE
+# define HAVE_VNC_POWER_CONTROL
+#endif
+
+enum {
+ GVNC_PLUGIN_FEATURE_PREF_VIEWONLY = 1,
+ GVNC_PLUGIN_FEATURE_DYNRESUPDATE,
+ GVNC_PLUGIN_FEATURE_PREF_DISABLECLIPBOARD,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTDEL,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTBACKSPACE,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF1,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF2,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF3,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF4,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF5,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF6,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF7,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF8,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF9,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF10,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF11,
+ GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF12,
+#ifdef HAVE_VNC_POWER_CONTROL
+ GVNC_PLUGIN_FEATURE_TOOL_REBOOT,
+ GVNC_PLUGIN_FEATURE_TOOL_RESET,
+ GVNC_PLUGIN_FEATURE_TOOL_SHUTDOWN,
+#endif
+ GVNC_PLUGIN_FEATURE_SCALE
+};
+
+
+static RemminaPluginService *remmina_plugin_service = NULL;
+
+gchar* str_replace(const gchar *string, const gchar *search, const gchar *replacement)
+{
+ TRACE_CALL(__func__);
+ gchar *str, **arr;
+
+ g_return_val_if_fail(string != NULL, NULL);
+ g_return_val_if_fail(search != NULL, NULL);
+
+ if (replacement == NULL)
+ replacement = "";
+
+ arr = g_strsplit(string, search, -1);
+ if (arr != NULL && arr[0] != NULL)
+ str = g_strjoinv(replacement, arr);
+ else
+ str = g_strdup(string);
+
+ g_strfreev(arr);
+ return str;
+}
+
+/* Send a keystroke to the plugin window */
+static void gvnc_plugin_keystroke(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (gpdata->vnc) {
+ vnc_display_send_keys(VNC_DISPLAY(gpdata->vnc),
+ keystrokes,
+ keylen);
+ }
+}
+
+#ifdef HAVE_VNC_POWER_CONTROL
+static void gvnc_plugin_power_ctrl(RemminaProtocolWidget *gp, VncConnectionPowerAction action)
+{
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ VncConnection *conn = vnc_display_get_connection(VNC_DISPLAY(gpdata->vnc));
+
+ if (!vnc_connection_get_power_control(conn)) {
+ REMMINA_PLUGIN_DEBUG("Power control not available on this server");
+ return;
+ }
+ vnc_connection_power_control(conn, action);
+}
+#endif
+
+
+static void gvnc_plugin_mouse_grab(GtkWidget *vncdisplay, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ VncGrabSequence *seq = vnc_display_get_grab_keys(VNC_DISPLAY(gpdata->vnc));
+ gchar *seqstr = vnc_grab_sequence_as_string(seq);
+
+ REMMINA_PLUGIN_DEBUG("Pointer grabbed: %s", seqstr);
+}
+
+static void gvnc_plugin_mouse_ungrab(GtkWidget *vncdisplay, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Pointer ungrabbed");
+}
+
+static void gvnc_plugin_on_vnc_error(GtkWidget *vncdisplay G_GNUC_UNUSED, const gchar *msg, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+
+ REMMINA_PLUGIN_DEBUG("Error: %s\n", msg);
+ /* "vnc-error" is always followed by "vnc-disconnected",
+ * so save the error for that signal */
+ g_free(gpdata->error_msg);
+ gpdata->error_msg = g_strdup(msg);
+}
+
+static gboolean gvnc_plugin_get_screenshot(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd) __attribute__ ((unused));
+static gboolean gvnc_plugin_get_screenshot(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd)
+{
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ //gsize szmem;
+ const VncPixelFormat *currentFormat;
+ //GError *err = NULL;
+
+ if (!gpdata)
+ return FALSE;
+
+ /* Get current pixel format for server */
+ currentFormat = vnc_connection_get_pixel_format(gpdata->conn);
+
+
+ GdkPixbuf *pix = vnc_display_get_pixbuf(VNC_DISPLAY(gpdata->vnc));
+
+ rpsd->width = gdk_pixbuf_get_width(pix);
+ rpsd->height = gdk_pixbuf_get_height(pix);
+ rpsd->bitsPerPixel = currentFormat->bits_per_pixel;
+ rpsd->bytesPerPixel = rpsd->bitsPerPixel / 8;
+ //szmem = gdk_pixbuf_get_byte_length(pix);
+
+ //szmem = rpsd->width * rpsd->height * rpsd->bytesPerPixel;
+
+ //REMMINA_PLUGIN_DEBUG("allocating %zu bytes for a full screenshot", szmem);
+ //REMMINA_PLUGIN_DEBUG("Calculated screenshot size: %zu", szcalc);
+ //rpsd->buffer = malloc(szmem);
+
+ //memcpy(rpsd->buffer, pix, szmem);
+ //gdk_pixbuf_save_to_buffer(pix, &rpsd->buffer, &szmem, "jpeg", &err, "quality", "100", NULL);
+
+
+ /* Returning TRUE instruct also the caller to deallocate rpsd->buffer */
+ return TRUE;
+}
+
+void gvnc_plugin_paste_text(RemminaProtocolWidget *gp, const gchar *text)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ gchar *out;
+ gsize a, b;
+ GError *error = NULL;
+
+ if (!gpdata) return;
+
+ out = g_convert_with_fallback(text, -1, "iso8859-1//TRANSLIT", "utf-8", NULL, &a, &b, &error);
+ if (out) {
+ REMMINA_PLUGIN_DEBUG("Pasting text");
+ vnc_display_client_cut_text(VNC_DISPLAY(gpdata->vnc), out);
+ g_free(out);
+ } else {
+ REMMINA_PLUGIN_DEBUG("Error pasting text: %s", error->message);
+ g_error_free(error);
+ }
+}
+
+static void gvnc_plugin_clipboard_cb(GtkClipboard *cb, GdkEvent *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ //GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ gchar *text;
+
+ REMMINA_PLUGIN_DEBUG("owner-change event received");
+
+ if (cb && gtk_clipboard_get_owner(cb) == (GObject *)gp)
+ return;
+
+ text = gtk_clipboard_wait_for_text(cb);
+ if (!text)
+ return;
+
+ gvnc_plugin_paste_text(gp, text);
+ g_free(text);
+}
+
+
+/* text was actually requested */
+static void gvnc_plugin_clipboard_copy(GtkClipboard *clipboard G_GNUC_UNUSED, GtkSelectionData *data, guint info G_GNUC_UNUSED, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (!gpdata) return;
+ gtk_selection_data_set_text(data, gpdata->clipstr, -1);
+ REMMINA_PLUGIN_DEBUG("Text copied");
+}
+
+static void gvnc_plugin_cut_text(VncDisplay *vnc G_GNUC_UNUSED, const gchar *text, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+
+ REMMINA_PLUGIN_DEBUG("Got clipboard request for “%s”", text);
+
+ GtkClipboard *cb;
+ gsize a, b;
+ GtkTargetEntry targets[] = {
+ { g_strdup("UTF8_STRING"), 0, 0 },
+ { g_strdup("COMPOUND_TEXT"), 0, 0 },
+ { g_strdup("TEXT"), 0, 0 },
+ { g_strdup("STRING"), 0, 0 },
+ };
+
+ if (!text)
+ return;
+ g_free(gpdata->clipstr);
+ gpdata->clipstr = g_convert(text, -1, "utf-8", "iso8859-1", &a, &b, NULL);
+
+ if (gpdata->clipstr) {
+ cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
+
+ REMMINA_PLUGIN_DEBUG("setting clipboard with owner to owner %p", gp);
+ gtk_clipboard_set_with_owner(cb,
+ targets,
+ G_N_ELEMENTS(targets),
+ (GtkClipboardGetFunc)gvnc_plugin_clipboard_copy,
+ NULL,
+ G_OBJECT(gp));
+ }
+
+ //g_signal_emit_by_name(session, "session-cut-text", text);
+}
+
+
+static void gvnc_plugin_desktop_resize(GtkWidget *vncdisplay G_GNUC_UNUSED, int width, int height, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+
+ REMMINA_PLUGIN_DEBUG("Remote desktop size changed to %dx%d\n", width, height);
+ REMMINA_PLUGIN_DEBUG("Desktop size is set to %dx%d\n", gpdata->width, gpdata->height);
+ remmina_plugin_service->protocol_plugin_set_width(gp, width);
+ remmina_plugin_service->protocol_plugin_set_height(gp, height);
+
+ remmina_plugin_service->protocol_plugin_desktop_resize(gp);
+}
+
+static void gvnc_plugin_on_bell(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Bell message received");
+ //GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "disableserverbell", FALSE))
+ return;
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(gp));
+
+ if (window)
+ gdk_window_beep(window);
+ REMMINA_PLUGIN_DEBUG("Beep emitted");
+}
+static void gvnc_plugin_update_scale_mode(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaScaleMode scaleMode = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp);
+
+#if 0
+ g_object_set(gpdata->display,
+ "scaling", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED),
+ "resize-guest", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES),
+ NULL);
+#endif
+ if (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED || scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES)
+ vnc_display_set_scaling(VNC_DISPLAY(gpdata->vnc), TRUE);
+ else
+ vnc_display_set_scaling(VNC_DISPLAY(gpdata->vnc), FALSE);
+
+ //width = remmina_plugin_service->protocol_plugin_get_width(gp);
+ //height = remmina_plugin_service->protocol_plugin_get_height(gp);
+ gpdata->width = vnc_display_get_width(VNC_DISPLAY(gpdata->vnc));
+ gpdata->height = vnc_display_get_height(VNC_DISPLAY(gpdata->vnc));
+
+ if (scaleMode != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE) {
+ /* In scaled mode, the VncDisplay will get its dimensions from its parent */
+ gtk_widget_set_size_request(GTK_WIDGET(gpdata->vnc), -1, -1);
+ } else {
+ /* In non scaled mode, the plugins forces dimensions of the VncDisplay */
+#if 0
+ g_object_get(gpdata->display_channel,
+ "width", &width,
+ "height", &height,
+ NULL);
+ gtk_widget_set_size_request(GTK_WIDGET(gpdata->vnc), width, height);
+#endif
+ gtk_widget_set_size_request(GTK_WIDGET(gpdata->vnc), gpdata->width, gpdata->height);
+ }
+ remmina_plugin_service->protocol_plugin_update_align(gp);
+}
+
+static gboolean gvnc_plugin_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+
+ return TRUE;
+}
+
+static void gvnc_plugin_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ guint keys[3];
+
+ switch (feature->id) {
+ case GVNC_PLUGIN_FEATURE_PREF_VIEWONLY:
+ gpdata->viewonly = remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE);
+ break;
+ case GVNC_PLUGIN_FEATURE_DYNRESUPDATE:
+ case GVNC_PLUGIN_FEATURE_SCALE:
+ gvnc_plugin_update_scale_mode(gp);
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTDEL:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_Delete;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTBACKSPACE:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_BackSpace;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF1:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F1;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF2:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F2;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF3:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F3;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF4:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F4;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF5:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F5;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF6:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F6;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF7:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F7;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF9:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F9;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF10:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F10;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF11:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F11;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF12:
+ keys[0] = GDK_KEY_Control_L;
+ keys[1] = GDK_KEY_Alt_L;
+ keys[2] = GDK_KEY_F12;
+ gvnc_plugin_keystroke(gp, keys, G_N_ELEMENTS(keys));
+ break;
+#ifdef HAVE_VNC_POWER_CONTROL
+ case GVNC_PLUGIN_FEATURE_TOOL_REBOOT:
+ gvnc_plugin_power_ctrl(gp, VNC_CONNECTION_POWER_ACTION_REBOOT);
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_RESET:
+ gvnc_plugin_power_ctrl(gp, VNC_CONNECTION_POWER_ACTION_RESET);
+ break;
+ case GVNC_PLUGIN_FEATURE_TOOL_SHUTDOWN:
+ gvnc_plugin_power_ctrl(gp, VNC_CONNECTION_POWER_ACTION_SHUTDOWN);
+ break;
+#endif
+ default:
+ break;
+ }
+}
+
+static void gvnc_plugin_auth_unsupported(VncDisplay *vnc G_GNUC_UNUSED, unsigned int authType, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+
+ g_clear_pointer(&gpdata->error_msg, g_free);
+ gchar *msg = g_strdup_printf(_("Unsupported authentication type %u"), authType);
+
+ remmina_plugin_service->protocol_plugin_set_error(gp, "%s", msg);
+ g_free(msg);
+}
+
+static void gvnc_plugin_auth_failure(VncDisplay *vnc G_GNUC_UNUSED, const gchar *reason, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+
+ g_clear_pointer(&gpdata->error_msg, g_free);
+ gchar *msg = g_strdup_printf(_("Authentication failure: %s"), reason);
+
+ remmina_plugin_service->protocol_plugin_set_error(gp, "%s", msg);
+ g_free(msg);
+}
+
+static gboolean gvnc_plugin_ask_auth(GtkWidget *vncdisplay, GValueArray *credList, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gint ret;
+ gboolean disablepasswordstoring;
+ gchar *s_username = NULL, *s_password = NULL;
+ gboolean wantPassword = FALSE, wantUsername = FALSE;
+ int i;
+ gboolean save;
+
+
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+
+ GArray *credArray = g_array_sized_new(FALSE, TRUE, sizeof(GValue), credList->n_values);
+ g_array_set_clear_func(credArray, (GDestroyNotify) g_value_unset);
+ g_array_append_vals(credArray, credList->values, credList->n_values);
+
+ REMMINA_PLUGIN_DEBUG("Got credential request for %d credential(s)\n", credArray->len);
+
+ for (i = 0 ; i < credArray->len ; i++) {
+ GValue *cred = &g_array_index(credArray, GValue, i);
+ switch (g_value_get_enum(cred)) {
+ case VNC_DISPLAY_CREDENTIAL_USERNAME:
+ wantUsername = TRUE;
+ break;
+ case VNC_DISPLAY_CREDENTIAL_PASSWORD:
+ wantPassword = TRUE;
+ break;
+ case VNC_DISPLAY_CREDENTIAL_CLIENTNAME:
+ break;
+ default:
+ REMMINA_PLUGIN_DEBUG("Unsupported credential type %d", g_value_get_enum(cred));
+ vnc_display_close(VNC_DISPLAY(gpdata->vnc));
+ goto cleanup;
+ }
+ }
+ disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE);
+
+ ret = remmina_plugin_service->protocol_plugin_init_auth(gp,
+ (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD)
+ | (wantUsername ? REMMINA_MESSAGE_PANEL_FLAG_USERNAME : 0),
+ _("Enter VNC authentication credentials"),
+ (wantUsername ? remmina_plugin_service->file_get_string(remminafile, "username") : NULL),
+ (wantPassword ? remmina_plugin_service->file_get_string(remminafile, "password") : NULL),
+ NULL,
+ NULL);
+ if (ret == GTK_RESPONSE_OK) {
+ s_username = remmina_plugin_service->protocol_plugin_init_get_username(gp);
+ s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp);
+ remmina_plugin_service->file_set_string(remminafile, "username", s_username);
+
+ save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp);
+ if (save) {
+ // User has requested to save credentials. We put the password
+ // into remminafile->settings. It will be saved later, on successful connection, by
+ // rcw.c
+ remmina_plugin_service->file_set_string(remminafile, "password", s_password);
+ } else {
+ remmina_plugin_service->file_set_string(remminafile, "password", NULL);
+ }
+
+ for (i = 0 ; i < credArray->len ; i++) {
+ GValue *cred = &g_array_index(credArray, GValue, i);
+ switch (g_value_get_enum(cred)) {
+ case VNC_DISPLAY_CREDENTIAL_USERNAME:
+ if (!s_username ||
+ vnc_display_set_credential(VNC_DISPLAY(gpdata->vnc),
+ g_value_get_enum(cred),
+ s_username)) {
+ g_debug("Failed to set credential type %d", g_value_get_enum(cred));
+ vnc_display_close(VNC_DISPLAY(gpdata->vnc));
+ }
+ break;
+ case VNC_DISPLAY_CREDENTIAL_PASSWORD:
+ if (!s_password ||
+ vnc_display_set_credential(VNC_DISPLAY(gpdata->vnc),
+ g_value_get_enum(cred),
+ s_password)) {
+ g_debug("Failed to set credential type %d", g_value_get_enum(cred));
+ vnc_display_close(VNC_DISPLAY(gpdata->vnc));
+ }
+ break;
+ case VNC_DISPLAY_CREDENTIAL_CLIENTNAME:
+ if (vnc_display_set_credential(VNC_DISPLAY(gpdata->vnc),
+ g_value_get_enum(cred),
+ "remmina")) {
+ g_debug("Failed to set credential type %d", g_value_get_enum(cred));
+ vnc_display_close(VNC_DISPLAY(gpdata->vnc));
+ }
+ break;
+ default:
+ g_debug("Unsupported credential type %d", g_value_get_enum(cred));
+ vnc_display_close(VNC_DISPLAY(gpdata->vnc));
+ }
+ }
+
+ if (s_username) g_free(s_username);
+ if (s_password) g_free(s_password);
+
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+
+cleanup:
+ g_free(s_username);
+ g_free(s_password);
+
+ //g_object_set(gpdata->session, "password", s_password, NULL);
+ return TRUE;
+}
+
+static void gvnc_plugin_initialized(GtkWidget *vncdisplay, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ REMMINA_PLUGIN_DEBUG("Connection initialized");
+ g_return_if_fail(gpdata != NULL);
+ REMMINA_PLUGIN_DEBUG("Presenting the window");
+
+ VncAudioFormat format = {
+ VNC_AUDIO_FORMAT_RAW_S32,
+ 2,
+ 44100,
+ };
+
+ REMMINA_PLUGIN_DEBUG("Gathering the VNC connection object");
+ gpdata->conn = vnc_display_get_connection(VNC_DISPLAY(gpdata->vnc));
+ g_return_if_fail(gpdata->conn != NULL);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "enableaudio", FALSE)) {
+ REMMINA_PLUGIN_DEBUG("Setting up VNC audio channel");
+ if (vnc_connection_set_audio_format(gpdata->conn, &format))
+ REMMINA_PLUGIN_DEBUG("VNC audio format set");
+ else {
+ REMMINA_PLUGIN_DEBUG("VNC audio format returned an error");
+ return;
+ }
+
+ if (vnc_connection_set_audio(gpdata->conn, VNC_AUDIO(gpdata->pa)))
+ REMMINA_PLUGIN_DEBUG("VNC audio channel has been set");
+ else {
+ REMMINA_PLUGIN_DEBUG("VNC audio channel cannot be set");
+ return;
+ }
+ REMMINA_PLUGIN_DEBUG("Enabling audio");
+ if (vnc_connection_audio_enable(gpdata->conn))
+ REMMINA_PLUGIN_DEBUG("Audio enabled");
+ else
+ REMMINA_PLUGIN_DEBUG("Audio cannot be enabled");
+ }
+ gpdata->width = vnc_display_get_width(VNC_DISPLAY(gpdata->vnc));
+ gpdata->width = vnc_display_get_height(VNC_DISPLAY(gpdata->vnc));
+}
+
+static void gvnc_plugin_disconnected(VncDisplay *vnc G_GNUC_UNUSED, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ //GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+
+ REMMINA_PLUGIN_DEBUG("[%s] Plugin disconnected", PLUGIN_NAME);
+}
+static gboolean gvnc_plugin_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+
+ REMMINA_PLUGIN_DEBUG("[%s] Plugin close connection", PLUGIN_NAME);
+
+ if (gpdata) {
+ if (gpdata->error_msg) g_free(gpdata->error_msg);
+ if (gpdata->vnc)
+ vnc_display_close(VNC_DISPLAY(gpdata->vnc));
+ //g_object_unref(gpdata->vnc);
+ }
+
+ /* Remove instance->context from gp object data to avoid double free */
+ g_object_steal_data(G_OBJECT(gp), "plugin-data");
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ return FALSE;
+}
+
+static void gvnc_plugin_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GVncPluginData *gpdata;
+ //VncGrabSequence *seq;
+
+ GtkClipboard *cb;
+
+ gpdata = g_new0(GVncPluginData, 1);
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
+
+ gpdata->pa = NULL;
+
+ REMMINA_PLUGIN_DEBUG("[%s] Plugin init", PLUGIN_NAME);
+
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "gvncdebug", FALSE))
+ vnc_util_set_debug(TRUE);
+
+ gpdata->vnc = vnc_display_new();
+ if (remmina_plugin_service->file_get_int(remminafile, "enableaudio", FALSE))
+ gpdata->pa = vnc_audio_pulse_new();
+
+
+ g_signal_connect(gpdata->vnc, "vnc-auth-credential",
+ G_CALLBACK(gvnc_plugin_ask_auth), gp);
+ g_signal_connect(gpdata->vnc, "vnc-auth-failure",
+ G_CALLBACK(gvnc_plugin_auth_failure), gp);
+ g_signal_connect(gpdata->vnc, "vnc-auth-unsupported",
+ G_CALLBACK(gvnc_plugin_auth_unsupported), gp);
+ g_signal_connect(gpdata->vnc, "vnc-disconnected",
+ G_CALLBACK(gvnc_plugin_disconnected), gp);
+ g_signal_connect(gpdata->vnc, "vnc-initialized",
+ G_CALLBACK(gvnc_plugin_initialized), gp);
+ g_signal_connect(gpdata->vnc, "vnc-desktop-resize",
+ G_CALLBACK(gvnc_plugin_desktop_resize), gp);
+ g_signal_connect(gpdata->vnc, "vnc-bell",
+ G_CALLBACK(gvnc_plugin_on_bell), gp);
+ g_signal_connect(gpdata->vnc, "vnc-error",
+ G_CALLBACK(gvnc_plugin_on_vnc_error), gp);
+ g_signal_connect(gpdata->vnc, "vnc-pointer-grab",
+ G_CALLBACK(gvnc_plugin_mouse_grab), gp);
+ g_signal_connect(gpdata->vnc, "vnc-pointer-ungrab",
+ G_CALLBACK(gvnc_plugin_mouse_ungrab), gp);
+ g_signal_connect(gpdata->vnc, "vnc-server-cut-text",
+ G_CALLBACK(gvnc_plugin_cut_text), gp);
+ //seq = vnc_grab_sequence_new_from_string ("Control_R");
+ //vnc_display_set_grab_keys(VNC_DISPLAY(gpdata->vnc), seq);
+ //vnc_grab_sequence_free(seq);
+
+ /* Setup the clipboard */
+ cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
+ gpdata->signal_clipboard = g_signal_connect(cb,
+ "owner-change",
+ G_CALLBACK(gvnc_plugin_clipboard_cb),
+ gp);
+}
+
+static gboolean gvnc_plugin_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gint port;
+ gchar *host = NULL, *tunnel = NULL;
+ GVncPluginData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ REMMINA_PLUGIN_DEBUG("[%s] Plugin open connection", PLUGIN_NAME);
+
+ gpdata->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add(GTK_CONTAINER(gp), gpdata->box);
+ gtk_widget_set_hexpand(GTK_WIDGET(gpdata->vnc), TRUE);
+ gtk_widget_set_vexpand(GTK_WIDGET(gpdata->vnc), TRUE);
+ gtk_container_add(GTK_CONTAINER(gpdata->box), gpdata->vnc);
+
+ remmina_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->vnc);
+
+
+ const gchar *address = remmina_plugin_service->file_get_string(remminafile, "server");
+ if(strstr(g_strdup(address), "unix:///") != NULL) {
+ REMMINA_PLUGIN_DEBUG("address contain unix:// -> %s", address);
+ gchar *val = str_replace (address, "unix://", "");
+ REMMINA_PLUGIN_DEBUG("address after cleaning = %s", val);
+ gint fd = remmina_plugin_service->open_unix_sock(val);
+ REMMINA_PLUGIN_DEBUG("Unix socket fd: %d", fd);
+ gpdata->fd = fd;
+ g_free(val);
+
+ } else {
+ /* Setup SSH tunnel if needed */
+ tunnel = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, GVNC_DEFAULT_PORT, FALSE);
+ if (!tunnel)
+ return FALSE;
+ remmina_plugin_service->get_server_port(tunnel,
+ GVNC_DEFAULT_PORT,
+ &host,
+ &port);
+ }
+
+
+
+ gpdata->depth_profile = remmina_plugin_service->file_get_int(remminafile, "depth_profile", 24);
+ gpdata->viewonly = remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE);
+ vnc_display_set_depth(VNC_DISPLAY(gpdata->vnc), gpdata->depth_profile);
+ if (gpdata->fd > 0)
+ vnc_display_open_fd(VNC_DISPLAY(gpdata->vnc), gpdata->fd);
+ else
+ vnc_display_open_host(VNC_DISPLAY(gpdata->vnc), host, g_strdup_printf("%d", port));
+ gpdata->lossy_encoding = remmina_plugin_service->file_get_int(remminafile, "lossy_encoding", FALSE);
+ vnc_display_set_lossy_encoding(VNC_DISPLAY(gpdata->vnc), gpdata->lossy_encoding);
+ vnc_display_set_shared_flag(VNC_DISPLAY(gpdata->vnc), gpdata->shared);
+
+ if(host) g_free(host);
+ if(tunnel) g_free(tunnel);
+
+ /* TRUE Conflict with remmina? */
+ vnc_display_set_keyboard_grab(VNC_DISPLAY(gpdata->vnc), FALSE);
+ vnc_display_set_read_only(VNC_DISPLAY(gpdata->vnc), gpdata->viewonly);
+ /* TRUE Conflict with remmina? */
+ vnc_display_set_pointer_grab(VNC_DISPLAY(gpdata->vnc), FALSE);
+ vnc_display_set_pointer_local(VNC_DISPLAY(gpdata->vnc), TRUE);
+
+ vnc_display_set_force_size(VNC_DISPLAY(gpdata->vnc), FALSE);
+ vnc_display_set_scaling(VNC_DISPLAY(gpdata->vnc), TRUE);
+#ifdef HAVE_VNC_REMOTE_RESIZE
+ vnc_display_set_allow_resize(VNC_DISPLAY(gpdata->vnc), TRUE);
+ vnc_display_set_zoom_level(VNC_DISPLAY(gpdata->vnc), opt_zoom);
+#endif
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+ gtk_widget_show_all(gpdata->box);
+ return TRUE;
+}
+
+/* Array of key/value pairs for color depths */
+static gpointer colordepth_list[] =
+{
+ "0", N_("Use server settings"),
+ "1", N_("True colour (24 bits)"),
+ "2", N_("High colour (16 bits)"),
+ "3", N_("Low colour (8 bits)"),
+ "4", N_("Ultra low colour (3 bits)"),
+ NULL
+};
+
+/* Array of RemminaProtocolSetting for basic settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting tooltip
+ * g) Validation data pointer, will be passed to the validation callback method.
+ * h) Validation callback method (Can be NULL. Every entry will be valid then.)
+ * use following prototype:
+ * gboolean mysetting_validator_method(gpointer key, gpointer value,
+ * gpointer validator_data);
+ * gpointer key is a gchar* containing the setting's name,
+ * gpointer value contains the value which should be validated,
+ * gpointer validator_data contains your passed data.
+ */
+static const RemminaProtocolSetting gvnc_plugin_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("VNC password"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "depth_profile", N_("Colour depth"), FALSE, colordepth_list, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "lossy_encoding", N_("Use JPEG Compression"), TRUE, NULL, N_("This might not work on all VNC servers"), NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "gvncdebug", N_("Enable GTK-VNC debug"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL }
+};
+
+/* Array of RemminaProtocolSetting for advanced settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting Tooltip
+ */
+static const RemminaProtocolSetting gvnc_plugin_advanced_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableclipboard", N_("No clipboard sync"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableserverbell", N_("Ignore remote bell messages"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enableaudio", N_("Enable audio channel"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "viewonly", N_("View only"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "shared", N_("Shared connection"), TRUE, NULL, N_("If the server should try to share the desktop by leaving other clients connected") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, TRUE, NULL, NULL }
+};
+
+/* Array for available features.
+ * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */
+static const RemminaProtocolFeature gvnc_plugin_features[] =
+{
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, GVNC_PLUGIN_FEATURE_PREF_VIEWONLY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "viewonly", N_("View only") },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, GVNC_PLUGIN_FEATURE_PREF_DISABLECLIPBOARD, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "disableclipboard", N_("No clipboard sync") },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTDEL, N_("Send Ctrl+Alt+_Del"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTBACKSPACE, N_("Send Ctrl+Alt+_Backspace"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF1, N_("Send Ctrl+Alt+_F1"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF2, N_("Send Ctrl+Alt+_F2"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF3, N_("Send Ctrl+Alt+_F3"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF4, N_("Send Ctrl+Alt+_F4"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF5, N_("Send Ctrl+Alt+_F5"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF6, N_("Send Ctrl+Alt+_F6"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF7, N_("Send Ctrl+Alt+_F7"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF8, N_("Send Ctrl+Alt+_F8"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF9, N_("Send Ctrl+Alt+_F9"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF10, N_("Send Ctrl+Alt+_F10"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF11, N_("Send Ctrl+Alt+_F11"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SENDCTRLALTF12, N_("Send Ctrl+Alt+_F12"), NULL, NULL },
+#ifdef HAVE_VNC_POWER_CONTROL
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_REBOOT, N_("Reboot remote host"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_RESET, N_("Reset remote host (hard reboot)"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, GVNC_PLUGIN_FEATURE_TOOL_SHUTDOWN, N_("Shutdown remote host"), NULL, NULL },
+#endif
+ { REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE, GVNC_PLUGIN_FEATURE_DYNRESUPDATE, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, GVNC_PLUGIN_FEATURE_SCALE, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL }
+};
+
+/* Protocol plugin definition and features */
+static RemminaProtocolPlugin remmina_plugin = {
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ PLUGIN_NAME, // Name
+ PLUGIN_DESCRIPTION, // Description
+ GETTEXT_PACKAGE, // Translation domain
+ PLUGIN_VERSION, // Version number
+ PLUGIN_APPICON, // Icon for normal connection
+ PLUGIN_APPICON, // Icon for SSH connection
+ gvnc_plugin_basic_settings, // Array for basic settings
+ gvnc_plugin_advanced_settings, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type
+ gvnc_plugin_features, // Array for available features
+ gvnc_plugin_init, // Plugin initialization
+ gvnc_plugin_open_connection, // Plugin open connection
+ gvnc_plugin_close_connection, // Plugin close connection
+ gvnc_plugin_query_feature, // Query for available features
+ gvnc_plugin_call_feature, // Call a feature
+ gvnc_plugin_keystroke, // Send a keystroke
+ NULL, // No screenshot support available
+ //gvnc_plugin_get_screenshot, // No screenshot support available
+ NULL, // RCW map event
+ NULL // RCW unmap event
+};
+
+G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_service = service;
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+ if (!service->register_plugin((RemminaPlugin *)&remmina_plugin))
+ return FALSE;
+
+ return TRUE;
+}
diff --git a/plugins/gvnc/gvnc_plugin.h b/plugins/gvnc/gvnc_plugin.h
new file mode 100644
index 0000000..613d032
--- /dev/null
+++ b/plugins/gvnc/gvnc_plugin.h
@@ -0,0 +1,131 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "common/remmina_plugin.h"
+
+#include <gdk/gdkkeysyms.h>
+#include <vncdisplay.h>
+#include <vncutil.h>
+#include <vncaudiopulse.h>
+
+
+#ifndef GDK_Return
+#define GDK_Return GDK_KEY_Return
+#endif
+#ifndef GDK_Escape
+#define GDK_Escape GDK_KEY_Escape
+#endif
+#ifndef GDK_BackSpace
+#define GDK_BackSpace GDK_KEY_BackSpace
+#endif
+#ifndef GDK_Delete
+#define GDK_Delete GDK_KEY_Delete
+#endif
+#ifndef GDK_Control_L
+#define GDK_Control_L GDK_KEY_Control_L
+#endif
+#ifndef GDK_Alt_L
+#define GDK_Alt_L GDK_KEY_Alt_L
+#endif
+#ifndef GDK_F1
+#define GDK_F1 GDK_KEY_F1
+#endif
+#ifndef GDK_F2
+#define GDK_F2 GDK_KEY_F2
+#endif
+#ifndef GDK_F3
+#define GDK_F3 GDK_KEY_F3
+#endif
+#ifndef GDK_F4
+#define GDK_F4 GDK_KEY_F4
+#endif
+#ifndef GDK_F5
+#define GDK_F5 GDK_KEY_F5
+#endif
+#ifndef GDK_F6
+#define GDK_F6 GDK_KEY_F6
+#endif
+#ifndef GDK_F7
+#define GDK_F7 GDK_KEY_F7
+#endif
+#ifndef GDK_F8
+#define GDK_F8 GDK_KEY_F8
+#endif
+#ifndef GDK_F11
+#define GDK_F11 GDK_KEY_F11
+#endif
+
+#define GET_PLUGIN_DATA(gp) (GVncPluginData *)g_object_get_data(G_OBJECT(gp), "plugin-data")
+
+#define REMMINA_PLUGIN_INFO(fmt, ...) \
+ remmina_plugin_service->_remmina_info(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_MESSAGE(fmt, ...) \
+ remmina_plugin_service->_remmina_message(__func, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) \
+ remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_WARNING(fmt, ...) \
+ remmina_plugin_service->_remmina_warning(__func__, fmt, ##__VA_ARGS__)
+
+/* This will intentionally crash Remmina */
+#define REMMINA_PLUGIN_ERROR(fmt, ...) \
+ remmina_plugin_service->_remmina_error(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_CRITICAL(fmt, ...) \
+ remmina_plugin_service->_remmina_critical(__func__, fmt, ##__VA_ARGS__)
+#define REMMINA_PLUGIN_AUDIT(fmt, ...) \
+ remmina_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__)
+
+typedef struct _GVncPluginData {
+ GtkWidget * box;
+ GtkWidget * vnc;
+ VncConnection * conn;
+ VncAudioPulse * pa;
+ gchar * error_msg;
+ gchar * clipstr;
+ gulong signal_clipboard;
+ gint depth_profile;
+ gint shared;
+ gboolean lossy_encoding;
+ gboolean viewonly;
+ gint width;
+ gint height;
+ gint fd;
+ gchar * addr;
+} GVncPluginData;
+
+G_BEGIN_DECLS
+G_END_DECLS
diff --git a/plugins/gvnc/gvnc_plugin_config.h b/plugins/gvnc/gvnc_plugin_config.h
new file mode 100644
index 0000000..af55b3c
--- /dev/null
+++ b/plugins/gvnc/gvnc_plugin_config.h
@@ -0,0 +1,43 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#ifndef __PLUGIN_CONFIG_H
+#define __PLUGIN_CONFIG_H
+
+#define PLUGIN_NAME "GVNC"
+#define PLUGIN_DESCRIPTION N_("Remmina VNC plugin for GNOME and KVM")
+#define PLUGIN_VERSION "0.1b"
+#define PLUGIN_APPICON "org.remmina.Remmina-gvnc-symbolic"
+#endif
diff --git a/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-ssh-symbolic.svg b/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-ssh-symbolic.svg
new file mode 100644
index 0000000..6c1da59
--- /dev/null
+++ b/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-ssh-symbolic.svg
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="99.772003"
+ height="99.772079"
+ viewBox="0 0 26.39801 26.398028"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="vnc over ssh.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1.0493442"
+ inkscape:cx="-11.459557"
+ inkscape:cy="109.76653"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1366"
+ inkscape:window-height="715"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-162.7958"
+ originy="-83.119086" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-162.79579,-187.48289)">
+ <path
+ d="m 175.99485,195.20186 c -1.73487,0 -3.13161,1.42611 -3.13161,3.18029 v 1.51686 h -0.7829 c -0.43365,0 -0.7829,0.34925 -0.7829,0.7829 v 4.69715 c 0,0.43365 0.34925,0.7829 0.7829,0.7829 h 7.82875 c 0.43366,0 0.78291,-0.34925 0.78291,-0.7829 v -4.69715 c 0,-0.43365 -0.34925,-0.7829 -0.78291,-0.7829 h -0.7829 v -1.51686 c 0,-1.75365 -1.39673,-3.18029 -3.13134,-3.18029 z m 0,1.56581 c 0.8673,0 1.56554,0.69823 1.56554,1.56554 v 1.5658 h -3.13135 v -1.5658 c 0,-0.86731 0.69824,-1.56554 1.56581,-1.56554 z"
+ id="path34"
+ inkscape:connector-curvature="0"
+ style="isolation:isolate;fill:#171717;stroke-width:0.26458332" />
+ <path
+ d="m 187.30817,213.88092 c 0,0 0.14367,-0.008 0.34237,-0.0442 0.38523,-0.0569 0.67627,-0.195 0.89138,-0.38312 0.0415,-0.0317 0.082,-0.0661 0.12144,-0.10319 0.037,-0.0394 0.0712,-0.0796 0.10319,-0.12144 0.18785,-0.21484 0.32623,-0.50615 0.38285,-0.89112 0.0368,-0.1987 0.0444,-0.34263 0.0444,-0.34263 v -22.62691 c 0,-1.88542 -1.88568,-1.88542 -1.88568,-1.88542 h -22.6269 c 0,0 -1.88543,0 -1.88543,1.88542 v 22.6269 c 0,0 0.008,0.14393 0.0442,0.34264 0.0585,0.38497 0.19447,0.67627 0.38285,0.89111 0.032,0.0418 0.0675,0.082 0.10319,0.12145 0.0397,0.037 0.0802,0.0714 0.12144,0.10318 0.21511,0.18812 0.50668,0.32624 0.89138,0.38312 0.19976,0.0363 0.34238,0.0442 0.34238,0.0442 z m -1.88569,-4.5347 h -18.85553 v -18.09221 h 18.85553 z"
+ id="path36"
+ inkscape:connector-curvature="0"
+ style="isolation:isolate;fill:#171717;stroke-width:0.26458332" />
+ </g>
+</svg>
diff --git a/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-symbolic.svg b/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-symbolic.svg
new file mode 100644
index 0000000..060a62a
--- /dev/null
+++ b/plugins/gvnc/scalable/emblems/org.remmina.Remmina-gvnc-symbolic.svg
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100"
+ height="100"
+ viewBox="0 0 26.458335 26.458332"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="vnc protocol.svg">
+ <defs
+ id="defs2">
+ <mask
+ id="_mask_t2Us8opi94eYjaS69EfyEcHSNVQ03DDZ"
+ x="-200%"
+ y="-200%"
+ width="400%"
+ height="400%">
+ <rect
+ x="0"
+ y="0"
+ width="100%"
+ height="100%"
+ style="fill:#ffffff"
+ id="rect40" />
+ <path
+ d="m 850.286,202.852 c 0,0 0.452,-0.025 1.077,-0.139 1.211,-0.179 2.128,-0.614 2.804,-1.205 0.13,-0.1 0.257,-0.208 0.382,-0.324 0.116,-0.125 0.224,-0.251 0.324,-0.382 0.592,-0.677 1.026,-1.593 1.205,-2.804 0.115,-0.625 0.139,-1.077 0.139,-1.077 V 125.75 c 0,-5.931 -5.931,-5.931 -5.931,-5.931 h -71.171 c 0,0 -5.931,0 -5.931,5.931 v 71.171 c 0,0 0.026,0.452 0.139,1.077 0.184,1.211 0.612,2.127 1.205,2.804 0.101,0.131 0.212,0.257 0.324,0.382 0.125,0.116 0.252,0.224 0.383,0.324 0.676,0.591 1.593,1.026 2.803,1.205 0.628,0.114 1.077,0.139 1.077,0.139 z m -5.931,-14.263 h -59.309 v -56.908 h 59.309 z"
+ id="path42"
+ inkscape:connector-curvature="0"
+ style="fill:#000000;stroke:none" />
+ </mask>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1.4839967"
+ inkscape:cx="-65.390539"
+ inkscape:cy="-17.020138"
+ inkscape:document-units="px"
+ inkscape:current-layer="g820"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1366"
+ inkscape:window-height="715"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-146.65307"
+ originy="-135.56714" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-146.65306,-134.97453)">
+ <g
+ id="g820"
+ transform="matrix(1.0697634,0,0,1.0697634,-9.2735492,-11.925523)">
+ <path
+ inkscape:connector-curvature="0"
+ id="path38"
+ d="m 147.83075,137.94029 c 0,0 -1.17769,2e-5 -1.17769,1.17769 v 14.13263 c 0,0 0.005,0.09 0.0274,0.21405 0.0365,0.24056 0.12144,0.42255 0.23928,0.55686 0.02,0.0261 0.0421,0.051 0.0644,0.0757 0.0248,0.023 0.0503,0.0445 0.0761,0.0644 0.13431,0.11739 0.31608,0.20363 0.55643,0.23928 0.12474,0.0227 0.21405,0.0278 0.21405,0.0278 h 4.62807 v -2.83218 H 149.0084 V 140.2961 h 11.77727 v 4.05511 h 2.35536 v -5.23323 c 0,-1.17767 -1.17768,-1.17769 -1.17768,-1.17769 z m 12.95495,10.06883 v 3.58743 h -4.66941 v 2.83218 h 5.84709 c 0,0 0.09,-0.005 0.21405,-0.0278 0.24056,-0.0356 0.4221,-0.1219 0.55643,-0.23928 0.0258,-0.0199 0.0514,-0.0414 0.0761,-0.0644 0.023,-0.0247 0.0445,-0.0497 0.0644,-0.0757 0.11738,-0.13431 0.20386,-0.3163 0.23927,-0.55686 0.023,-0.12408 0.0274,-0.21405 0.0274,-0.21405 v -5.24149 z"
+ style="isolation:isolate;fill:#171717;stroke-width:0.22274615" />
+ <path
+ style="isolation:isolate;fill:#171717;stroke-width:0.22274615"
+ inkscape:connector-curvature="0"
+ id="path38-5"
+ d="m 168.41828,161.43286 c 0,0 0.0898,-0.005 0.21384,-0.0276 0.24056,-0.0356 0.42232,-0.12185 0.55664,-0.23923 0.0258,-0.0199 0.0513,-0.0414 0.0759,-0.0644 0.0229,-0.0248 0.0445,-0.0499 0.0644,-0.0759 0.11739,-0.13432 0.20381,-0.31608 0.23923,-0.55665 0.023,-0.12407 0.0276,-0.21405 0.0276,-0.21405 v -14.13259 c 0,-1.17766 -1.17767,-1.17766 -1.17767,-1.17766 h -14.13279 c 0,0 -1.17765,0 -1.17765,1.17766 v 14.13258 c 0,0 0.005,0.09 0.0276,0.21406 0.0365,0.24057 0.1214,0.42233 0.23924,0.55664 0.02,0.0261 0.0421,0.0512 0.0644,0.0759 0.0248,0.023 0.0501,0.0445 0.0759,0.0644 0.13432,0.11738 0.3163,0.20359 0.55664,0.23923 0.12474,0.0227 0.21384,0.0276 0.21384,0.0276 z m -1.17788,-2.83222 v -11.30036 h -11.77704 11.77704 z"
+ sodipodi:nodetypes="cccccccsccsccccccccccccc" />
+ </g>
+ </g>
+</svg>
diff --git a/plugins/kwallet/CMakeLists.txt b/plugins/kwallet/CMakeLists.txt
new file mode 100644
index 0000000..a448e31
--- /dev/null
+++ b/plugins/kwallet/CMakeLists.txt
@@ -0,0 +1,54 @@
+# remmina-plugin-www - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. If you
+# do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+
+
+# find_suggested_package(KF5Wallet) has already been run on main CMakeLists.txt
+
+find_suggested_package(KF5Wallet)
+if(KF5Wallet_FOUND)
+ message(STATUS "KF5Wallet library found.")
+ set(REMMINA_PLUGIN_KF5WALLET_SRCS
+ src/kwallet_plugin_main.c
+ src/kwallet_plugin.cpp
+ src/kwallet_plugin.h
+ )
+
+ add_library(remmina-plugin-kwallet MODULE ${REMMINA_PLUGIN_KF5WALLET_SRCS})
+ set_target_properties(remmina-plugin-kwallet PROPERTIES PREFIX "")
+ set_target_properties(remmina-plugin-kwallet PROPERTIES NO_SONAME 1)
+
+ include_directories(${GTK_INCLUDE_DIRS})
+ target_link_libraries(remmina-plugin-kwallet KF5::Wallet ${GLib_LIBRARY})
+
+ install(TARGETS remmina-plugin-kwallet DESTINATION ${REMMINA_PLUGINDIR})
+endif()
diff --git a/plugins/kwallet/src/kwallet_plugin.cpp b/plugins/kwallet/src/kwallet_plugin.cpp
new file mode 100644
index 0000000..aeda596
--- /dev/null
+++ b/plugins/kwallet/src/kwallet_plugin.cpp
@@ -0,0 +1,89 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL.
+ * If you modify file(s) with this exception, you may extend this exception
+ * to your version of the file(s), but you are not obligated to do so.
+ * If you do not wish to do so, delete this exception statement from your
+ * version.
+ * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+
+#include "kwallet_plugin.h"
+#include <KWallet>
+
+static KWallet::Wallet* wallet;
+
+static char folderName[] = "Remmina";
+
+int rp_kwallet_init(void)
+{
+ QString s = KWallet::Wallet::LocalWallet();
+ wallet = KWallet::Wallet::openWallet(s, 0);
+ if (!wallet) {
+ return 0;
+ }
+
+ if (!wallet->createFolder(folderName)) {
+ delete wallet;
+ return 0;
+ }
+ if (!wallet->setFolder(folderName)) {
+ delete wallet;
+ return 0;
+ }
+
+ return 1;
+}
+
+int rp_kwallet_is_service_available(void)
+{
+ return wallet != 0;
+}
+
+void rp_kwallet_store_password(const char *key, const char *password)
+{
+ wallet->writePassword(key, password);
+}
+
+char *rp_kwallet_get_password(const char *key)
+{
+ QString password;
+ if (wallet->readPassword(key, password) != 0) {
+ return NULL;
+ }
+ QByteArray pba = password.toUtf8();
+ char *cpassword = (char *)malloc(pba.size()+1);
+ strcpy(cpassword, pba.data());
+ return cpassword;
+}
+
+void rp_kwallet_delete_password(const char *key)
+{
+ wallet->removeEntry(key);
+}
diff --git a/plugins/kwallet/src/kwallet_plugin.h b/plugins/kwallet/src/kwallet_plugin.h
new file mode 100644
index 0000000..7499f8f
--- /dev/null
+++ b/plugins/kwallet/src/kwallet_plugin.h
@@ -0,0 +1,52 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL.
+ * If you modify file(s) with this exception, you may extend this exception
+ * to your version of the file(s), but you are not obligated to do so.
+ * If you do not wish to do so, delete this exception statement from your
+ * version.
+ * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include <gtk/gtk.h>
+#include "remmina/plugin.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+int rp_kwallet_init(void);
+void rp_kwallet_store_password(const char *key, const char *password);
+char *rp_kwallet_get_password(const char *key);
+void rp_kwallet_delete_password(const char *key);
+int rp_kwallet_is_service_available(void);
+
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/plugins/kwallet/src/kwallet_plugin_main.c b/plugins/kwallet/src/kwallet_plugin_main.c
new file mode 100644
index 0000000..6a6be3b
--- /dev/null
+++ b/plugins/kwallet/src/kwallet_plugin_main.c
@@ -0,0 +1,146 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL.
+ * If you modify file(s) with this exception, you may extend this exception
+ * to your version of the file(s), but you are not obligated to do so.
+ * If you do not wish to do so, delete this exception statement from your
+ * version.
+ * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "config.h"
+#include <gtk/gtk.h>
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+#include <remmina/plugin.h>
+
+#include "kwallet_plugin.h"
+
+static RemminaPluginService *remmina_plugin_service = NULL;
+
+gboolean remmina_plugin_kwallet_is_service_available(RemminaPlugin *plugin)
+{
+ return rp_kwallet_is_service_available();
+}
+
+
+static gchar *build_kwallet_key(RemminaFile *remminafile, const gchar *key)
+{
+ const gchar *path;
+ gchar *kwkey;
+ size_t kwkey_sz;
+
+ path = remmina_plugin_service->file_get_path(remminafile);
+
+ kwkey_sz = strlen(key) + 1 + strlen(path) + 1;
+ kwkey = g_malloc(kwkey_sz);
+
+ strcpy(kwkey, key);
+ strcat(kwkey, ";");
+ strcat(kwkey, path);
+
+ return kwkey;
+}
+
+void remmina_plugin_kwallet_store_password(RemminaPlugin *plugin, RemminaFile *remminafile, const gchar *key, const gchar *password)
+{
+ TRACE_CALL(__func__);
+ gchar *kwkey;
+ kwkey = build_kwallet_key(remminafile, key);
+ rp_kwallet_store_password(kwkey, password);
+ g_free(kwkey);
+}
+
+gchar*
+remmina_plugin_kwallet_get_password(RemminaPlugin *plugin, RemminaFile *remminafile, const gchar *key)
+{
+ TRACE_CALL(__func__);
+ gchar *kwkey, *password;
+
+ kwkey = build_kwallet_key(remminafile, key);
+ password = rp_kwallet_get_password(kwkey);
+ g_free(kwkey);
+
+ return password;
+}
+
+void remmina_plugin_kwallet_delete_password(RemminaPlugin *plugin, RemminaFile *remminafile, const gchar *key)
+{
+ TRACE_CALL(__func__);
+ gchar *kwkey;
+ kwkey = build_kwallet_key(remminafile, key);
+ rp_kwallet_delete_password(kwkey);
+ g_free(kwkey);
+}
+
+gboolean remmina_plugin_kwallet_init(RemminaPlugin *plugin)
+{
+ /* Activates only when KDE is running */
+ const gchar *envvar;
+
+ envvar = g_environ_getenv(g_get_environ(), "XDG_CURRENT_DESKTOP");
+ if (!envvar || strcmp(envvar, "KDE") != 0)
+ return FALSE;
+
+ return rp_kwallet_init();
+
+}
+
+static RemminaSecretPlugin remmina_plugin_kwallet =
+{ REMMINA_PLUGIN_TYPE_SECRET,
+ "kwallet",
+ N_("Secured password storage in KWallet"),
+ NULL,
+ VERSION,
+ 1000,
+ remmina_plugin_kwallet_init,
+ remmina_plugin_kwallet_is_service_available,
+ remmina_plugin_kwallet_store_password,
+ remmina_plugin_kwallet_get_password,
+ remmina_plugin_kwallet_delete_password,
+};
+
+G_MODULE_EXPORT gboolean
+remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+
+ /* This function should only register the secret plugin. No init action
+ * should be performed here. Initialization will be done later
+ * with remmina_plugin_xxx_init() . */
+
+ remmina_plugin_service = service;
+ if (!service->register_plugin((RemminaPlugin*)&remmina_plugin_kwallet)) {
+ return FALSE;
+ }
+
+ return TRUE;
+
+}
+
diff --git a/plugins/python_wrapper/CMakeLists.txt b/plugins/python_wrapper/CMakeLists.txt
new file mode 100644
index 0000000..d77eac5
--- /dev/null
+++ b/plugins/python_wrapper/CMakeLists.txt
@@ -0,0 +1,68 @@
+# Remmina - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2016-2018 Denis Ollier
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. * If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. * If you
+# do not wish to do so, delete this exception statement from your
+# version. * If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+set(REMMINA_PLUGIN_PYTHON_WRAPPER_SRCS
+ python_wrapper_common.c
+ python_wrapper_common.h
+ python_wrapper_entry.c
+ python_wrapper_entry.h
+ python_wrapper_file.c
+ python_wrapper_file.h
+ python_wrapper_plugin.c
+ python_wrapper_plugin.h
+ python_wrapper_pref.c
+ python_wrapper_pref.h
+ python_wrapper_protocol.c
+ python_wrapper_protocol.h
+ python_wrapper_protocol_widget.c
+ python_wrapper_protocol_widget.h
+ python_wrapper_remmina.c
+ python_wrapper_remmina.h
+ python_wrapper_remmina_file.c
+ python_wrapper_remmina_file.h
+ python_wrapper_secret.c
+ python_wrapper_secret.h
+ python_wrapper_tool.c
+ python_wrapper_tool.h
+ )
+
+add_library(remmina-plugin-python_wrapper MODULE ${REMMINA_PLUGIN_PYTHON_WRAPPER_SRCS})
+set_target_properties(remmina-plugin-python_wrapper PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-python_wrapper PROPERTIES NO_SONAME 1)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${PYTHON_WRAPPER_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-python_wrapper ${REMMINA_COMMON_LIBRARIES} ${PYTHON_WRAPPER_LIBRARIES})
+
+install(TARGETS remmina-plugin-python_wrapper DESTINATION ${REMMINA_PLUGINDIR})
+
+target_include_directories(remmina-plugin-python_wrapper PRIVATE ${PYTHON_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-python_wrapper ${PYTHON_LIBRARIES})
diff --git a/plugins/python_wrapper/pygobject.h b/plugins/python_wrapper/pygobject.h
new file mode 100644
index 0000000..145d1c6
--- /dev/null
+++ b/plugins/python_wrapper/pygobject.h
@@ -0,0 +1,669 @@
+/* -*- Mode: C; c-basic-offset: 4 -*- */
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#ifndef _PYGOBJECT_H_
+#define _PYGOBJECT_H_
+
+#include <Python.h>
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/* PyGClosure is a _private_ structure */
+typedef void (* PyClosureExceptionHandler) (GValue *ret, guint n_param_values, const GValue *params);
+typedef struct _PyGClosure PyGClosure;
+typedef struct _PyGObjectData PyGObjectData;
+
+struct _PyGClosure {
+ GClosure closure;
+ PyObject *callback;
+ PyObject *extra_args; /* tuple of extra args to pass to callback */
+ PyObject *swap_data; /* other object for gtk_signal_connect__object */
+ PyClosureExceptionHandler exception_handler;
+};
+
+typedef enum {
+ PYGOBJECT_USING_TOGGLE_REF = 1 << 0,
+ PYGOBJECT_IS_FLOATING_REF = 1 << 1
+} PyGObjectFlags;
+
+ /* closures is just an alias for what is found in the
+ * PyGObjectData */
+typedef struct {
+ PyObject_HEAD
+ GObject *obj;
+ PyObject *inst_dict; /* the instance dictionary -- must be last */
+ PyObject *weakreflist; /* list of weak references */
+
+ /*< private >*/
+ /* using union to preserve ABI compatibility (structure size
+ * must not change) */
+ union {
+ GSList *closures; /* stale field; no longer updated DO-NOT-USE! */
+ PyGObjectFlags flags;
+ } private_flags;
+
+} PyGObject;
+
+#define pygobject_get(v) (((PyGObject *)(v))->obj)
+#define pygobject_check(v,base) (PyObject_TypeCheck(v,base))
+
+typedef struct {
+ PyObject_HEAD
+ gpointer boxed;
+ GType gtype;
+ gboolean free_on_dealloc;
+} PyGBoxed;
+
+#define pyg_boxed_get(v,t) ((t *)((PyGBoxed *)(v))->boxed)
+#define pyg_boxed_check(v,typecode) (PyObject_TypeCheck(v, &PyGBoxed_Type) && ((PyGBoxed *)(v))->gtype == typecode)
+
+typedef struct {
+ PyObject_HEAD
+ gpointer pointer;
+ GType gtype;
+} PyGPointer;
+
+#define pyg_pointer_get(v,t) ((t *)((PyGPointer *)(v))->pointer)
+#define pyg_pointer_check(v,typecode) (PyObject_TypeCheck(v, &PyGPointer_Type) && ((PyGPointer *)(v))->gtype == typecode)
+
+typedef void (*PyGFatalExceptionFunc) (void);
+typedef void (*PyGThreadBlockFunc) (void);
+
+typedef struct {
+ PyObject_HEAD
+ GParamSpec *pspec;
+} PyGParamSpec;
+
+#define PyGParamSpec_Get(v) (((PyGParamSpec *)v)->pspec)
+#define PyGParamSpec_Check(v) (PyObject_TypeCheck(v, &PyGParamSpec_Type))
+
+typedef int (*PyGClassInitFunc) (gpointer gclass, PyTypeObject *pyclass);
+typedef PyTypeObject * (*PyGTypeRegistrationFunction) (const gchar *name,
+ gpointer data);
+
+struct _PyGObject_Functions {
+ /*
+ * All field names in here are considered private,
+ * use the macros below instead, which provides stability
+ */
+ void (* register_class)(PyObject *dict, const gchar *class_name,
+ GType gtype, PyTypeObject *type, PyObject *bases);
+ void (* register_wrapper)(PyObject *self);
+ PyTypeObject *(* lookup_class)(GType type);
+ PyObject *(* newgobj)(GObject *obj);
+
+ GClosure *(* closure_new)(PyObject *callback, PyObject *extra_args,
+ PyObject *swap_data);
+ void (* object_watch_closure)(PyObject *self, GClosure *closure);
+ GDestroyNotify destroy_notify;
+
+ GType (* type_from_object)(PyObject *obj);
+ PyObject *(* type_wrapper_new)(GType type);
+
+ gint (* enum_get_value)(GType enum_type, PyObject *obj, gint *val);
+ gint (* flags_get_value)(GType flag_type, PyObject *obj, gint *val);
+ void (* register_gtype_custom)(GType gtype,
+ PyObject *(* from_func)(const GValue *value),
+ int (* to_func)(GValue *value, PyObject *obj));
+ int (* value_from_pyobject)(GValue *value, PyObject *obj);
+ PyObject *(* value_as_pyobject)(const GValue *value, gboolean copy_boxed);
+
+ void (* register_interface)(PyObject *dict, const gchar *class_name,
+ GType gtype, PyTypeObject *type);
+
+ PyTypeObject *boxed_type;
+ void (* register_boxed)(PyObject *dict, const gchar *class_name,
+ GType boxed_type, PyTypeObject *type);
+ PyObject *(* boxed_new)(GType boxed_type, gpointer boxed,
+ gboolean copy_boxed, gboolean own_ref);
+
+ PyTypeObject *pointer_type;
+ void (* register_pointer)(PyObject *dict, const gchar *class_name,
+ GType pointer_type, PyTypeObject *type);
+ PyObject *(* pointer_new)(GType boxed_type, gpointer pointer);
+
+ void (* enum_add_constants)(PyObject *module, GType enum_type,
+ const gchar *strip_prefix);
+ void (* flags_add_constants)(PyObject *module, GType flags_type,
+ const gchar *strip_prefix);
+
+ const gchar *(* constant_strip_prefix)(const gchar *name,
+ const gchar *strip_prefix);
+
+ gboolean (* error_check)(GError **error);
+
+ /* hooks to register handlers for getting GDK threads to cooperate
+ * with python threading */
+ void (* set_thread_block_funcs) (PyGThreadBlockFunc block_threads_func,
+ PyGThreadBlockFunc unblock_threads_func);
+ PyGThreadBlockFunc block_threads;
+ PyGThreadBlockFunc unblock_threads;
+ PyTypeObject *paramspec_type;
+ PyObject *(* paramspec_new)(GParamSpec *spec);
+ GParamSpec *(*paramspec_get)(PyObject *tuple);
+ int (*pyobj_to_unichar_conv)(PyObject *pyobj, void* ptr);
+ gboolean (*parse_constructor_args)(GType obj_type,
+ char **arg_names,
+ char **prop_names,
+ GParameter *params,
+ guint *nparams,
+ PyObject **py_args);
+ PyObject *(* param_gvalue_as_pyobject) (const GValue* gvalue,
+ gboolean copy_boxed,
+ const GParamSpec* pspec);
+ int (* gvalue_from_param_pyobject) (GValue* value,
+ PyObject* py_obj,
+ const GParamSpec* pspec);
+ PyTypeObject *enum_type;
+ PyObject *(*enum_add)(PyObject *module,
+ const char *type_name_,
+ const char *strip_prefix,
+ GType gtype);
+ PyObject* (*enum_from_gtype)(GType gtype, int value);
+
+ PyTypeObject *flags_type;
+ PyObject *(*flags_add)(PyObject *module,
+ const char *type_name_,
+ const char *strip_prefix,
+ GType gtype);
+ PyObject* (*flags_from_gtype)(GType gtype, int value);
+
+ gboolean threads_enabled;
+ int (*enable_threads) (void);
+
+ int (*gil_state_ensure) (void);
+ void (*gil_state_release) (int flag);
+
+ void (*register_class_init) (GType gtype, PyGClassInitFunc class_init);
+ void (*register_interface_info) (GType gtype, const GInterfaceInfo *info);
+ void (*closure_set_exception_handler) (GClosure *closure, PyClosureExceptionHandler handler);
+
+ void (*add_warning_redirection) (const char *domain,
+ PyObject *warning);
+ void (*disable_warning_redirections) (void);
+ void (*type_register_custom)(const gchar *type_name,
+ PyGTypeRegistrationFunction callback,
+ gpointer data);
+ gboolean (*gerror_exception_check) (GError **error);
+ PyObject* (*option_group_new) (GOptionGroup *group);
+ GType (* type_from_object_strict) (PyObject *obj, gboolean strict);
+};
+
+#ifndef _INSIDE_PYGOBJECT_
+
+#if defined(NO_IMPORT) || defined(NO_IMPORT_PYGOBJECT)
+extern struct _PyGObject_Functions *_PyGObject_API;
+#else
+struct _PyGObject_Functions *_PyGObject_API;
+#endif
+
+#define pygobject_register_class (_PyGObject_API->register_class)
+#define pygobject_register_wrapper (_PyGObject_API->register_wrapper)
+#define pygobject_lookup_class (_PyGObject_API->lookup_class)
+#define pygobject_new (_PyGObject_API->newgobj)
+#define pyg_closure_new (_PyGObject_API->closure_new)
+#define pygobject_watch_closure (_PyGObject_API->object_watch_closure)
+#define pyg_closure_set_exception_handler (_PyGObject_API->closure_set_exception_handler)
+#define pyg_destroy_notify (_PyGObject_API->destroy_notify)
+#define pyg_type_from_object_strict (_PyGObject_API->type_from_object_strict)
+#define pyg_type_from_object (_PyGObject_API->type_from_object)
+#define pyg_type_wrapper_new (_PyGObject_API->type_wrapper_new)
+#define pyg_enum_get_value (_PyGObject_API->enum_get_value)
+#define pyg_flags_get_value (_PyGObject_API->flags_get_value)
+#define pyg_register_gtype_custom (_PyGObject_API->register_gtype_custom)
+#define pyg_value_from_pyobject (_PyGObject_API->value_from_pyobject)
+#define pyg_value_as_pyobject (_PyGObject_API->value_as_pyobject)
+#define pyg_register_interface (_PyGObject_API->register_interface)
+#define PyGBoxed_Type (*_PyGObject_API->boxed_type)
+#define pyg_register_boxed (_PyGObject_API->register_boxed)
+#define pyg_boxed_new (_PyGObject_API->boxed_new)
+#define PyGPointer_Type (*_PyGObject_API->pointer_type)
+#define pyg_register_pointer (_PyGObject_API->register_pointer)
+#define pyg_pointer_new (_PyGObject_API->pointer_new)
+#define pyg_enum_add_constants (_PyGObject_API->enum_add_constants)
+#define pyg_flags_add_constants (_PyGObject_API->flags_add_constants)
+#define pyg_constant_strip_prefix (_PyGObject_API->constant_strip_prefix)
+#define pyg_error_check (_PyGObject_API->error_check)
+#define pyg_set_thread_block_funcs (_PyGObject_API->set_thread_block_funcs)
+#define PyGParamSpec_Type (*_PyGObject_API->paramspec_type)
+#define pyg_param_spec_new (_PyGObject_API->paramspec_new)
+#define pyg_param_spec_from_object (_PyGObject_API->paramspec_get)
+#define pyg_pyobj_to_unichar_conv (_PyGObject_API->pyobj_to_unichar_conv)
+#define pyg_parse_constructor_args (_PyGObject_API->parse_constructor_args)
+#define pyg_param_gvalue_as_pyobject (_PyGObject_API->value_as_pyobject)
+#define pyg_param_gvalue_from_pyobject (_PyGObject_API->gvalue_from_param_pyobject)
+#define PyGEnum_Type (*_PyGObject_API->enum_type)
+#define pyg_enum_add (_PyGObject_API->enum_add)
+#define pyg_enum_from_gtype (_PyGObject_API->enum_from_gtype)
+#define PyGFlags_Type (*_PyGObject_API->flags_type)
+#define pyg_flags_add (_PyGObject_API->flags_add)
+#define pyg_flags_from_gtype (_PyGObject_API->flags_from_gtype)
+#define pyg_enable_threads (_PyGObject_API->enable_threads)
+#define pyg_gil_state_ensure (_PyGObject_API->gil_state_ensure)
+#define pyg_gil_state_release (_PyGObject_API->gil_state_release)
+#define pyg_register_class_init (_PyGObject_API->register_class_init)
+#define pyg_register_interface_info (_PyGObject_API->register_interface_info)
+#define pyg_add_warning_redirection (_PyGObject_API->add_warning_redirection)
+#define pyg_disable_warning_redirections (_PyGObject_API->disable_warning_redirections)
+#define pyg_type_register_custom_callback (_PyGObject_API->type_register_custom)
+#define pyg_gerror_exception_check (_PyGObject_API->gerror_exception_check)
+#define pyg_option_group_new (_PyGObject_API->option_group_new)
+
+#define pyg_block_threads() G_STMT_START { \
+ if (_PyGObject_API->block_threads != NULL) \
+ (* _PyGObject_API->block_threads)(); \
+ } G_STMT_END
+#define pyg_unblock_threads() G_STMT_START { \
+ if (_PyGObject_API->unblock_threads != NULL) \
+ (* _PyGObject_API->unblock_threads)(); \
+ } G_STMT_END
+
+#define pyg_threads_enabled (_PyGObject_API->threads_enabled)
+
+#define pyg_begin_allow_threads \
+ G_STMT_START { \
+ PyThreadState *_save = NULL; \
+ if (_PyGObject_API->threads_enabled) \
+ _save = PyEval_SaveThread();
+#define pyg_end_allow_threads \
+ if (_PyGObject_API->threads_enabled) \
+ PyEval_RestoreThread(_save); \
+ } G_STMT_END
+
+
+/**
+ * pygobject_init:
+ * @req_major: minimum version major number, or -1
+ * @req_minor: minimum version minor number, or -1
+ * @req_micro: minimum version micro number, or -1
+ *
+ * Imports and initializes the 'gobject' python module. Can
+ * optionally check for a required minimum version if @req_major,
+ * @req_minor, and @req_micro are all different from -1.
+ *
+ * Returns: a new reference to the gobject module on success, NULL in
+ * case of failure (and raises ImportError).
+ **/
+static inline PyObject *
+pygobject_init(int req_major, int req_minor, int req_micro)
+{
+ PyObject *gobject, *cobject;
+
+ gobject = PyImport_ImportModule("gi._gobject");
+ if (!gobject) {
+ if (PyErr_Occurred())
+ {
+ PyObject *type, *value, *traceback;
+ PyObject *py_orig_exc;
+ PyErr_Fetch(&type, &value, &traceback);
+ py_orig_exc = PyObject_Repr(value);
+ Py_XDECREF(type);
+ Py_XDECREF(value);
+ Py_XDECREF(traceback);
+
+
+#if PY_VERSION_HEX < 0x03000000
+ PyErr_Format(PyExc_ImportError,
+ "could not import gobject (error was: %s)",
+ PyString_AsString(py_orig_exc));
+#else
+ {
+ /* Can not use PyErr_Format because it doesn't have
+ * a format string for dealing with PyUnicode objects
+ * like PyUnicode_FromFormat has
+ */
+ PyObject *errmsg = PyUnicode_FromFormat("could not import gobject (error was: %U)",
+ py_orig_exc);
+
+ if (errmsg) {
+ PyErr_SetObject(PyExc_ImportError,
+ errmsg);
+ Py_DECREF(errmsg);
+ }
+ /* if errmsg is NULL then we might have OOM
+ * PyErr should already be set and trying to
+ * return our own error would be futile
+ */
+ }
+#endif
+ Py_DECREF(py_orig_exc);
+ } else {
+ PyErr_SetString(PyExc_ImportError,
+ "could not import gobject (no error given)");
+ }
+ return NULL;
+ }
+
+ cobject = PyObject_GetAttrString(gobject, "_PyGObject_API");
+#if PY_VERSION_HEX >= 0x03000000
+ if (cobject && PyCapsule_CheckExact(cobject))
+ _PyGObject_API = (struct _PyGObject_Functions *) PyCapsule_GetPointer(cobject, "gobject._PyGObject_API");
+
+#else
+ if (cobject && PyCObject_Check(cobject))
+ _PyGObject_API = (struct _PyGObject_Functions *) PyCObject_AsVoidPtr(cobject);
+#endif
+ else {
+ PyErr_SetString(PyExc_ImportError,
+ "could not import gobject (could not find _PyGObject_API object)");
+ Py_DECREF(gobject);
+ return NULL;
+ }
+
+ if (req_major != -1)
+ {
+ int found_major, found_minor, found_micro;
+ PyObject *version;
+
+ version = PyObject_GetAttrString(gobject, "pygobject_version");
+ if (!version) {
+ PyErr_SetString(PyExc_ImportError,
+ "could not import gobject (version too old)");
+ Py_DECREF(gobject);
+ return NULL;
+ }
+ if (!PyArg_ParseTuple(version, "iii",
+ &found_major, &found_minor, &found_micro)) {
+ PyErr_SetString(PyExc_ImportError,
+ "could not import gobject (version has invalid format)");
+ Py_DECREF(version);
+ Py_DECREF(gobject);
+ return NULL;
+ }
+ Py_DECREF(version);
+ if (req_major != found_major ||
+ req_minor > found_minor ||
+ (req_minor == found_minor && req_micro > found_micro)) {
+ PyErr_Format(PyExc_ImportError,
+ "could not import gobject (version mismatch, %d.%d.%d is required, "
+ "found %d.%d.%d)", req_major, req_minor, req_micro,
+ found_major, found_minor, found_micro);
+ Py_DECREF(gobject);
+ return NULL;
+ }
+ }
+ return gobject;
+}
+
+/**
+ * PYLIST_FROMGLIBLIST:
+ * @type: the type of the GLib list e.g. #GList or #GSList
+ * @prefix: the prefix of functions that manipulate a list of the type
+ * given by type.
+ *
+ * A macro that creates a type specific code block which converts a GLib
+ * list (#GSList or #GList) to a Python list. The first two args of the macro
+ * are used to specify the type and list function prefix so that the type
+ * specific macros can be generated.
+ *
+ * The rest of the args are for the standard args for the type specific
+ * macro(s) created from this macro.
+ */
+ #define PYLIST_FROMGLIBLIST(type,prefix,py_list,list,item_convert_func,\
+ list_free,list_item_free) \
+G_STMT_START \
+{ \
+ gint i, len; \
+ PyObject *item; \
+ void (*glib_list_free)(type*) = list_free; \
+ GFunc glib_list_item_free = (GFunc)list_item_free; \
+ \
+ len = prefix##_length(list); \
+ py_list = PyList_New(len); \
+ for (i = 0; i < len; i++) { \
+ gpointer list_item = prefix##_nth_data(list, i); \
+ \
+ item = item_convert_func; \
+ PyList_SetItem(py_list, i, item); \
+ } \
+ if (glib_list_item_free != NULL) \
+ prefix##_foreach(list, glib_list_item_free, NULL); \
+ if (glib_list_free != NULL) \
+ glib_list_free(list); \
+} G_STMT_END
+
+/**
+ * PYLIST_FROMGLIST:
+ * @py_list: the name of the Python list
+ *
+ * @list: the #GList to be converted to a Python list
+ *
+ * @item_convert_func: the function that converts a list item to a Python
+ * object. The function must refer to the list item using "@list_item" and
+ * must return a #PyObject* object. An example conversion function is:
+ * [[
+ * PyString_FromString(list_item)
+ * ]]
+ * A more elaborate function is:
+ * [[
+ * pyg_boxed_new(GTK_TYPE_RECENT_INFO, list_item, TRUE, TRUE)
+ * ]]
+ * @list_free: the name of a function that takes a single arg (the list) and
+ * frees its memory. Can be NULL if the list should not be freed. An example
+ * is:
+ * [[
+ * g_list_free
+ * ]]
+ * @list_item_free: the name of a #GFunc function that frees the memory used
+ * by the items in the list or %NULL if the list items do not have to be
+ * freed. A simple example is:
+ * [[
+ * g_free
+ * ]]
+ *
+ * A macro that adds code that converts a #GList to a Python list.
+ *
+ */
+#define PYLIST_FROMGLIST(py_list,list,item_convert_func,list_free,\
+ list_item_free) \
+ PYLIST_FROMGLIBLIST(GList,g_list,py_list,list,item_convert_func,\
+ list_free,list_item_free)
+
+/**
+ * PYLIST_FROMGSLIST:
+ * @py_list: the name of the Python list
+ *
+ * @list: the #GSList to be converted to a Python list
+ *
+ * @item_convert_func: the function that converts a list item to a Python
+ * object. The function must refer to the list item using "@list_item" and
+ * must return a #PyObject* object. An example conversion function is:
+ * [[
+ * PyString_FromString(list_item)
+ * ]]
+ * A more elaborate function is:
+ * [[
+ * pyg_boxed_new(GTK_TYPE_RECENT_INFO, list_item, TRUE, TRUE)
+ * ]]
+ * @list_free: the name of a function that takes a single arg (the list) and
+ * frees its memory. Can be %NULL if the list should not be freed. An example
+ * is:
+ * [[
+ * g_list_free
+ * ]]
+ * @list_item_free: the name of a #GFunc function that frees the memory used
+ * by the items in the list or %NULL if the list items do not have to be
+ * freed. A simple example is:
+ * [[
+ * g_free
+ * ]]
+ *
+ * A macro that adds code that converts a #GSList to a Python list.
+ *
+ */
+#define PYLIST_FROMGSLIST(py_list,list,item_convert_func,list_free,\
+ list_item_free) \
+ PYLIST_FROMGLIBLIST(GSList,g_slist,py_list,list,item_convert_func,\
+ list_free,list_item_free)
+
+/**
+ * PYLIST_ASGLIBLIST
+ * @type: the type of the GLib list e.g. GList or GSList
+ * @prefix: the prefix of functions that manipulate a list of the type
+ * given by type e.g. g_list or g_slist
+ *
+ * A macro that creates a type specific code block to be used to convert a
+ * Python list to a GLib list (GList or GSList). The first two args of the
+ * macro are used to specify the type and list function prefix so that the
+ * type specific macros can be generated.
+ *
+ * The rest of the args are for the standard args for the type specific
+ * macro(s) created from this macro.
+ */
+#define PYLIST_ASGLIBLIST(type,prefix,py_list,list,check_func,\
+ convert_func,child_free_func,errormsg,errorreturn) \
+G_STMT_START \
+{ \
+ Py_ssize_t i, n_list; \
+ GFunc glib_child_free_func = (GFunc)child_free_func; \
+ \
+ if (!(py_list = PySequence_Fast(py_list, ""))) { \
+ errormsg; \
+ return errorreturn; \
+ } \
+ n_list = PySequence_Fast_GET_SIZE(py_list); \
+ for (i = 0; i < n_list; i++) { \
+ PyObject *py_item = PySequence_Fast_GET_ITEM(py_list, i); \
+ \
+ if (!check_func) { \
+ if (glib_child_free_func) \
+ prefix##_foreach(list, glib_child_free_func, NULL); \
+ prefix##_free(list); \
+ Py_DECREF(py_list); \
+ errormsg; \
+ return errorreturn; \
+ } \
+ list = prefix##_prepend(list, convert_func); \
+ }; \
+ Py_DECREF(py_list); \
+ list = prefix##_reverse(list); \
+} \
+G_STMT_END
+/**
+ * PYLIST_ASGLIST
+ * @py_list: the Python list to be converted
+ * @list: the #GList list to be converted
+ * @check_func: the expression that takes a #PyObject* arg (must be named
+ * @py_item) and returns an int value indicating if the Python object matches
+ * the required list item type (0 - %False or 1 - %True). An example is:
+ * [[
+ * (PyString_Check(py_item)||PyUnicode_Check(py_item))
+ * ]]
+ * @convert_func: the function that takes a #PyObject* arg (must be named
+ * py_item) and returns a pointer to the converted list object. An example
+ * is:
+ * [[
+ * pygobject_get(py_item)
+ * ]]
+ * @child_free_func: the name of a #GFunc function that frees a GLib list
+ * item or %NULL if the list item does not have to be freed. This function is
+ * used to help free the items in a partially created list if there is an
+ * error. An example is:
+ * [[
+ * g_free
+ * ]]
+ * @errormsg: a function that sets up a Python error message. An example is:
+ * [[
+ * PyErr_SetString(PyExc_TypeError, "strings must be a sequence of" "strings
+ * or unicode objects")
+ * ]]
+ * @errorreturn: the value to return if an error occurs, e.g.:
+ * [[
+ * %NULL
+ * ]]
+ *
+ * A macro that creates code that converts a Python list to a #GList. The
+ * returned list must be freed using the appropriate list free function when
+ * it's no longer needed. If an error occurs the child_free_func is used to
+ * release the memory used by the list items and then the list memory is
+ * freed.
+ */
+#define PYLIST_ASGLIST(py_list,list,check_func,convert_func,child_free_func,\
+ errormsg,errorreturn) \
+ PYLIST_ASGLIBLIST(GList,g_list,py_list,list,check_func,convert_func,\
+ child_free_func,errormsg,errorreturn)
+
+/**
+ * PYLIST_ASGSLIST
+ * @py_list: the Python list to be converted
+ * @list: the #GSList list to be converted
+ * @check_func: the expression that takes a #PyObject* arg (must be named
+ * @py_item) and returns an int value indicating if the Python object matches
+ * the required list item type (0 - %False or 1 - %True). An example is:
+ * [[
+ * (PyString_Check(py_item)||PyUnicode_Check(py_item))
+ * ]]
+ * @convert_func: the function that takes a #PyObject* arg (must be named
+ * py_item) and returns a pointer to the converted list object. An example
+ * is:
+ * [[
+ * pygobject_get(py_item)
+ * ]]
+ * @child_free_func: the name of a #GFunc function that frees a GLib list
+ * item or %NULL if the list item does not have to be freed. This function is
+ * used to help free the items in a partially created list if there is an
+ * error. An example is:
+ * [[
+ * g_free
+ * ]]
+ * @errormsg: a function that sets up a Python error message. An example is:
+ * [[
+ * PyErr_SetString(PyExc_TypeError, "strings must be a sequence of" "strings
+ * or unicode objects")
+ * ]]
+ * @errorreturn: the value to return if an error occurs, e.g.:
+ * [[
+ * %NULL
+ * ]]
+ *
+ * A macro that creates code that converts a Python list to a #GSList. The
+ * returned list must be freed using the appropriate list free function when
+ * it's no longer needed. If an error occurs the child_free_func is used to
+ * release the memory used by the list items and then the list memory is
+ * freed.
+ */
+#define PYLIST_ASGSLIST(py_list,list,check_func,convert_func,child_free_func,\
+ errormsg,errorreturn) \
+ PYLIST_ASGLIBLIST(GSList,g_slist,py_list,list,check_func,convert_func,\
+ child_free_func,errormsg,errorreturn)
+
+#endif /* !_INSIDE_PYGOBJECT_ */
+
+G_END_DECLS
+
+#endif /* !_PYGOBJECT_H_ */
diff --git a/plugins/python_wrapper/python_wrapper_common.c b/plugins/python_wrapper/python_wrapper_common.c
new file mode 100644
index 0000000..e6a2d05
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_common.c
@@ -0,0 +1,327 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_common.h"
+
+#include <assert.h>
+#include <stdio.h>
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#include "pygobject.h"
+#pragma GCC diagnostic pop
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// D E C L A R A T I O N S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * A cache to store the last result that has been returned by the Python code using CallPythonMethod
+ * (@see python_wrapper_common.h)
+ */
+static PyObject* __last_result;
+static GPtrArray* plugin_map = NULL;
+
+static RemminaPluginService* remmina_plugin_service;
+
+const char* ATTR_NAME = "name";
+const char* ATTR_ICON_NAME = "icon_name";
+const char* ATTR_DESCRIPTION = "description";
+const char* ATTR_VERSION = "version";
+const char* ATTR_ICON_NAME_SSH = "icon_name_ssh";
+const char* ATTR_FEATURES = "features";
+const char* ATTR_BASIC_SETTINGS = "basic_settings";
+const char* ATTR_ADVANCED_SETTINGS = "advanced_settings";
+const char* ATTR_SSH_SETTING = "ssh_setting";
+const char* ATTR_EXPORT_HINTS = "export_hints";
+const char* ATTR_PREF_LABEL = "pref_label";
+const char* ATTR_INIT_ORDER = "init_order";
+
+/**
+ * To prevent some memory related attacks or accidental allocation of an excessive amount of byes, this limit should
+ * always be used to check for a sane amount of bytes to allocate.
+ */
+static const int REASONABLE_LIMIT_FOR_MALLOC = 1024 * 1024;
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+PyObject* python_wrapper_last_result(void)
+{
+ TRACE_CALL(__func__);
+
+ return __last_result;
+}
+
+PyObject* python_wrapper_last_result_set(PyObject* last_result)
+{
+ TRACE_CALL(__func__);
+
+ return __last_result = last_result;
+}
+
+gboolean python_wrapper_check_error(void)
+{
+ TRACE_CALL(__func__);
+
+ if (PyErr_Occurred())
+ {
+ PyErr_Print();
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+void python_wrapper_log_method_call(PyObject* instance, const char* method)
+{
+ TRACE_CALL(__func__);
+
+ assert(instance);
+ assert(method);
+ g_print("Python@%ld: %s.%s(...) -> %s\n",
+ PyObject_Hash(instance),
+ instance->ob_type->tp_name,
+ method,
+ PyUnicode_AsUTF8(PyObject_Str(python_wrapper_last_result())));
+}
+
+long python_wrapper_get_attribute_long(PyObject* instance, const char* attr_name, long def)
+{
+ TRACE_CALL(__func__);
+
+ assert(instance);
+ assert(attr_name);
+ PyObject* attr = PyObject_GetAttrString(instance, attr_name);
+ if (attr && PyLong_Check(attr))
+ {
+ return PyLong_AsLong(attr);
+ }
+
+ return def;
+}
+
+gboolean python_wrapper_check_attribute(PyObject* instance, const char* attr_name)
+{
+ TRACE_CALL(__func__);
+
+ assert(instance);
+ assert(attr_name);
+ if (PyObject_HasAttrString(instance, attr_name))
+ {
+ return TRUE;
+ }
+
+ g_printerr("Python plugin instance is missing member: %s\n", attr_name);
+ return FALSE;
+}
+
+void* python_wrapper_malloc(int bytes)
+{
+ TRACE_CALL(__func__);
+
+ assert(bytes > 0);
+ assert(bytes <= REASONABLE_LIMIT_FOR_MALLOC);
+
+ void* result = malloc(bytes);
+
+ if (!result)
+ {
+ g_printerr("Unable to allocate %d bytes in memory!\n", bytes);
+ perror("malloc");
+ }
+
+ return result;
+}
+
+char* python_wrapper_copy_string_from_python(PyObject* string, Py_ssize_t len)
+{
+ TRACE_CALL(__func__);
+
+ char* result = NULL;
+ if (len <= 0 || string == NULL)
+ {
+ return NULL;
+ }
+
+ const char* py_str = PyUnicode_AsUTF8(string);
+ if (py_str)
+ {
+ const int label_size = sizeof(char) * (len + 1);
+ result = (char*)python_wrapper_malloc(label_size);
+ result[len] = '\0';
+ memcpy(result, py_str, len);
+ }
+
+ return result;
+}
+
+void python_wrapper_set_service(RemminaPluginService* service)
+{
+ remmina_plugin_service = service;
+}
+
+RemminaPluginService* python_wrapper_get_service(void)
+{
+ return remmina_plugin_service;
+}
+
+void python_wrapper_add_plugin(PyPlugin* plugin)
+{
+ TRACE_CALL(__func__);
+
+ if (!plugin_map)
+ {
+ plugin_map = g_ptr_array_new();
+ }
+
+ PyPlugin* test = python_wrapper_get_plugin(plugin->generic_plugin->name);
+ if (test)
+ {
+ g_printerr("A plugin named '%s' has already been registered! Skipping...", plugin->generic_plugin->name);
+ }
+ else
+ {
+ g_ptr_array_add(plugin_map, plugin);
+ }
+}
+
+RemminaTypeHint python_wrapper_to_generic(PyObject* field, gpointer* target)
+{
+ TRACE_CALL(__func__);
+
+ if (PyUnicode_Check(field))
+ {
+ Py_ssize_t len = PyUnicode_GetLength(field);
+
+ if (len > 0)
+ {
+ *target = python_wrapper_copy_string_from_python(field, len);
+ }
+ else
+ {
+ *target = "";
+ }
+
+ return REMMINA_TYPEHINT_STRING;
+ }
+ else if (PyBool_Check(field))
+ {
+ *target = python_wrapper_malloc(sizeof(long));
+ long* long_target = (long*)target;
+ *long_target = PyLong_AsLong(field);
+ return REMMINA_TYPEHINT_BOOLEAN;
+ }
+ else if (PyLong_Check(field))
+ {
+ *target = python_wrapper_malloc(sizeof(long));
+ long* long_target = (long*)target;
+ *long_target = PyLong_AsLong(field);
+ return REMMINA_TYPEHINT_SIGNED;
+ }
+ else if (PyTuple_Check(field))
+ {
+ Py_ssize_t len = PyTuple_Size(field);
+ if (len)
+ {
+ gpointer* dest = (gpointer*)python_wrapper_malloc(sizeof(gpointer) * (len + 1));
+ memset(dest, 0, sizeof(gpointer) * (len + 1));
+
+ for (Py_ssize_t i = 0; i < len; ++i)
+ {
+ PyObject* item = PyTuple_GetItem(field, i);
+ python_wrapper_to_generic(item, dest + i);
+ }
+
+ *target = dest;
+ }
+ return REMMINA_TYPEHINT_TUPLE;
+ }
+
+ *target = NULL;
+ return REMMINA_TYPEHINT_UNDEFINED;
+}
+
+PyPlugin* python_wrapper_get_plugin(const gchar* name)
+{
+ TRACE_CALL(__func__);
+
+ assert(plugin_map);
+ assert(name);
+
+ for (gint i = 0; i < plugin_map->len; ++i)
+ {
+ PyPlugin* plugin = (PyPlugin*)g_ptr_array_index(plugin_map, i);
+ if (plugin->generic_plugin && plugin->generic_plugin->name && g_str_equal(name, plugin->generic_plugin->name))
+ {
+ return plugin;
+ }
+ }
+
+ return NULL;
+}
+
+PyPlugin* python_wrapper_get_plugin_by_protocol_widget(RemminaProtocolWidget* gp)
+{
+ TRACE_CALL(__func__);
+
+ assert(plugin_map);
+ assert(gp);
+
+ const gchar* name = python_wrapper_get_service()->protocol_widget_get_name(gp);
+ if (!name) {
+ return NULL;
+ }
+
+ return python_wrapper_get_plugin(name);
+}
+
+void init_pygobject()
+{
+ pygobject_init(-1, -1, -1);
+}
+
+GtkWidget* new_pywidget(GObject* obj)
+{
+ return (GtkWidget*)pygobject_new(obj);
+}
+
+GtkWidget* get_pywidget(PyObject* obj)
+{
+ return (GtkWidget*)pygobject_get(obj);
+}
diff --git a/plugins/python_wrapper/python_wrapper_common.h b/plugins/python_wrapper/python_wrapper_common.h
new file mode 100644
index 0000000..8c7a167
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_common.h
@@ -0,0 +1,295 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_common.h
+ *
+ * @brief Contains functions and constants that are commonly used throughout the Python plugin implementation.
+ *
+ * @details These functions should not be used outside of the Python plugin implementation, since everything is intended
+ * to be used with the Python engine.
+ */
+
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "common/remmina_plugin.h"
+
+#include <gtk/gtk.h>
+
+#include <Python.h>
+#include <glib.h>
+#include <Python.h>
+#include <structmember.h>
+
+#include "remmina/plugin.h"
+#include "config.h"
+
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// D E C L A R A T I O N S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+G_BEGIN_DECLS
+
+// - Attribute names
+
+extern const char* ATTR_NAME;
+extern const char* ATTR_ICON_NAME;
+extern const char* ATTR_DESCRIPTION;
+extern const char* ATTR_VERSION;
+extern const char* ATTR_ICON_NAME_SSH;
+extern const char* ATTR_FEATURES;
+extern const char* ATTR_BASIC_SETTINGS;
+extern const char* ATTR_ADVANCED_SETTINGS;
+extern const char* ATTR_SSH_SETTING;
+extern const char* ATTR_EXPORT_HINTS;
+extern const char* ATTR_PREF_LABEL;
+extern const char* ATTR_INIT_ORDER;
+
+// You can enable this for debuggin purposes or specify it in the build.
+// #define WITH_PYTHON_TRACE_CALLS
+
+/**
+ * If WITH_PYTHON_TRACE_CALLS is defined, it logs the calls to the Python code and errors in case.
+ */
+#ifdef WITH_PYTHON_TRACE_CALLS
+#define CallPythonMethod(instance, name, params, ...) \
+ python_wrapper_last_result_set(PyObject_CallMethod(instance, name, params, ##__VA_ARGS__)); \
+ python_wrapper_log_method_call(instance, name); \
+ python_wrapper_check_error()
+#else
+/**
+ * If WITH_TRACE_CALL is not defined, it still logs errors but doesn't print the call anymore.
+ */
+#define CallPythonMethod(instance, name, params, ...) \
+ PyObject_CallMethod(instance, name, params, ##__VA_ARGS__); \
+ python_wrapper_check_error()
+#endif // WITH_PYTHON_TRACE_CALLS
+
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// T Y P E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * @brief The Python abstraction of the protocol widget struct.
+ *
+ * @details This struct is responsible to provide the same accessibility to the protocol widget for Python as for
+ * native plugins.
+ */
+typedef struct
+{
+ PyObject_HEAD
+ RemminaProtocolWidget* gp;
+} PyRemminaProtocolWidget;
+
+/**
+ * @brief Maps an instance of a Python plugin to a Remmina one.
+ *
+ * @details This is used to map a Python plugin instance to the Remmina plugin one. Also instance specific data as the
+ * protocol widget are stored in this struct.
+ */
+typedef struct
+{
+ RemminaProtocolPlugin* protocol_plugin;
+ RemminaFilePlugin* file_plugin;
+ RemminaSecretPlugin* secret_plugin;
+ RemminaToolPlugin* tool_plugin;
+ RemminaEntryPlugin* entry_plugin;
+ RemminaPrefPlugin* pref_plugin;
+ RemminaPlugin* generic_plugin;
+ PyRemminaProtocolWidget* gp;
+ PyObject* instance;
+} PyPlugin;
+
+/**
+ * A struct used to communicate data between Python and C without strict data type.
+ */
+typedef struct
+{
+ PyObject_HEAD;
+ RemminaTypeHint type_hint;
+ gpointer raw;
+} PyGeneric;
+
+/**
+ * Checks if self is set and returns NULL if not.
+ */
+#define SELF_CHECK() if (!self) { \
+ g_printerr("[%s:%d]: self is null!\n", __FILE__, __LINE__); \
+ PyErr_SetString(PyExc_RuntimeError, "Method is not called from an instance (self is null)!"); \
+ return NULL; \
+ }
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Creates a new instance of PyGeneric.
+ */
+PyGeneric* python_wrapper_generic_new(void);
+
+/**
+ * Registers the given plugin if no other plugin with the same name has been already registered.
+ */
+void python_wrapper_add_plugin(PyPlugin* plugin);
+
+/**
+ * Sets the pointer to the plugin service of Remmina.
+ */
+void python_wrapper_set_service(RemminaPluginService* service);
+
+/**
+ * Gets the pointer to the plugin service of Remmina.
+ */
+RemminaPluginService* python_wrapper_get_service(void);
+
+/**
+ * Extracts data from a PyObject instance to a generic pointer and returns a type hint if it could be determined.
+ */
+RemminaTypeHint python_wrapper_to_generic(PyObject* field, gpointer* target);
+
+/**
+ * Gets the result of the last python method call.
+ */
+PyObject* python_wrapper_last_result(void);
+
+/**
+ * @brief Sets the result of the last python method call.
+ *
+ * @return Returns the passed result (it's done to be compatible with the CallPythonMethod macro).
+ */
+PyObject* python_wrapper_last_result_set(PyObject* result);
+
+/**
+ * @brief Prints a log message to inform the user a python message has been called.
+ *
+ * @detail This method is called from the CALL_PYTHON macro if WITH_PYTHON_TRACE_CALLS is defined.
+ *
+ * @param instance The instance that contains the called method.
+ * @param method The name of the method called.
+ */
+void python_wrapper_log_method_call(PyObject* instance, const char* method);
+
+/**
+ * @brief Checks if an error has occurred and prints it.
+ *
+ * @return Returns TRUE if an error has occurred.
+ */
+gboolean python_wrapper_check_error(void);
+
+/**
+ * @brief Gets the attribute as long value.
+ *
+ * @param instance The instance of the object to get the attribute.
+ * @param constant_name The name of the attribute to get.
+ * @param def The value to return if the attribute doesn't exist or is not set.
+ *
+ * @return The value attribute as long.
+ */
+long python_wrapper_get_attribute_long(PyObject* instance, const char* attr_name, long def);
+
+/**
+ * @brief Checks if a given attribute exists.
+ *
+ * @param instance The object to check for the attribute.
+ * @param attr_name The name of the attribute to check.
+ *
+ * @return Returns TRUE if the attribute exists.
+ */
+gboolean python_wrapper_check_attribute(PyObject* instance, const char* attr_name);
+
+/**
+ * @brief Allocates memory and checks for errors before returning.
+ *
+ * @param bytes Amount of bytes to allocate.
+ *
+ * @return Address to the allocated memory.
+ */
+void* python_wrapper_malloc(int bytes);
+
+/**
+ * @biref Copies a string from a Python object to a new point in memory.
+ *
+ * @param string The python object, containing the string to copy.
+ * @param len The length of the string to copy.
+ *
+ * @return A char pointer to the new copy of the string.
+ */
+char* python_wrapper_copy_string_from_python(PyObject* string, Py_ssize_t len);
+
+/**
+ * @brief Tries to find the Python plugin matching to the given instance of RemminaPlugin.
+ *
+ * @param plugin_map An array of PyPlugin pointers to search.
+ * @param instance The RemminaPlugin instance to find the correct PyPlugin instance for.
+ *
+ * @return A pointer to a PyPlugin instance if successful. Otherwise NULL is returned.
+ */
+PyPlugin* python_wrapper_get_plugin(const gchar* name);
+
+/**
+ * @brief Tries to find the Python plugin matching to the given instance of RemminaPlugin.
+ *
+ * @param plugin_map An array of PyPlugin pointers to search.
+ * @param instance The RemminaPlugin instance to find the correct PyPlugin instance for.
+ *
+ * @return A pointer to a PyPlugin instance if successful. Otherwise NULL is returned.
+ */
+PyPlugin* python_wrapper_get_plugin_by_protocol_widget(RemminaProtocolWidget* gp);
+
+/**
+ * Creates a new GtkWidget
+ * @param obj
+ * @return
+ */
+GtkWidget* new_pywidget(GObject* obj);
+
+/**
+ * Extracts a GtkWidget from a PyObject instance.
+ * @param obj
+ * @return
+ */
+GtkWidget* get_pywidget(PyObject* obj);
+
+/**
+ * Initializes the pygobject library. This needs to be called before any Python plugin is being initialized.
+ */
+void init_pygobject(void);
+
+G_END_DECLS
diff --git a/plugins/python_wrapper/python_wrapper_entry.c b/plugins/python_wrapper/python_wrapper_entry.c
new file mode 100644
index 0000000..8865863
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_entry.c
@@ -0,0 +1,100 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_entry.c
+ * @brief Contains the wiring of a Python pluing based on RemminaPluginProtocol.
+ * @author Mathias Winterhalter
+ * @date 07.04.2021
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_entry.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// D E C L A R A T I O N S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+void python_wrapper_entry_init(void)
+{
+ TRACE_CALL(__func__);
+}
+
+void python_wrapper_entry_entry_func_wrapper(RemminaEntryPlugin* instance)
+{
+ TRACE_CALL(__func__);
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+ if (plugin)
+ {
+ CallPythonMethod(plugin->instance, "entry_func", NULL);
+ }
+}
+
+RemminaPlugin* python_wrapper_create_entry_plugin(PyPlugin* plugin)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* instance = plugin->instance;
+
+ if (!python_wrapper_check_attribute(instance, ATTR_NAME)
+ || !python_wrapper_check_attribute(instance, ATTR_VERSION)
+ || !python_wrapper_check_attribute(instance, ATTR_DESCRIPTION))
+ {
+ g_printerr("Unable to create entry plugin. Aborting!\n");
+ return NULL;
+ }
+
+ RemminaEntryPlugin* remmina_plugin = (RemminaEntryPlugin*)python_wrapper_malloc(sizeof(RemminaEntryPlugin));
+
+ remmina_plugin->type = REMMINA_PLUGIN_TYPE_ENTRY;
+ remmina_plugin->domain = GETTEXT_PACKAGE;
+ remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME));
+ remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION));
+ remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION));
+ remmina_plugin->entry_func = python_wrapper_entry_entry_func_wrapper;
+
+ plugin->entry_plugin = remmina_plugin;
+ plugin->generic_plugin = (RemminaPlugin*)remmina_plugin;
+
+ python_wrapper_add_plugin(plugin);
+
+ return (RemminaPlugin*)remmina_plugin;
+}
diff --git a/plugins/python_wrapper/python_wrapper_entry.h b/plugins/python_wrapper/python_wrapper_entry.h
new file mode 100644
index 0000000..396f7be
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_entry.h
@@ -0,0 +1,63 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_entry.h
+ *
+ * @brief Contains the specialisation of RemminaPluginEntry plugins in Python.
+ */
+
+#pragma once
+
+#include "python_wrapper_common.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+G_BEGIN_DECLS
+
+/**
+ * Initializes the Python plugin specialisation for entry plugins.
+ */
+void python_wrapper_entry_init(void);
+
+/**
+ * @brief Creates a new instance of the RemminaPluginEntry, initializes its members and references the wrapper
+ * functions.
+ * @param instance The instance of the Python plugin.
+ * @return Returns a new instance of the RemminaPlugin (must be freed!).
+ */
+RemminaPlugin* python_wrapper_create_entry_plugin(PyPlugin* instance);
+
+G_END_DECLS
diff --git a/plugins/python_wrapper/python_wrapper_file.c b/plugins/python_wrapper/python_wrapper_file.c
new file mode 100644
index 0000000..e69c9b0
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_file.c
@@ -0,0 +1,160 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_common.h"
+#include "python_wrapper_file.h"
+#include "python_wrapper_remmina_file.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// D E C L A R A T I O N S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+void python_wrapper_file_init(void)
+{
+ TRACE_CALL(__func__);
+}
+
+gboolean python_wrapper_file_import_test_func_wrapper(RemminaFilePlugin* instance, const gchar* from_file)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* result = NULL;
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+
+ if (plugin)
+ {
+ result = CallPythonMethod(plugin->instance, "import_test_func", "s", from_file);
+ }
+
+ return result == Py_None || result != Py_False;
+}
+
+RemminaFile* python_wrapper_file_import_func_wrapper(RemminaFilePlugin* instance, const gchar* from_file)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* result = NULL;
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+ if (!plugin)
+ {
+ return NULL;
+ }
+
+ result = CallPythonMethod(plugin->instance, "import_func", "s", from_file);
+
+ if (result == Py_None || result == Py_False)
+ {
+ return NULL;
+ }
+
+ return ((PyRemminaFile*)result)->file;
+}
+
+gboolean python_wrapper_file_export_test_func_wrapper(RemminaFilePlugin* instance, RemminaFile* file)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* result = NULL;
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+ if (plugin)
+ {
+ result = CallPythonMethod(plugin->instance,
+ "export_test_func",
+ "O",
+ python_wrapper_remmina_file_to_python(file));
+ }
+
+ return result == Py_None || result != Py_False;
+}
+
+gboolean
+python_wrapper_file_export_func_wrapper(RemminaFilePlugin* instance, RemminaFile* file, const gchar* to_file)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* result = NULL;
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+ if (plugin)
+ {
+ result = CallPythonMethod(plugin->instance, "export_func", "s", to_file);
+ }
+
+ return result == Py_None || result != Py_False;
+}
+
+RemminaPlugin* python_wrapper_create_file_plugin(PyPlugin* plugin)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* instance = plugin->instance;
+ Py_IncRef(instance);
+
+ if (!python_wrapper_check_attribute(instance, ATTR_NAME))
+ {
+ g_printerr("Unable to create file plugin. Aborting!\n");
+ return NULL;
+ }
+
+ RemminaFilePlugin* remmina_plugin = (RemminaFilePlugin*)python_wrapper_malloc(sizeof(RemminaFilePlugin));
+
+ remmina_plugin->type = REMMINA_PLUGIN_TYPE_FILE;
+ remmina_plugin->domain = GETTEXT_PACKAGE;
+ remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME));
+ remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION));
+ remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION));
+ remmina_plugin->export_hints = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_EXPORT_HINTS));
+
+ remmina_plugin->import_test_func = python_wrapper_file_import_test_func_wrapper;
+ remmina_plugin->import_func = python_wrapper_file_import_func_wrapper;
+ remmina_plugin->export_test_func = python_wrapper_file_export_test_func_wrapper;
+ remmina_plugin->export_func = python_wrapper_file_export_func_wrapper;
+
+ plugin->file_plugin = remmina_plugin;
+ plugin->generic_plugin = (RemminaPlugin*)remmina_plugin;
+
+ python_wrapper_add_plugin(plugin);
+
+ return (RemminaPlugin*)remmina_plugin;
+}
diff --git a/plugins/python_wrapper/python_wrapper_file.h b/plugins/python_wrapper/python_wrapper_file.h
new file mode 100644
index 0000000..035712e
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_file.h
@@ -0,0 +1,63 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_file.h
+ *
+ * @brief Contains the specialisation of RemminaPluginFile plugins in Python.
+ */
+
+#pragma once
+
+#include "python_wrapper_common.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+G_BEGIN_DECLS
+
+/**
+ * Initializes the Python plugin specialisation for file plugins.
+ */
+void python_wrapper_file_init(void);
+
+/**
+ * @brief Creates a new instance of the RemminaPluginFile, initializes its members and references the wrapper
+ * functions.
+ * @param instance The instance of the Python plugin.
+ * @return Returns a new instance of the RemminaPlugin (must be freed!).
+ */
+RemminaPlugin* python_wrapper_create_file_plugin(PyPlugin* instance);
+
+G_END_DECLS
diff --git a/plugins/python_wrapper/python_wrapper_plugin.c b/plugins/python_wrapper/python_wrapper_plugin.c
new file mode 100644
index 0000000..bd57ffc
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_plugin.c
@@ -0,0 +1,220 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo
+ * Copyright (C) 2023-2024 Hiroyuki Tanaka, Sunil Bhat
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include <gtk/gtk.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#elif defined(GDK_WINDOWING_WAYLAND)
+#include <gdk/gdkwayland.h>
+#endif
+#include "config.h"
+#include "remmina/plugin.h"
+#include "remmina/remmina_trace_calls.h"
+#include "python_wrapper_common.h"
+#include "python_wrapper_plugin.h"
+#include "python_wrapper_remmina.h"
+#include "python_wrapper_protocol_widget.h"
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// D E C L A R A T I O N S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * An null terminated array of commands that are executed after the initialization of the Python engine. Every entry
+ * represents a line of Python code.
+ */
+
+static const gchar* python_wrapper_supported_extensions[] = {"py", NULL};
+
+static RemminaLanguageWrapperPlugin remmina_python_wrapper =
+{
+ REMMINA_PLUGIN_TYPE_LANGUAGE_WRAPPER, // Type
+ "Python Wrapper", // Name
+ "Enables Python plugins for Remmina", // Description
+ GETTEXT_PACKAGE, // Translation domain
+ "Python Wrapper for Remmina v0.1", // Version number
+ python_wrapper_supported_extensions, // Supported extentions
+ python_wrapper_init, // Plugin initialization
+ python_wrapper_load, // Plugin load Python file
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// U T I L S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * @brief Extracts the filename without extension from a path.
+ *
+ * @param in The string to extract the filename from
+ * @param out The resulting filename without extension (must point to allocated memory).
+ *
+ * @return The length of the filename extracted.
+ */
+static int basename_no_ext(const char* in, char** out)
+{
+ TRACE_CALL(__func__);
+
+ assert(in);
+ assert(out);
+
+ const char* base = strrchr(in, '/');
+ if (base)
+ {
+ base++;
+ } else {
+ return 0;
+ }
+
+ const char* base_end = strrchr(base, '.');
+ if (!base_end)
+ {
+ base_end = base + strlen(base);
+ }
+
+ const int length = base_end - base;
+ const int memsize = sizeof(char*) * ((length) + 1);
+
+ *out = (char*)python_wrapper_malloc(memsize);
+
+ memset(*out, 0, memsize);
+ strncpy(*out, base, length);
+ (*out)[length] = '\0';
+
+ return length;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+gboolean python_wrapper_init(RemminaLanguageWrapperPlugin* plugin)
+{
+ TRACE_CALL(__func__);
+ assert(plugin);
+ return TRUE;
+}
+
+gboolean python_wrapper_load(RemminaLanguageWrapperPlugin* plugin, const char* name)
+{
+ TRACE_CALL(__func__);
+
+ assert(plugin);
+ assert(name);
+
+
+ char* filename = NULL;
+ if (basename_no_ext(name, &filename) == 0)
+ {
+ g_printerr("[%s:%d]: Can not extract filename from '%s'!\n", __FILE__, __LINE__, name);
+ return FALSE;
+ }
+
+ PyObject* plugin_name = PyUnicode_DecodeFSDefault(filename);
+
+ if (!plugin_name)
+ {
+ free(filename);
+ g_printerr("[%s:%d]: Error converting plugin filename to PyUnicode!\n", __FILE__, __LINE__);
+ return FALSE;
+ }
+
+ wchar_t* program_name = NULL;
+ Py_ssize_t len = PyUnicode_AsWideChar(plugin_name, program_name, 0);
+ if (len <= 0)
+ {
+ free(filename);
+ g_printerr("[%s:%d]: Failed allocating %lu bytes!\n", __FILE__, __LINE__, (sizeof(wchar_t) * len));
+ return FALSE;
+ }
+
+ program_name = (wchar_t*)python_wrapper_malloc(sizeof(wchar_t) * len);
+ if (!program_name)
+ {
+ free(filename);
+ g_printerr("[%s:%d]: Failed allocating %lu bytes!\n", __FILE__, __LINE__, (sizeof(wchar_t) * len));
+ return FALSE;
+ }
+
+ PyUnicode_AsWideChar(plugin_name, program_name, len);
+
+ PySys_SetArgv(1, &program_name);
+
+ if (PyImport_Import(plugin_name))
+ {
+ free(filename);
+ return TRUE;
+ }
+
+ g_print("[%s:%d]: Failed to load python plugin file: '%s'\n", __FILE__, __LINE__, name);
+ PyErr_Print();
+ free(filename);
+
+ return FALSE;
+}
+
+
+G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+ python_wrapper_set_service(service);
+
+ python_wrapper_module_init();
+ Py_InitializeEx(0);
+
+ gchar* plugin_dir;
+ plugin_dir = g_build_path("/", g_get_user_config_dir(), "remmina", "plugins", NULL);
+
+ gchar* python_command = g_strdup_printf("sys.path.append('%s')", plugin_dir);
+
+ char* python_init_commands[] = { "import sys", python_command, "sys.path.append('" REMMINA_RUNTIME_PLUGINDIR "')", NULL }; // Sentinel};
+
+ for (char** ptr = python_init_commands; *ptr; ++ptr)
+ {
+ PyRun_SimpleString(*ptr);
+ }
+
+ g_free(python_command);
+ g_free(plugin_dir);
+
+ python_wrapper_protocol_widget_init();
+
+ service->register_plugin((RemminaPlugin*)&remmina_python_wrapper);
+
+ return TRUE;
+}
diff --git a/plugins/python_wrapper/python_wrapper_plugin.h b/plugins/python_wrapper/python_wrapper_plugin.h
new file mode 100644
index 0000000..5678088
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_plugin.h
@@ -0,0 +1,80 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper.h
+ *
+ * @brief Declares the interface between the Python plugin implementation and Remmina covering the initialization of
+ * the implementation and the load function, that allows Remmina to load plugins into the application.
+ *
+ * @details When Remmina discovers Python scripts in the plugin root folder the plugin manager passes the path to the
+ * Python plugin loader. There it gets executed and the plugin classes get mapped to "real" Remmina plugin
+ * instances.
+ *
+ * For the communication between Remmina and Python the python module called 'remmina' is initialized and
+ * loaded into the environment of the plugin script (@see python_wrapper_module.c).
+ *
+ * @see http://www.remmina.org/wp for more information.
+ */
+
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "remmina/plugin.h"
+
+G_BEGIN_DECLS
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * @brief Initializes the Python plugin loaders.
+ * @details This does not load any plugins but initializes the implementation (e.g. globals and the Python engine).
+ */
+gboolean python_wrapper_init(RemminaLanguageWrapperPlugin* plugin);
+
+/**
+ * @brief Loads a plugin from the Remmina plugin folder with the given name.
+ *
+ * @param service The instance of the service providing an API between Remmina and its plugins.
+ * @param filename The filename of the plugin to load.
+ *
+ * @return TRUE on success, FALSE otherwise.
+ */
+gboolean python_wrapper_load(RemminaLanguageWrapperPlugin* plugin, const gchar* filename);
+
+G_END_DECLS
diff --git a/plugins/python_wrapper/python_wrapper_pref.c b/plugins/python_wrapper/python_wrapper_pref.c
new file mode 100644
index 0000000..84a76db
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_pref.c
@@ -0,0 +1,104 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_common.h"
+#include "python_wrapper_pref.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// D E C L A R A T I O N S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+void python_wrapper_pref_init(void)
+{
+ TRACE_CALL(__func__);
+
+}
+
+/**
+ * @brief
+ */
+GtkWidget* python_wrapper_pref_get_pref_body_wrapper(RemminaPrefPlugin* instance)
+{
+ TRACE_CALL(__func__);
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+
+ PyObject* result = CallPythonMethod(plugin->instance, "get_pref_body", NULL, NULL);
+ if (result == Py_None || result == NULL)
+ {
+ return NULL;
+ }
+
+ return get_pywidget(result);
+}
+
+RemminaPlugin* python_wrapper_create_pref_plugin(PyPlugin* plugin)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* instance = plugin->instance;
+
+ if (!python_wrapper_check_attribute(instance, ATTR_NAME)
+ || !python_wrapper_check_attribute(instance, ATTR_VERSION)
+ || !python_wrapper_check_attribute(instance, ATTR_DESCRIPTION)
+ || !python_wrapper_check_attribute(instance, ATTR_PREF_LABEL))
+ {
+ g_printerr("Unable to create pref plugin. Aborting!\n");
+ return NULL;
+ }
+
+ RemminaPrefPlugin* remmina_plugin = (RemminaPrefPlugin*)python_wrapper_malloc(sizeof(RemminaPrefPlugin));
+
+ remmina_plugin->type = REMMINA_PLUGIN_TYPE_PREF;
+ remmina_plugin->domain = GETTEXT_PACKAGE;
+ remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME));
+ remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION));
+ remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION));
+ remmina_plugin->pref_label = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_PREF_LABEL));
+ remmina_plugin->get_pref_body = python_wrapper_pref_get_pref_body_wrapper;
+
+ plugin->pref_plugin = remmina_plugin;
+ plugin->generic_plugin = (RemminaPlugin*)remmina_plugin;
+
+ python_wrapper_add_plugin(plugin);
+
+ return (RemminaPlugin*)remmina_plugin;
+}
diff --git a/plugins/python_wrapper/python_wrapper_pref.h b/plugins/python_wrapper/python_wrapper_pref.h
new file mode 100644
index 0000000..9dadb96
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_pref.h
@@ -0,0 +1,63 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_pref.h
+ *
+ * @brief Contains the specialisation of RemminaPluginFile plugins in Python.
+ */
+
+#pragma once
+
+#include "python_wrapper_common.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+G_BEGIN_DECLS
+
+/**
+ * Initializes the Python plugin specialisation for preferences plugins.
+ */
+void python_wrapper_pref_init(void);
+
+/**
+ * @brief Creates a new instance of the RemminaPluginPref, initializes its members and references the wrapper
+ * functions.
+ * @param instance The instance of the Python plugin.
+ * @return Returns a new instance of the RemminaPlugin (must be freed!).
+ */
+RemminaPlugin* python_wrapper_create_pref_plugin(PyPlugin* instance);
+
+G_END_DECLS
diff --git a/plugins/python_wrapper/python_wrapper_protocol.c b/plugins/python_wrapper/python_wrapper_protocol.c
new file mode 100644
index 0000000..64225aa
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_protocol.c
@@ -0,0 +1,315 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_common.c
+ * @brief
+ * @author Mathias Winterhalter
+ * @date 07.04.2021
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_common.h"
+#include "python_wrapper_protocol.h"
+#include "python_wrapper_remmina.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// D E C L A R A T I O N S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+void python_wrapper_protocol_init(void)
+{
+ TRACE_CALL(__func__);
+}
+
+void remmina_protocol_init_wrapper(RemminaProtocolWidget* gp)
+{
+ TRACE_CALL(__func__);
+ PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ py_plugin->gp->gp = gp;
+ CallPythonMethod(py_plugin->instance, "init", "O", py_plugin->gp);
+}
+
+gboolean remmina_protocol_open_connection_wrapper(RemminaProtocolWidget* gp)
+{
+ TRACE_CALL(__func__);
+ PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ if (py_plugin)
+ {
+ PyObject* result = CallPythonMethod(py_plugin->instance, "open_connection", "O", py_plugin->gp);
+ return result == Py_True;
+ }
+ else
+ {
+ return gtk_false();
+ }
+}
+
+gboolean remmina_protocol_close_connection_wrapper(RemminaProtocolWidget* gp)
+{
+ TRACE_CALL(__func__);
+ PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ PyObject* result = CallPythonMethod(py_plugin->instance, "close_connection", "O", py_plugin->gp);
+ return result == Py_True;
+}
+
+gboolean remmina_protocol_query_feature_wrapper(RemminaProtocolWidget* gp,
+ const RemminaProtocolFeature* feature)
+{
+ TRACE_CALL(__func__);
+ PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ PyRemminaProtocolFeature* pyFeature = python_wrapper_protocol_feature_new();
+ pyFeature->type = (gint)feature->type;
+ pyFeature->id = feature->id;
+ pyFeature->opt1 = python_wrapper_generic_new();
+ pyFeature->opt1->raw = feature->opt1;
+ pyFeature->opt2 = python_wrapper_generic_new();
+ pyFeature->opt2->raw = feature->opt2;
+ pyFeature->opt3 = python_wrapper_generic_new();
+ pyFeature->opt3->raw = feature->opt3;
+
+ PyObject* result = CallPythonMethod(py_plugin->instance, "query_feature", "OO", py_plugin->gp, pyFeature);
+ Py_DecRef((PyObject*)pyFeature);
+ Py_DecRef((PyObject*)pyFeature->opt1);
+ Py_DecRef((PyObject*)pyFeature->opt2);
+ Py_DecRef((PyObject*)pyFeature->opt3);
+ return result == Py_True;
+}
+
+void remmina_protocol_call_feature_wrapper(RemminaProtocolWidget* gp, const RemminaProtocolFeature* feature)
+{
+ TRACE_CALL(__func__);
+ PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ PyRemminaProtocolFeature* pyFeature = python_wrapper_protocol_feature_new();
+ pyFeature->type = (gint)feature->type;
+ pyFeature->id = feature->id;
+ pyFeature->opt1 = python_wrapper_generic_new();
+ pyFeature->opt1->raw = feature->opt1;
+ pyFeature->opt1->type_hint = feature->opt1_type_hint;
+ pyFeature->opt2 = python_wrapper_generic_new();
+ pyFeature->opt2->raw = feature->opt2;
+ pyFeature->opt2->type_hint = feature->opt2_type_hint;
+ pyFeature->opt3 = python_wrapper_generic_new();
+ pyFeature->opt3->raw = feature->opt3;
+ pyFeature->opt3->type_hint = feature->opt3_type_hint;
+
+ CallPythonMethod(py_plugin->instance, "call_feature", "OO", py_plugin->gp, pyFeature);
+ Py_DecRef((PyObject*)pyFeature);
+ Py_DecRef((PyObject*)pyFeature->opt1);
+ Py_DecRef((PyObject*)pyFeature->opt2);
+ Py_DecRef((PyObject*)pyFeature->opt3);
+}
+
+void remmina_protocol_send_keytrokes_wrapper(RemminaProtocolWidget* gp,
+ const guint keystrokes[],
+ const gint keylen)
+{
+ TRACE_CALL(__func__);
+ PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ PyObject* obj = PyList_New(keylen);
+ Py_IncRef(obj);
+ for (int i = 0; i < keylen; ++i)
+ {
+ PyList_SetItem(obj, i, PyLong_FromLong(keystrokes[i]));
+ }
+ CallPythonMethod(py_plugin->instance, "send_keystrokes", "OO", py_plugin->gp, obj);
+ Py_DecRef(obj);
+}
+
+gboolean remmina_protocol_get_plugin_screenshot_wrapper(RemminaProtocolWidget* gp,
+ RemminaPluginScreenshotData* rpsd)
+{
+ TRACE_CALL(__func__);
+
+ PyPlugin* py_plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ PyRemminaPluginScreenshotData* data = python_wrapper_screenshot_data_new();
+ Py_IncRef((PyObject*)data);
+ PyObject* result = CallPythonMethod(py_plugin->instance, "get_plugin_screenshot", "OO", py_plugin->gp, data);
+ if (result == Py_True)
+ {
+ if (!PyByteArray_Check((PyObject*)data->buffer))
+ {
+ g_printerr("Unable to parse screenshot data. 'buffer' needs to be an byte array!");
+ return 0;
+ }
+ Py_ssize_t buffer_len = PyByteArray_Size((PyObject*)data->buffer);
+
+ // Is being freed by Remmina!
+ rpsd->buffer = (unsigned char*)python_wrapper_malloc(sizeof(unsigned char) * buffer_len);
+ if (!rpsd->buffer)
+ {
+ return 0;
+ }
+ memcpy(rpsd->buffer, PyByteArray_AsString((PyObject*)data->buffer), sizeof(unsigned char) * buffer_len);
+ rpsd->bytesPerPixel = data->bytesPerPixel;
+ rpsd->bitsPerPixel = data->bitsPerPixel;
+ rpsd->height = data->height;
+ rpsd->width = data->width;
+ }
+ Py_DecRef((PyObject*)data->buffer);
+ Py_DecRef((PyObject*)data);
+ return result == Py_True;
+}
+
+gboolean remmina_protocol_map_event_wrapper(RemminaProtocolWidget* gp)
+{
+ PyPlugin* plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ PyObject* result = CallPythonMethod(plugin->instance, "map_event", "O", plugin->gp);
+ return PyBool_Check(result) && result == Py_True;
+}
+
+gboolean remmina_protocol_unmap_event_wrapper(RemminaProtocolWidget* gp)
+{
+ PyPlugin* plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ PyObject* result = CallPythonMethod(plugin->instance, "unmap_event", "O", plugin->gp);
+ return PyBool_Check(result) && result == Py_True;
+}
+
+RemminaPlugin* python_wrapper_create_protocol_plugin(PyPlugin* plugin)
+{
+ PyObject* instance = plugin->instance;
+
+ if (!python_wrapper_check_attribute(instance, ATTR_ICON_NAME_SSH)
+ || !python_wrapper_check_attribute(instance, ATTR_ICON_NAME)
+ || !python_wrapper_check_attribute(instance, ATTR_FEATURES)
+ || !python_wrapper_check_attribute(instance, ATTR_BASIC_SETTINGS)
+ || !python_wrapper_check_attribute(instance, ATTR_ADVANCED_SETTINGS)
+ || !python_wrapper_check_attribute(instance, ATTR_SSH_SETTING))
+ {
+ g_printerr("Unable to create protocol plugin. Aborting!\n");
+ return NULL;
+ }
+
+ RemminaProtocolPlugin* remmina_plugin = (RemminaProtocolPlugin*)python_wrapper_malloc(sizeof(RemminaProtocolPlugin));
+
+ remmina_plugin->type = REMMINA_PLUGIN_TYPE_PROTOCOL;
+ remmina_plugin->domain = GETTEXT_PACKAGE;
+ remmina_plugin->basic_settings = NULL;
+ remmina_plugin->advanced_settings = NULL;
+ remmina_plugin->features = NULL;
+
+ remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME));
+ remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION));
+ remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION));
+ remmina_plugin->icon_name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_ICON_NAME));
+ remmina_plugin->icon_name_ssh = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_ICON_NAME_SSH));
+
+ PyObject* list = PyObject_GetAttrString(instance, "basic_settings");
+ Py_ssize_t len = PyList_Size(list);
+ if (len)
+ {
+ RemminaProtocolSetting* basic_settings = (RemminaProtocolSetting*)python_wrapper_malloc(
+ sizeof(RemminaProtocolSetting) * (len + 1));
+ memset(basic_settings, 0, sizeof(RemminaProtocolSetting) * (len + 1));
+
+ for (Py_ssize_t i = 0; i < len; ++i)
+ {
+ RemminaProtocolSetting* dest = basic_settings + i;
+ python_wrapper_to_protocol_setting(dest, PyList_GetItem(list, i));
+ }
+ RemminaProtocolSetting* dest = basic_settings + len;
+ dest->type = REMMINA_PROTOCOL_SETTING_TYPE_END;
+ remmina_plugin->basic_settings = basic_settings;
+ }
+
+ list = PyObject_GetAttrString(instance, "advanced_settings");
+ len = PyList_Size(list);
+ if (len)
+ {
+ RemminaProtocolSetting* advanced_settings = (RemminaProtocolSetting*)python_wrapper_malloc(
+ sizeof(RemminaProtocolSetting) * (len + 1));
+ memset(advanced_settings, 0, sizeof(RemminaProtocolSetting) * (len + 1));
+
+ for (Py_ssize_t i = 0; i < len; ++i)
+ {
+ RemminaProtocolSetting* dest = advanced_settings + i;
+ python_wrapper_to_protocol_setting(dest, PyList_GetItem(list, i));
+ }
+
+ RemminaProtocolSetting* dest = advanced_settings + len;
+ dest->type = REMMINA_PROTOCOL_SETTING_TYPE_END;
+
+ remmina_plugin->advanced_settings = advanced_settings;
+ }
+
+ list = PyObject_GetAttrString(instance, "features");
+ len = PyList_Size(list);
+ if (len)
+ {
+ RemminaProtocolFeature* features = (RemminaProtocolFeature*)python_wrapper_malloc(
+ sizeof(RemminaProtocolFeature) * (len + 1));
+ memset(features, 0, sizeof(RemminaProtocolFeature) * (len + 1));
+
+ for (Py_ssize_t i = 0; i < len; ++i)
+ {
+ RemminaProtocolFeature* dest = features + i;
+ python_wrapper_to_protocol_feature(dest, PyList_GetItem(list, i));
+ }
+
+ RemminaProtocolFeature* dest = features + len;
+ dest->type = REMMINA_PROTOCOL_FEATURE_TYPE_END;
+
+ remmina_plugin->features = features;
+ }
+
+ remmina_plugin->ssh_setting = (RemminaProtocolSSHSetting)python_wrapper_get_attribute_long(instance,
+ ATTR_SSH_SETTING,
+ REMMINA_PROTOCOL_SSH_SETTING_NONE);
+
+ remmina_plugin->init = remmina_protocol_init_wrapper; // Plugin initialization
+ remmina_plugin->open_connection = remmina_protocol_open_connection_wrapper; // Plugin open connection
+ remmina_plugin->close_connection = remmina_protocol_close_connection_wrapper; // Plugin close connection
+ remmina_plugin->query_feature = remmina_protocol_query_feature_wrapper; // Query for available features
+ remmina_plugin->call_feature = remmina_protocol_call_feature_wrapper; // Call a feature
+ remmina_plugin->send_keystrokes =
+ remmina_protocol_send_keytrokes_wrapper; // Send a keystroke
+ remmina_plugin->get_plugin_screenshot =
+ remmina_protocol_get_plugin_screenshot_wrapper; // Screenshot support unavailable
+
+ remmina_plugin->map_event = remmina_protocol_map_event_wrapper;
+ remmina_plugin->unmap_event = remmina_protocol_unmap_event_wrapper;
+
+ plugin->protocol_plugin = remmina_plugin;
+ plugin->generic_plugin = (RemminaPlugin*)remmina_plugin;
+
+ python_wrapper_add_plugin(plugin);
+
+ return (RemminaPlugin*)remmina_plugin;
+} \ No newline at end of file
diff --git a/plugins/python_wrapper/python_wrapper_protocol.h b/plugins/python_wrapper/python_wrapper_protocol.h
new file mode 100644
index 0000000..7c4c9da
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_protocol.h
@@ -0,0 +1,109 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_protocol.h
+ *
+ * @brief Contains the specialisation of RemminaPluginFile plugins in Python.
+ */
+
+#pragma once
+
+#include "python_wrapper_common.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "remmina/plugin.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+G_BEGIN_DECLS
+
+/**
+ * Wrapper for a Python object that contains a pointer to an instance of RemminaProtocolFeature.
+ */
+typedef struct
+{
+ PyObject_HEAD
+ RemminaProtocolFeatureType type;
+ gint id;
+ PyGeneric* opt1;
+ PyGeneric* opt2;
+ PyGeneric* opt3;
+} PyRemminaProtocolFeature;
+
+/**
+ *
+ */
+typedef struct
+{
+ PyObject_HEAD
+ PyByteArrayObject* buffer;
+ int bitsPerPixel;
+ int bytesPerPixel;
+ int width;
+ int height;
+} PyRemminaPluginScreenshotData;
+
+/**
+ * Initializes the Python plugin specialisation for protocol plugins.
+ */
+void python_wrapper_protocol_init(void);
+
+/**
+ * @brief Creates a new instance of the RemminaPluginProtocol, initializes its members and references the wrapper
+ * functions.
+ *
+ * @param instance The instance of the Python plugin.
+ *
+ * @return Returns a new instance of the RemminaPlugin (must be freed!).
+ */
+RemminaPlugin* python_wrapper_create_protocol_plugin(PyPlugin* plugin);
+
+/**
+ *
+ * @return
+ */
+PyRemminaProtocolFeature* python_wrapper_protocol_feature_new(void);
+
+/**
+ *
+ * @return
+ */
+PyRemminaPluginScreenshotData* python_wrapper_screenshot_data_new(void);
+
+G_END_DECLS
diff --git a/plugins/python_wrapper/python_wrapper_protocol_widget.c b/plugins/python_wrapper/python_wrapper_protocol_widget.c
new file mode 100644
index 0000000..3f8ee7a
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_protocol_widget.c
@@ -0,0 +1,845 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_protocol_widget.c
+ * @brief Implementation of the Protocol Widget API.
+ * @author Mathias Winterhalter
+ * @date 19.11.2020
+ *
+ * The RemminaPluginService provides an API for plugins to interact with Remmina. The
+ * module called 'remmina' forwards this interface to make it accessible for Python
+ * scripts.
+ *
+ * This is an example of a minimal protocol plugin:
+ *
+ * @code
+ * import remmina
+ *
+ * class MyProtocol:
+ * def __init__(self):
+ * self.name = "MyProtocol"
+ * self.description = "Example protocol plugin to explain how Python plugins work."
+ * self.version = "0.1"
+ * self.icon_name = ""
+ * self.icon_name_ssh = ""
+ *
+ * def init(self, handle):
+ * print("This is getting logged to the standard output of Remmina.")
+ * remmina.log_print("For debugging purposes it would be better to log the output to the %s window %s!" % ("debug", ":)"))
+ * self.init_your_stuff(handle)
+ *
+ * def open_connection(self, handle):
+ * if not self.connect():
+ * remmina.log_print("Error! Can not connect...")
+ * return False
+ *
+ * remmina.remmina_signal_connected(handle)
+ * remmina.log_print("Connection established!")
+ * return True
+ *
+ *
+ * def close_connection(self, handle):
+ * self.disconnect()
+ * return True
+ *
+ * plugin = MyProtocol()
+ * remmina.register_plugin(plugin)
+ * @endcode
+ *
+ *
+ *
+ * @see http://www.remmina.org/wp for more information.
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N L U C E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_common.h"
+#include "remmina/plugin.h"
+#include "remmina/types.h"
+#include "python_wrapper_remmina_file.h"
+#include "python_wrapper_protocol_widget.h"
+#include "python_wrapper_protocol.h"
+
+// -- Python Type -> RemminaWidget
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// D E C L A R A T I O N S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static PyObject* protocol_widget_get_viewport(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_width(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_set_width(PyRemminaProtocolWidget* self, PyObject* var_width);
+static PyObject* protocol_widget_get_height(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_set_height(PyRemminaProtocolWidget* self, PyObject* var_height);
+static PyObject* protocol_widget_get_current_scale_mode(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_expand(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_set_expand(PyRemminaProtocolWidget* self, PyObject* var_expand);
+static PyObject* protocol_widget_has_error(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_set_error(PyRemminaProtocolWidget* self, PyObject* var_msg);
+static PyObject* protocol_widget_is_closed(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_file(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_emit_signal(PyRemminaProtocolWidget* self, PyObject* var_signal);
+static PyObject* protocol_widget_register_hostkey(PyRemminaProtocolWidget* self, PyObject* var_widget);
+static PyObject* protocol_widget_start_direct_tunnel(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_start_reverse_tunnel(PyRemminaProtocolWidget* self, PyObject* var_local_port);
+static PyObject* protocol_widget_start_xport_tunnel(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_set_display(PyRemminaProtocolWidget* self, PyObject* var_display);
+static PyObject* protocol_widget_signal_connection_closed(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_signal_connection_opened(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_update_align(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_unlock_dynres(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_desktop_resize(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_panel_new_certificate(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_panel_changed_certificate(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_username(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_password(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_domain(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_savepassword(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_panel_authx509(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_cacert(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_cacrl(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_clientcert(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_get_clientkey(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_save_cred(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_panel_show_listen(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_panel_show_retry(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_panel_show(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_panel_hide(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_ssh_exec(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_chat_open(PyRemminaProtocolWidget* self, PyObject* var_name);
+static PyObject* protocol_widget_chat_close(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_chat_receive(PyRemminaProtocolWidget* self, PyObject* args);
+static PyObject* protocol_widget_send_keys_signals(PyRemminaProtocolWidget* self, PyObject* args);
+
+static struct PyMethodDef python_protocol_widget_type_methods[] =
+ {{ "get_viewport", (PyCFunction)protocol_widget_get_viewport, METH_NOARGS, "" },
+ { "get_width", (PyCFunction)protocol_widget_get_width, METH_NOARGS, "" },
+ { "set_width", (PyCFunction)protocol_widget_set_width, METH_VARARGS, "" },
+ { "get_height", (PyCFunction)protocol_widget_get_height, METH_VARARGS, "" },
+ { "set_height", (PyCFunction)protocol_widget_set_height, METH_VARARGS, "" },
+ { "get_current_scale_mode", (PyCFunction)protocol_widget_get_current_scale_mode, METH_VARARGS, "" },
+ { "get_expand", (PyCFunction)protocol_widget_get_expand, METH_VARARGS, "" },
+ { "set_expand", (PyCFunction)protocol_widget_set_expand, METH_VARARGS, "" },
+ { "has_error", (PyCFunction)protocol_widget_has_error, METH_VARARGS, "" },
+ { "set_error", (PyCFunction)protocol_widget_set_error, METH_VARARGS, "" },
+ { "is_closed", (PyCFunction)protocol_widget_is_closed, METH_VARARGS, "" },
+ { "get_file", (PyCFunction)protocol_widget_get_file, METH_NOARGS, "" },
+ { "emit_signal", (PyCFunction)protocol_widget_emit_signal, METH_VARARGS, "" },
+ { "register_hostkey", (PyCFunction)protocol_widget_register_hostkey, METH_VARARGS, "" },
+ { "start_direct_tunnel", (PyCFunction)protocol_widget_start_direct_tunnel, METH_VARARGS | METH_KEYWORDS, "" },
+ { "start_reverse_tunnel", (PyCFunction)protocol_widget_start_reverse_tunnel, METH_VARARGS, "" },
+ { "start_xport_tunnel", (PyCFunction)protocol_widget_start_xport_tunnel, METH_VARARGS, "" },
+ { "set_display", (PyCFunction)protocol_widget_set_display, METH_VARARGS, "" },
+ { "signal_connection_closed", (PyCFunction)protocol_widget_signal_connection_closed, METH_VARARGS, "" },
+ { "signal_connection_opened", (PyCFunction)protocol_widget_signal_connection_opened, METH_VARARGS, "" },
+ { "update_align", (PyCFunction)protocol_widget_update_align, METH_VARARGS, "" },
+ { "unlock_dynres", (PyCFunction)protocol_widget_unlock_dynres, METH_VARARGS, "" },
+ { "desktop_resize", (PyCFunction)protocol_widget_desktop_resize, METH_VARARGS, "" },
+ { "panel_new_certificate", (PyCFunction)protocol_widget_panel_new_certificate, METH_VARARGS | METH_KEYWORDS, "" },
+ { "panel_changed_certificate", (PyCFunction)protocol_widget_panel_changed_certificate,
+ METH_VARARGS | METH_KEYWORDS, "" },
+ { "get_username", (PyCFunction)protocol_widget_get_username, METH_VARARGS, "" },
+ { "get_password", (PyCFunction)protocol_widget_get_password, METH_VARARGS, "" },
+ { "get_domain", (PyCFunction)protocol_widget_get_domain, METH_VARARGS, "" },
+ { "get_savepassword", (PyCFunction)protocol_widget_get_savepassword, METH_VARARGS, "" },
+ { "panel_authx509", (PyCFunction)protocol_widget_panel_authx509, METH_VARARGS, "" },
+ { "get_cacert", (PyCFunction)protocol_widget_get_cacert, METH_VARARGS, "" },
+ { "get_cacrl", (PyCFunction)protocol_widget_get_cacrl, METH_VARARGS, "" },
+ { "get_clientcert", (PyCFunction)protocol_widget_get_clientcert, METH_VARARGS, "" },
+ { "get_clientkey", (PyCFunction)protocol_widget_get_clientkey, METH_VARARGS, "" },
+ { "save_cred", (PyCFunction)protocol_widget_save_cred, METH_VARARGS, "" },
+ { "panel_show_listen", (PyCFunction)protocol_widget_panel_show_listen, METH_VARARGS, "" },
+ { "panel_show_retry", (PyCFunction)protocol_widget_panel_show_retry, METH_VARARGS, "" },
+ { "panel_show", (PyCFunction)protocol_widget_panel_show, METH_VARARGS, "" },
+ { "panel_hide", (PyCFunction)protocol_widget_panel_hide, METH_VARARGS, "" },
+ { "ssh_exec", (PyCFunction)protocol_widget_ssh_exec, METH_VARARGS | METH_KEYWORDS, "" },
+ { "chat_open", (PyCFunction)protocol_widget_chat_open, METH_VARARGS, "" },
+ { "chat_close", (PyCFunction)protocol_widget_chat_close, METH_VARARGS, "" },
+ { "chat_receive", (PyCFunction)protocol_widget_chat_receive, METH_VARARGS, "" },
+ { "send_keys_signals", (PyCFunction)protocol_widget_send_keys_signals, METH_VARARGS | METH_KEYWORDS, "" },
+ { NULL }};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static PyObject* python_protocol_feature_new(PyTypeObject* type, PyObject* kws, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ PyRemminaProtocolWidget* self;
+ self = (PyRemminaProtocolWidget*)type->tp_alloc(type, 0);
+ if (!self)
+ {
+ return NULL;
+ }
+
+ return (PyObject*)self;
+}
+
+static int python_protocol_feature_init(PyObject* self, PyObject* args, PyObject* kwds)
+{
+ return 0;
+}
+
+static PyTypeObject python_protocol_widget_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "remmina.RemminaProtocolWidget",
+ .tp_doc = "RemminaProtocolWidget",
+ .tp_basicsize = sizeof(PyRemminaProtocolWidget),
+ .tp_itemsize = 0,
+ .tp_flags = Py_TPFLAGS_DEFAULT,
+ .tp_new = python_protocol_feature_new,
+ .tp_init = python_protocol_feature_init,
+ .tp_methods = python_protocol_widget_type_methods
+};
+
+PyRemminaProtocolWidget* python_wrapper_protocol_widget_create(void)
+{
+ TRACE_CALL(__func__);
+
+ PyRemminaProtocolWidget* result = PyObject_NEW(PyRemminaProtocolWidget, &python_protocol_widget_type);
+ assert(result);
+
+ PyErr_Print();
+ Py_INCREF(result);
+ result->gp = NULL;
+ return result;
+}
+
+void python_wrapper_protocol_widget_init(void)
+{
+ init_pygobject();
+}
+
+void python_wrapper_protocol_widget_type_ready(void)
+{
+ TRACE_CALL(__func__);
+ if (PyType_Ready(&python_protocol_widget_type) < 0)
+ {
+ g_printerr("Error initializing remmina.RemminaWidget!\n");
+ PyErr_Print();
+ }
+}
+
+static PyObject* protocol_widget_get_viewport(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+ return (PyObject*)new_pywidget(G_OBJECT(python_wrapper_get_service()->protocol_widget_gtkviewport(self->gp)));
+}
+
+static PyObject* protocol_widget_get_width(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("i", python_wrapper_get_service()->protocol_widget_get_width(self->gp));
+}
+
+static PyObject* protocol_widget_set_width(PyRemminaProtocolWidget* self, PyObject* var_width)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ if (!var_width)
+ {
+ g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ if (PyLong_Check(var_width))
+ {
+ g_printerr("[%s:%d@%s]: Argument is not of type Long!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ gint width = (gint)PyLong_AsLong(var_width);
+ python_wrapper_get_service()->protocol_widget_set_height(self->gp, width);
+
+ return Py_None;
+}
+
+static PyObject* protocol_widget_get_height(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("i", python_wrapper_get_service()->protocol_widget_get_height(self->gp));
+}
+
+static PyObject* protocol_widget_set_height(PyRemminaProtocolWidget* self, PyObject* var_height)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ if (!var_height)
+ {
+ g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ if (PyLong_Check(var_height))
+ {
+ g_printerr("[%s:%d@%s]: Argument is not of type Long!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ gint height = (gint)PyLong_AsLong(var_height);
+ python_wrapper_get_service()->protocol_widget_set_height(self->gp, height);
+
+ return Py_None;
+}
+
+static PyObject* protocol_widget_get_current_scale_mode(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("i", python_wrapper_get_service()->protocol_widget_get_current_scale_mode(self->gp));
+}
+
+static PyObject* protocol_widget_get_expand(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_get_expand(self->gp));
+}
+
+static PyObject* protocol_widget_set_expand(PyRemminaProtocolWidget* self, PyObject* var_expand)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ if (!var_expand)
+ {
+ g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ if (PyBool_Check(var_expand))
+ {
+ g_printerr("[%s:%d@%s]: Argument is not of type Boolean!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ python_wrapper_get_service()->protocol_widget_set_expand(self->gp, PyObject_IsTrue(var_expand));
+
+ return Py_None;
+}
+
+static PyObject* protocol_widget_has_error(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_has_error(self->gp));
+}
+
+static PyObject* protocol_widget_set_error(PyRemminaProtocolWidget* self, PyObject* var_msg)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ if (!var_msg)
+ {
+ g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ if (PyUnicode_Check(var_msg))
+ {
+ g_printerr("[%s:%d@%s]: Argument is not of type String!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ const gchar* msg = PyUnicode_AsUTF8(var_msg);
+ python_wrapper_get_service()->protocol_widget_set_error(self->gp, msg);
+
+ return Py_None;
+}
+
+static PyObject* protocol_widget_is_closed(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_is_closed(self->gp));
+}
+
+static PyObject* protocol_widget_get_file(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ RemminaFile* file = python_wrapper_get_service()->protocol_widget_get_file(self->gp);
+ return (PyObject*)python_wrapper_remmina_file_to_python(file);
+}
+
+static PyObject* protocol_widget_emit_signal(PyRemminaProtocolWidget* self, PyObject* var_signal)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ if (!var_signal)
+ {
+ g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ if (PyUnicode_Check(var_signal))
+ {
+ g_printerr("[%s:%d@%s]: Argument is not of type String!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ python_wrapper_get_service()->protocol_widget_set_error(self->gp, PyUnicode_AsUTF8(var_signal));
+
+ return Py_None;
+}
+
+static PyObject* protocol_widget_register_hostkey(PyRemminaProtocolWidget* self, PyObject* var_widget)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ if (!var_widget)
+ {
+ g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ python_wrapper_get_service()->protocol_widget_register_hostkey(self->gp, get_pywidget(var_widget));
+
+ return Py_None;
+}
+
+static PyObject* protocol_widget_start_direct_tunnel(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ gint default_port;
+ gboolean port_plus;
+
+ if (!args)
+ {
+ g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__);
+ }
+
+ if (PyArg_ParseTuple(args, "ii", &default_port, &port_plus))
+ {
+ return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_start_direct_tunnel(self->gp, default_port, port_plus));
+ }
+ else
+ {
+ PyErr_Print();
+ return NULL;
+ }
+ return Py_None;
+}
+
+static PyObject* protocol_widget_start_reverse_tunnel(PyRemminaProtocolWidget* self, PyObject* var_local_port)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ if (!PyLong_Check(var_local_port))
+ {
+ g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ if (!PyLong_Check(var_local_port))
+ {
+ g_printerr("[%s:%d@%s]: Argument is not of type Long!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_start_reverse_tunnel(self->gp, (gint)PyLong_AsLong(var_local_port)));
+}
+
+static gboolean xport_tunnel_init(RemminaProtocolWidget* gp, gint remotedisplay, const gchar* server, gint port)
+{
+ TRACE_CALL(__func__);
+ PyPlugin* plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ PyObject* result = PyObject_CallMethod(plugin->instance, "xport_tunnel_init", "Oisi", gp, remotedisplay, server, port);
+ return PyObject_IsTrue(result);
+}
+
+static PyObject* protocol_widget_start_xport_tunnel(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_start_xport_tunnel(self->gp, xport_tunnel_init));
+}
+
+static PyObject* protocol_widget_set_display(PyRemminaProtocolWidget* self, PyObject* var_display)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ if (!var_display)
+ {
+ g_printerr("[%s:%d@%s]: Argument is null!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ if (!PyLong_Check(var_display))
+ {
+ g_printerr("[%s:%d@%s]: Argument is not of type Long!\n", __FILE__, __LINE__, __func__);
+ return NULL;
+ }
+
+ python_wrapper_get_service()->protocol_widget_set_display(self->gp, (gint)PyLong_AsLong(var_display));
+
+ return Py_None;
+}
+
+static PyObject* protocol_widget_signal_connection_closed(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ python_wrapper_get_service()->protocol_widget_signal_connection_closed(self->gp);
+ return Py_None;
+}
+
+static PyObject* protocol_widget_signal_connection_opened(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ python_wrapper_get_service()->protocol_widget_signal_connection_opened(self->gp);
+ return Py_None;
+}
+
+static PyObject* protocol_widget_update_align(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ python_wrapper_get_service()->protocol_widget_update_align(self->gp);
+ return Py_None;
+}
+
+static PyObject* protocol_widget_unlock_dynres(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ python_wrapper_get_service()->protocol_widget_unlock_dynres(self->gp);
+ return Py_None;
+}
+
+static PyObject* protocol_widget_desktop_resize(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ python_wrapper_get_service()->protocol_widget_desktop_resize(self->gp);
+ return Py_None;
+}
+
+static PyObject* protocol_widget_panel_new_certificate(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+ gchar* subject, * issuer, * fingerprint;
+
+ if (PyArg_ParseTuple(args, "sss", &subject, &issuer, &fingerprint))
+ {
+ python_wrapper_get_service()->protocol_widget_panel_new_certificate(self->gp, subject, issuer, fingerprint);
+ }
+ else
+ {
+ PyErr_Print();
+ return NULL;
+ }
+ return Py_None;
+}
+
+static PyObject* protocol_widget_panel_changed_certificate(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+ gchar* subject, * issuer, * new_fingerprint, * old_fingerprint;
+
+ if (PyArg_ParseTuple(args, "sss", &subject, &issuer, &new_fingerprint, &old_fingerprint))
+ {
+ python_wrapper_get_service()->protocol_widget_panel_changed_certificate(self->gp, subject, issuer, new_fingerprint, old_fingerprint);
+ }
+ else
+ {
+ PyErr_Print();
+ return NULL;
+ }
+ return Py_None;
+}
+
+static PyObject* protocol_widget_get_username(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_username(self->gp));
+}
+
+static PyObject* protocol_widget_get_password(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_password(self->gp));
+}
+
+static PyObject* protocol_widget_get_domain(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_domain(self->gp));
+}
+
+static PyObject* protocol_widget_get_savepassword(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("p", python_wrapper_get_service()->protocol_widget_get_savepassword(self->gp));
+}
+
+static PyObject* protocol_widget_panel_authx509(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("i", python_wrapper_get_service()->protocol_widget_panel_authx509(self->gp));
+}
+
+static PyObject* protocol_widget_get_cacert(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_cacert(self->gp));
+}
+
+static PyObject* protocol_widget_get_cacrl(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_cacrl(self->gp));
+}
+
+static PyObject* protocol_widget_get_clientcert(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_clientcert(self->gp));
+}
+
+static PyObject* protocol_widget_get_clientkey(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ return Py_BuildValue("s", python_wrapper_get_service()->protocol_widget_get_clientkey(self->gp));
+}
+
+static PyObject* protocol_widget_save_cred(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ python_wrapper_get_service()->protocol_widget_save_cred(self->gp);
+ return Py_None;
+}
+
+static PyObject* protocol_widget_panel_show_listen(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+ gint port = 0;
+
+ if (PyArg_ParseTuple(args, "i", &port))
+ {
+ python_wrapper_get_service()->protocol_widget_panel_show_listen(self->gp, port);
+ }
+ else
+ {
+ PyErr_Print();
+ return NULL;
+ }
+ return Py_None;
+}
+
+static PyObject* protocol_widget_panel_show_retry(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ python_wrapper_get_service()->protocol_widget_panel_show_retry(self->gp);
+ return Py_None;
+}
+
+static PyObject* protocol_widget_panel_show(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ python_wrapper_get_service()->protocol_widget_panel_show(self->gp);
+ return Py_None;
+}
+
+static PyObject* protocol_widget_panel_hide(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ python_wrapper_get_service()->protocol_widget_panel_hide(self->gp);
+ return Py_None;
+}
+
+static PyObject* protocol_widget_ssh_exec(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+ gboolean wait;
+ gchar* cmd;
+
+ if (PyArg_ParseTuple(args, "ps", &wait, &cmd))
+ {
+ python_wrapper_get_service()->protocol_widget_ssh_exec(self->gp, wait, cmd);
+ }
+ else
+ {
+ PyErr_Print();
+ return NULL;
+ }
+ return Py_None;
+}
+
+static void _on_send_callback_wrapper(RemminaProtocolWidget* gp, const gchar* text)
+{
+ PyPlugin* plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ PyObject_CallMethod(plugin->instance, "on_send", "Os", gp, text);
+}
+
+static void _on_destroy_callback_wrapper(RemminaProtocolWidget* gp)
+{
+ PyPlugin* plugin = python_wrapper_get_plugin_by_protocol_widget(gp);
+ PyObject_CallMethod(plugin->instance, "on_destroy", "O", gp);
+}
+
+static PyObject* protocol_widget_chat_open(PyRemminaProtocolWidget* self, PyObject* var_name)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ if (!PyUnicode_Check(var_name))
+ {
+ g_printerr("[%s:%d@%s]: Argument is not of type String!\n", __FILE__, __LINE__, __func__);
+ }
+
+ python_wrapper_get_service()->protocol_widget_chat_open(self->gp,
+ PyUnicode_AsUTF8(var_name),
+ _on_send_callback_wrapper,
+ _on_destroy_callback_wrapper);
+
+ return Py_None;
+}
+
+static PyObject* protocol_widget_chat_close(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+
+ python_wrapper_get_service()->protocol_widget_panel_hide(self->gp);
+ return Py_None;
+}
+
+static PyObject* protocol_widget_chat_receive(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+ gchar* text;
+
+ if (PyArg_ParseTuple(args, "s", &text))
+ {
+ python_wrapper_get_service()->protocol_widget_chat_receive(self->gp, text);
+ }
+ else
+ {
+ PyErr_Print();
+ return NULL;
+ }
+
+ return Py_None;
+}
+
+static PyObject* protocol_widget_send_keys_signals(PyRemminaProtocolWidget* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+ SELF_CHECK();
+ guint* keyvals;
+ int length;
+ GdkEventType event_type;
+ PyObject* widget;
+
+ if (PyArg_ParseTuple(args, "Osii", &widget, &keyvals, &length, &event_type) && widget && keyvals)
+ {
+ if (event_type < GDK_NOTHING || event_type >= GDK_EVENT_LAST)
+ {
+ g_printerr("[%s:%d@%s]: %d is not a known value for GdkEventType!\n", __FILE__, __LINE__, __func__, event_type);
+ return NULL;
+ }
+ else
+ {
+ python_wrapper_get_service()->protocol_widget_send_keys_signals((GtkWidget*)widget, keyvals, length, event_type);
+ }
+ }
+ else
+ {
+ PyErr_Print();
+ return NULL;
+ }
+
+ return Py_None;
+}
diff --git a/plugins/python_wrapper/python_wrapper_protocol_widget.h b/plugins/python_wrapper/python_wrapper_protocol_widget.h
new file mode 100644
index 0000000..40e54ac
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_protocol_widget.h
@@ -0,0 +1,65 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_protocol_widget.h
+ *
+ * @brief Contains the implementation of the widget handling used from the protocol plugin.
+ */
+
+#pragma once
+
+#include "python_wrapper_common.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+G_BEGIN_DECLS
+
+/**
+ * Initializes the widget backend of the protocol plugin implementation.
+ */
+void python_wrapper_protocol_widget_init(void);
+
+/**
+ * Initializes Python types used for protocol widgets.
+ */
+void python_wrapper_protocol_widget_type_ready(void);
+
+/**
+ * Creates a new instance of PyRemminaProtocolWidget and initializes its fields.
+ */
+PyRemminaProtocolWidget* python_wrapper_protocol_widget_create();
+
+G_END_DECLS
diff --git a/plugins/python_wrapper/python_wrapper_remmina.c b/plugins/python_wrapper/python_wrapper_remmina.c
new file mode 100644
index 0000000..7a4a771
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_remmina.c
@@ -0,0 +1,1241 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_common.h"
+#include "remmina/plugin.h"
+#include "remmina/types.h"
+
+#include "python_wrapper_remmina.h"
+#include "python_wrapper_protocol_widget.h"
+
+#include "python_wrapper_entry.h"
+#include "python_wrapper_file.h"
+#include "python_wrapper_protocol.h"
+#include "python_wrapper_tool.h"
+#include "python_wrapper_secret.h"
+#include "python_wrapper_pref.h"
+
+#include "python_wrapper_remmina_file.h"
+
+#include <string.h>
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// D E C L A R A T I O N S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+/**
+ * Util function to check if a specific member is define in a Python object.
+ */
+gboolean python_wrapper_check_mandatory_member(PyObject* instance, const gchar* member);
+
+static PyObject* python_wrapper_debug_wrapper(PyObject* self, PyObject* msg);
+static PyObject* remmina_register_plugin_wrapper(PyObject* self, PyObject* plugin);
+static PyObject* remmina_file_get_datadir_wrapper(PyObject* self, PyObject* plugin);
+static PyObject* remmina_file_new_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject* remmina_unlock_new_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject* remmina_pref_set_value_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject* remmina_pref_get_value_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject* remmina_pref_get_scale_quality_wrapper(PyObject* self, PyObject* plugin);
+static PyObject* remmina_pref_get_sshtunnel_port_wrapper(PyObject* self, PyObject* plugin);
+static PyObject* remmina_pref_get_ssh_loglevel_wrapper(PyObject* self, PyObject* plugin);
+static PyObject* remmina_pref_get_ssh_parseconfig_wrapper(PyObject* self, PyObject* plugin);
+static PyObject* remmina_pref_keymap_get_keyval_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject* python_wrapper_log_print_wrapper(PyObject* self, PyObject* arg);
+static PyObject* remmina_widget_pool_register_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject* rcw_open_from_file_full_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject* remmina_public_get_server_port_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject* remmina_masterthread_exec_is_main_thread_wrapper(PyObject* self, PyObject* plugin);
+static PyObject* remmina_gtksocket_available_wrapper(PyObject* self, PyObject* plugin);
+static PyObject*
+remmina_protocol_widget_get_profile_remote_height_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject*
+remmina_protocol_widget_get_profile_remote_width_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject* python_wrapper_show_dialog_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+static PyObject* python_wrapper_get_mainwindow_wrapper(PyObject* self, PyObject* args);
+static PyObject* remmina_protocol_plugin_signal_connection_opened_wrapper(PyObject* self, PyObject* args);
+static PyObject* remmina_protocol_plugin_signal_connection_closed_wrapper(PyObject* self, PyObject* args);
+static PyObject* remmina_protocol_plugin_init_auth_wrapper(PyObject* self, PyObject* args, PyObject* kwargs);
+
+/**
+ * Declares functions for the Remmina module. These functions can be called from Python and are wired to one of the
+ * functions here in this file.
+ */
+static PyMethodDef remmina_python_module_type_methods[] = {
+ /**
+ * The first function that need to be called from the plugin code, since it registers the Python class acting as
+ * a Remmina plugin. Without this call the plugin will not be recognized.
+ */
+ { "register_plugin", remmina_register_plugin_wrapper, METH_O, NULL },
+
+ /**
+ * Prints a string into the Remmina log infrastructure.
+ */
+ { "log_print", python_wrapper_log_print_wrapper, METH_VARARGS, NULL },
+
+ /**
+ * Prints a debug message if enabled.
+ */
+ { "debug", python_wrapper_debug_wrapper, METH_VARARGS, NULL },
+
+ /**
+ * Shows a GTK+ dialog.
+ */
+ { "show_dialog", (PyCFunction)python_wrapper_show_dialog_wrapper, METH_VARARGS | METH_KEYWORDS,
+ NULL },
+
+ /**
+ * Returns the GTK+ object of the main window of Remmina.
+ */
+ { "get_main_window", python_wrapper_get_mainwindow_wrapper, METH_NOARGS, NULL },
+
+ /**
+ * Calls remmina_file_get_datadir and returns its result.
+ */
+ { "get_datadir", remmina_file_get_datadir_wrapper, METH_VARARGS, NULL },
+
+ /**
+ * Calls remmina_file_new and returns its result.
+ */
+ { "file_new", (PyCFunction)remmina_file_new_wrapper, METH_VARARGS | METH_KEYWORDS, NULL },
+
+ /**
+ * Calls remmina_unlock_new and returns its result.
+ */
+ { "unlock_new", (PyCFunction)remmina_unlock_new_wrapper, METH_VARARGS | METH_KEYWORDS, NULL },
+
+ /**
+ * Calls remmina_pref_set_value and returns its result.
+ */
+ { "pref_set_value", (PyCFunction)remmina_pref_set_value_wrapper, METH_VARARGS | METH_KEYWORDS, NULL },
+
+ /**
+ * Calls remmina_pref_get_value and returns its result.
+ */
+ { "pref_get_value", (PyCFunction)remmina_pref_get_value_wrapper, METH_VARARGS | METH_KEYWORDS, NULL },
+
+ /**
+ * Calls remmina_pref_get_scale_quality and returns its result.
+ */
+ { "pref_get_scale_quality", remmina_pref_get_scale_quality_wrapper, METH_VARARGS, NULL },
+
+ /**
+ * Calls remmina_pref_get_sshtunnel_port and returns its result.
+ */
+ { "pref_get_sshtunnel_port", remmina_pref_get_sshtunnel_port_wrapper, METH_VARARGS, NULL },
+
+ /**
+ * Calls remmina_pref_get_ssh_loglevel and returns its result.
+ */
+ { "pref_get_ssh_loglevel", remmina_pref_get_ssh_loglevel_wrapper, METH_VARARGS, NULL },
+
+ /**
+ * Calls remmina_pref_get_ssh_parseconfig and returns its result.
+ */
+ { "pref_get_ssh_parseconfig", remmina_pref_get_ssh_parseconfig_wrapper, METH_VARARGS, NULL },
+
+ /**
+ * Calls remmina_pref_keymap_get_keyval and returns its result.
+ */
+ { "pref_keymap_get_keyval", (PyCFunction)remmina_pref_keymap_get_keyval_wrapper, METH_VARARGS | METH_KEYWORDS,
+ NULL },
+
+ /**
+ * Calls remmina_widget_pool_register and returns its result.
+ */
+ { "widget_pool_register", (PyCFunction)remmina_widget_pool_register_wrapper, METH_VARARGS | METH_KEYWORDS, NULL },
+
+ /**
+ * Calls rcw_open_from_file_full and returns its result.
+ */
+ { "rcw_open_from_file_full", (PyCFunction)rcw_open_from_file_full_wrapper, METH_VARARGS | METH_KEYWORDS, NULL },
+
+ /**
+ * Calls remmina_public_get_server_port and returns its result.
+ */
+ { "public_get_server_port", (PyCFunction)remmina_public_get_server_port_wrapper, METH_VARARGS | METH_KEYWORDS,
+ NULL },
+
+ /**
+ * Calls remmina_masterthread_exec_is_main_thread and returns its result.
+ */
+ { "masterthread_exec_is_main_thread", remmina_masterthread_exec_is_main_thread_wrapper, METH_VARARGS, NULL },
+
+ /**
+ * Calls remmina_gtksocket_available and returns its result.
+ */
+ { "gtksocket_available", remmina_gtksocket_available_wrapper, METH_VARARGS, NULL },
+
+ /**
+ * Calls remmina_protocol_widget_get_profile_remote_width and returns its result.
+ */
+ { "protocol_widget_get_profile_remote_width",
+ (PyCFunction)remmina_protocol_widget_get_profile_remote_width_wrapper, METH_VARARGS | METH_KEYWORDS, NULL },
+
+ /**
+ * Calls remmina_protocol_widget_get_profile_remote_height and returns its result.
+ */
+ { "protocol_widget_get_profile_remote_height",
+ (PyCFunction)remmina_protocol_widget_get_profile_remote_height_wrapper, METH_VARARGS | METH_KEYWORDS, NULL },
+ { "protocol_plugin_signal_connection_opened", (PyCFunction)remmina_protocol_plugin_signal_connection_opened_wrapper,
+ METH_VARARGS, NULL },
+
+ { "protocol_plugin_signal_connection_closed", (PyCFunction)remmina_protocol_plugin_signal_connection_closed_wrapper,
+ METH_VARARGS, NULL },
+
+ { "protocol_plugin_init_auth", (PyCFunction)remmina_protocol_plugin_init_auth_wrapper, METH_VARARGS | METH_KEYWORDS,
+ NULL },
+
+ /* Sentinel */
+ { NULL }
+};
+
+/**
+ * Adapter struct to handle Remmina protocol settings.
+ */
+typedef struct
+{
+ PyObject_HEAD
+ RemminaProtocolSettingType settingType;
+ gchar* name;
+ gchar* label;
+ gboolean compact;
+ PyObject* opt1;
+ PyObject* opt2;
+} PyRemminaProtocolSetting;
+
+/**
+ * The definition of the Python module 'remmina'.
+ */
+static PyModuleDef remmina_python_module_type = {
+ PyModuleDef_HEAD_INIT,
+ .m_name = "remmina",
+ .m_doc = "Remmina API.",
+ .m_size = -1,
+ .m_methods = remmina_python_module_type_methods
+};
+
+// -- Python Object -> Setting
+
+/**
+ * Initializes the memory and the fields of the remmina.Setting Python type.
+ * @details This function is callback for the Python engine.
+ */
+static PyObject* python_protocol_setting_new(PyTypeObject* type, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ PyRemminaProtocolSetting* self = (PyRemminaProtocolSetting*)type->tp_alloc(type, 0);
+
+ if (!self)
+ {
+ return NULL;
+ }
+
+ self->name = "";
+ self->label = "";
+ self->compact = FALSE;
+ self->opt1 = NULL;
+ self->opt2 = NULL;
+ self->settingType = 0;
+
+ return (PyObject*)self;
+}
+
+/**
+ * Constructor of the remmina.Setting Python type.
+ * @details This function is callback for the Python engine.
+ */
+static int python_protocol_setting_init(PyRemminaProtocolSetting* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static gchar* kwlist[] = { "type", "name", "label", "compact", "opt1", "opt2", NULL };
+ PyObject* name;
+ PyObject* label;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|lOOpOO", kwlist,
+ &self->settingType, &name, &label, &self->compact, &self->opt1, &self->opt2))
+ {
+ return -1;
+ }
+
+ Py_ssize_t len = PyUnicode_GetLength(label);
+ if (len == 0)
+ {
+ self->label = "";
+ }
+ else
+ {
+ self->label = python_wrapper_copy_string_from_python(label, len);
+ if (!self->label)
+ {
+ g_printerr("Unable to extract label during initialization of Python settings module!\n");
+ python_wrapper_check_error();
+ }
+ }
+
+ len = PyUnicode_GetLength(name);
+ if (len == 0)
+ {
+ self->name = "";
+ }
+ else
+ {
+ self->name = python_wrapper_copy_string_from_python(label, len);
+ if (!self->name)
+ {
+ g_printerr("Unable to extract name during initialization of Python settings module!\n");
+ python_wrapper_check_error();
+ }
+ }
+
+ return 0;
+}
+
+static PyMemberDef python_protocol_setting_type_members[] = {
+ { "settingType", offsetof(PyRemminaProtocolSetting, settingType), T_INT, 0, NULL },
+ { "name", offsetof(PyRemminaProtocolSetting, name), T_STRING, 0, NULL },
+ { "label", offsetof(PyRemminaProtocolSetting, label), T_STRING, 0, NULL },
+ { "compact", offsetof(PyRemminaProtocolSetting, compact), T_BOOL, 0, NULL },
+ { "opt1", offsetof(PyRemminaProtocolSetting, opt1), T_OBJECT, 0, NULL },
+ { "opt2", offsetof(PyRemminaProtocolSetting, opt2), T_OBJECT, 0, NULL },
+ { NULL }
+};
+
+static PyTypeObject python_protocol_setting_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "remmina.Setting",
+ .tp_doc = "Remmina Setting information",
+ .tp_basicsize = sizeof(PyRemminaProtocolSetting),
+ .tp_itemsize = 0,
+ .tp_flags = Py_TPFLAGS_DEFAULT,
+ .tp_new = python_protocol_setting_new,
+ .tp_init = (initproc)python_protocol_setting_init,
+ .tp_members = python_protocol_setting_type_members
+};
+
+// -- Python Type -> Feature
+
+
+static PyMemberDef python_protocol_feature_members[] = {
+ { "type", T_INT, offsetof(PyRemminaProtocolFeature, type), 0, NULL },
+ { "id", T_INT, offsetof(PyRemminaProtocolFeature, id), 0, NULL },
+ { "opt1", T_OBJECT, offsetof(PyRemminaProtocolFeature, opt1), 0, NULL },
+ { "opt2", T_OBJECT, offsetof(PyRemminaProtocolFeature, opt2), 0, NULL },
+ { "opt3", T_OBJECT, offsetof(PyRemminaProtocolFeature, opt3), 0, NULL },
+ { NULL }
+};
+
+PyObject* python_protocol_feature_new(PyTypeObject* type, PyObject* kws, PyObject* args)
+{
+ TRACE_CALL(__func__);
+
+ PyRemminaProtocolFeature* self;
+ self = (PyRemminaProtocolFeature*)type->tp_alloc(type, 0);
+ if (!self)
+ return NULL;
+
+ self->id = 0;
+ self->type = 0;
+ self->opt1 = (PyGeneric*)Py_None;
+ self->opt1->raw = NULL;
+ self->opt1->type_hint = REMMINA_TYPEHINT_UNDEFINED;
+ self->opt2 = (PyGeneric*)Py_None;
+ self->opt2->raw = NULL;
+ self->opt2->type_hint = REMMINA_TYPEHINT_UNDEFINED;
+ self->opt3 = (PyGeneric*)Py_None;
+ self->opt3->raw = NULL;
+ self->opt3->type_hint = REMMINA_TYPEHINT_UNDEFINED;
+
+ return (PyObject*)self;
+}
+
+static int python_protocol_feature_init(PyRemminaProtocolFeature* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "type", "id", "opt1", "opt2", "opt3", NULL };
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|llOOO", kwlist, &self->type, &self->id, &self->opt1, &self
+ ->opt2, &self->opt3))
+ return -1;
+
+ return 0;
+}
+
+static PyTypeObject python_protocol_feature_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "remmina.ProtocolFeature",
+ .tp_doc = "Remmina Setting information",
+ .tp_basicsize = sizeof(PyRemminaProtocolFeature),
+ .tp_itemsize = 0,
+ .tp_flags = Py_TPFLAGS_DEFAULT,
+ .tp_new = python_protocol_feature_new,
+ .tp_init = (initproc)python_protocol_feature_init,
+ .tp_members = python_protocol_feature_members
+};
+
+PyRemminaProtocolFeature* python_wrapper_protocol_feature_new(void)
+{
+ PyRemminaProtocolFeature* feature = (PyRemminaProtocolFeature*)PyObject_New(PyRemminaProtocolFeature, &python_protocol_feature_type);
+ feature->id = 0;
+ feature->opt1 = python_wrapper_generic_new();
+ feature->opt1->raw = NULL;
+ feature->opt1->type_hint = REMMINA_TYPEHINT_UNDEFINED;
+ feature->opt2 = python_wrapper_generic_new();
+ feature->opt2->raw = NULL;
+ feature->opt2->type_hint = REMMINA_TYPEHINT_UNDEFINED;
+ feature->opt3 = python_wrapper_generic_new();
+ feature->opt3->raw = NULL;
+ feature->opt3->type_hint = REMMINA_TYPEHINT_UNDEFINED;
+ feature->type = 0;
+ Py_IncRef((PyObject*)feature);
+ return feature;
+}
+
+
+// -- Python Type -> Screenshot Data
+
+static PyMemberDef python_screenshot_data_members[] = {
+ { "buffer", T_OBJECT, offsetof(PyRemminaPluginScreenshotData, buffer), 0, NULL },
+ { "width", T_INT, offsetof(PyRemminaPluginScreenshotData, width), 0, NULL },
+ { "height", T_INT, offsetof(PyRemminaPluginScreenshotData, height), 0, NULL },
+ { "bitsPerPixel", T_INT, offsetof(PyRemminaPluginScreenshotData, bitsPerPixel), 0, NULL },
+ { "bytesPerPixel", T_INT, offsetof(PyRemminaPluginScreenshotData, bytesPerPixel), 0, NULL },
+ { NULL }
+};
+
+PyObject* python_screenshot_data_new(PyTypeObject* type, PyObject* kws, PyObject* args)
+{
+ TRACE_CALL(__func__);
+
+ PyRemminaPluginScreenshotData* self;
+ self = (PyRemminaPluginScreenshotData*)type->tp_alloc(type, 0);
+ if (!self)
+ return NULL;
+
+ self->buffer = (PyByteArrayObject*)PyObject_New(PyByteArrayObject, &PyByteArray_Type);
+ self->height = 0;
+ self->width = 0;
+ self->bitsPerPixel = 0;
+ self->bytesPerPixel = 0;
+
+ return (PyObject*)self;
+}
+
+static int python_screenshot_data_init(PyRemminaPluginScreenshotData* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ g_printerr("Not to be initialized within Python!");
+ return -1;
+}
+
+static PyTypeObject python_screenshot_data_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "remmina.RemminaScreenshotData",
+ .tp_doc = "Remmina Screenshot Data",
+ .tp_basicsize = sizeof(PyRemminaPluginScreenshotData),
+ .tp_itemsize = 0,
+ .tp_flags = Py_TPFLAGS_DEFAULT,
+ .tp_new = python_screenshot_data_new,
+ .tp_init = (initproc)python_screenshot_data_init,
+ .tp_members = python_screenshot_data_members
+};
+PyRemminaPluginScreenshotData* python_wrapper_screenshot_data_new(void)
+{
+ PyRemminaPluginScreenshotData* data = (PyRemminaPluginScreenshotData*)PyObject_New(PyRemminaPluginScreenshotData, &python_screenshot_data_type);
+ data->buffer = PyObject_New(PyByteArrayObject, &PyByteArray_Type);
+ Py_IncRef((PyObject*)data->buffer);
+ data->height = 0;
+ data->width = 0;
+ data->bitsPerPixel = 0;
+ data->bytesPerPixel = 0;
+ return data;
+}
+
+static PyObject* python_wrapper_generic_to_int(PyGeneric* self, PyObject* args);
+static PyObject* python_wrapper_generic_to_bool(PyGeneric* self, PyObject* args);
+static PyObject* python_wrapper_generic_to_string(PyGeneric* self, PyObject* args);
+
+static void python_wrapper_generic_dealloc(PyObject* self)
+{
+ PyObject_Del(self);
+}
+
+static PyMethodDef python_wrapper_generic_methods[] = {
+ { "to_int", (PyCFunction)python_wrapper_generic_to_int, METH_NOARGS, "" },
+ { "to_bool", (PyCFunction)python_wrapper_generic_to_bool, METH_NOARGS, "" },
+ { "to_string", (PyCFunction)python_wrapper_generic_to_string, METH_NOARGS, "" },
+ { NULL }
+};
+
+static PyMemberDef python_wrapper_generic_members[] = {
+ { "raw", T_OBJECT, offsetof(PyGeneric, raw), 0, "" },
+ { NULL }
+};
+
+PyObject* python_wrapper_generic_type_new(PyTypeObject* type, PyObject* kws, PyObject* args)
+{
+ TRACE_CALL(__func__);
+
+ PyGeneric* self;
+ self = (PyGeneric*)type->tp_alloc(type, 0);
+ if (!self)
+ return NULL;
+
+ self->raw = Py_None;
+
+ return (PyObject*)self;
+}
+
+static int python_wrapper_generic_init(PyGeneric* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "raw", NULL };
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", kwlist, &self->raw))
+ return -1;
+
+ return 0;
+}
+
+static PyTypeObject python_generic_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "remmina.Generic",
+ .tp_doc = "",
+ .tp_basicsize = sizeof(PyGeneric),
+ .tp_itemsize = 0,
+ .tp_dealloc = python_wrapper_generic_dealloc,
+ .tp_flags = Py_TPFLAGS_DEFAULT,
+ .tp_new = python_wrapper_generic_type_new,
+ .tp_init = (initproc)python_wrapper_generic_init,
+ .tp_members = python_wrapper_generic_members,
+ .tp_methods = python_wrapper_generic_methods,
+};
+
+PyGeneric* python_wrapper_generic_new(void)
+{
+ PyGeneric* generic = (PyGeneric*)PyObject_New(PyGeneric, &python_generic_type);
+ generic->raw = PyLong_FromLongLong(0LL);
+ Py_IncRef((PyObject*)generic);
+ return generic;
+}
+
+static PyObject* python_wrapper_generic_to_int(PyGeneric* self, PyObject* args)
+{
+ SELF_CHECK();
+
+ if (self->raw == NULL)
+ {
+ return Py_None;
+ }
+ else if (self->type_hint == REMMINA_TYPEHINT_SIGNED)
+ {
+ return PyLong_FromLongLong((long long)self->raw);
+ }
+ else if (self->type_hint == REMMINA_TYPEHINT_UNSIGNED)
+ {
+ return PyLong_FromUnsignedLongLong((unsigned long long)self->raw);
+ }
+
+ return Py_None;
+}
+static PyObject* python_wrapper_generic_to_bool(PyGeneric* self, PyObject* args)
+{
+ SELF_CHECK();
+
+ if (self->raw == NULL)
+ {
+ return Py_None;
+ }
+ else if (self->type_hint == REMMINA_TYPEHINT_BOOLEAN)
+ {
+ return PyBool_FromLong((long)self->raw);
+ }
+
+ return Py_None;
+}
+static PyObject* python_wrapper_generic_to_string(PyGeneric* self, PyObject* args)
+{
+ SELF_CHECK();
+
+ if (self->raw == NULL)
+ {
+ return Py_None;
+ }
+ else if (self->type_hint == REMMINA_TYPEHINT_STRING)
+ {
+ return PyUnicode_FromString((const char*)self->raw);
+ }
+
+ return Py_None;
+}
+
+/**
+ * Is called from the Python engine when it initializes the 'remmina' module.
+ * @details This function is only called by the Python engine!
+ */
+PyMODINIT_FUNC python_wrapper_module_initialize(void)
+{
+ TRACE_CALL(__func__);
+
+ if (PyType_Ready(&python_screenshot_data_type) < 0)
+ {
+ PyErr_Print();
+ return NULL;
+ }
+
+ if (PyType_Ready(&python_generic_type) < 0)
+ {
+ PyErr_Print();
+ return NULL;
+ }
+
+ if (PyType_Ready(&python_protocol_setting_type) < 0)
+ {
+ PyErr_Print();
+ return NULL;
+ }
+
+ if (PyType_Ready(&python_protocol_feature_type) < 0)
+ {
+ PyErr_Print();
+ return NULL;
+ }
+
+ python_wrapper_protocol_widget_type_ready();
+ python_wrapper_remmina_init_types();
+
+ PyObject* module = PyModule_Create(&remmina_python_module_type);
+ if (!module)
+ {
+ PyErr_Print();
+ return NULL;
+ }
+
+ PyModule_AddIntConstant(module, "BUTTONS_CLOSE", (long)GTK_BUTTONS_CLOSE);
+ PyModule_AddIntConstant(module, "BUTTONS_NONE", (long)GTK_BUTTONS_NONE);
+ PyModule_AddIntConstant(module, "BUTTONS_OK", (long)GTK_BUTTONS_OK);
+ PyModule_AddIntConstant(module, "BUTTONS_CLOSE", (long)GTK_BUTTONS_CLOSE);
+ PyModule_AddIntConstant(module, "BUTTONS_CANCEL", (long)GTK_BUTTONS_CANCEL);
+ PyModule_AddIntConstant(module, "BUTTONS_YES_NO", (long)GTK_BUTTONS_YES_NO);
+ PyModule_AddIntConstant(module, "BUTTONS_OK_CANCEL", (long)GTK_BUTTONS_OK_CANCEL);
+
+ PyModule_AddIntConstant(module, "MESSAGE_INFO", (long)GTK_MESSAGE_INFO);
+ PyModule_AddIntConstant(module, "MESSAGE_WARNING", (long)GTK_MESSAGE_WARNING);
+ PyModule_AddIntConstant(module, "MESSAGE_QUESTION", (long)GTK_MESSAGE_QUESTION);
+ PyModule_AddIntConstant(module, "MESSAGE_ERROR", (long)GTK_MESSAGE_ERROR);
+ PyModule_AddIntConstant(module, "MESSAGE_OTHER", (long)GTK_MESSAGE_OTHER);
+
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_SERVER", (long)REMMINA_PROTOCOL_SETTING_TYPE_SERVER);
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_PASSWORD", (long)REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD);
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_RESOLUTION", (long)REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION);
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_ASSISTANCE", (long)REMMINA_PROTOCOL_SETTING_TYPE_ASSISTANCE);
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_KEYMAP", (long)REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP);
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_TEXT", (long)REMMINA_PROTOCOL_SETTING_TYPE_TEXT);
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_SELECT", (long)REMMINA_PROTOCOL_SETTING_TYPE_SELECT);
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_COMBO", (long)REMMINA_PROTOCOL_SETTING_TYPE_COMBO);
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_CHECK", (long)REMMINA_PROTOCOL_SETTING_TYPE_CHECK);
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_FILE", (long)REMMINA_PROTOCOL_SETTING_TYPE_FILE);
+ PyModule_AddIntConstant(module, "PROTOCOL_SETTING_TYPE_FOLDER", (long)REMMINA_PROTOCOL_SETTING_TYPE_FOLDER);
+ PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_MULTIMON", (long)REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON);
+
+ PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_PREF", (long)REMMINA_PROTOCOL_FEATURE_TYPE_PREF);
+ PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_TOOL", (long)REMMINA_PROTOCOL_FEATURE_TYPE_TOOL);
+ PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_UNFOCUS", (long)REMMINA_PROTOCOL_FEATURE_TYPE_UNFOCUS);
+ PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_SCALE", (long)REMMINA_PROTOCOL_FEATURE_TYPE_SCALE);
+ PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_DYNRESUPDATE", (long)REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE);
+ PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_TYPE_GTKSOCKET", (long)REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET);
+
+ PyModule_AddIntConstant(module, "PROTOCOL_SSH_SETTING_NONE", (long)REMMINA_PROTOCOL_SSH_SETTING_NONE);
+ PyModule_AddIntConstant(module, "PROTOCOL_SSH_SETTING_TUNNEL", (long)REMMINA_PROTOCOL_SSH_SETTING_TUNNEL);
+ PyModule_AddIntConstant(module, "PROTOCOL_SSH_SETTING_SSH", (long)REMMINA_PROTOCOL_SSH_SETTING_SSH);
+ PyModule_AddIntConstant(module, "PROTOCOL_SSH_SETTING_REVERSE_TUNNEL", (long)REMMINA_PROTOCOL_SSH_SETTING_REVERSE_TUNNEL);
+ PyModule_AddIntConstant(module, "PROTOCOL_SSH_SETTING_SFTP", (long)REMMINA_PROTOCOL_SSH_SETTING_SFTP);
+
+ PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_PREF_RADIO", (long)REMMINA_PROTOCOL_FEATURE_PREF_RADIO);
+ PyModule_AddIntConstant(module, "PROTOCOL_FEATURE_PREF_CHECK", (long)REMMINA_PROTOCOL_FEATURE_PREF_CHECK);
+
+ PyModule_AddIntConstant(module, "MESSAGE_PANEL_FLAG_USERNAME", REMMINA_MESSAGE_PANEL_FLAG_USERNAME);
+ PyModule_AddIntConstant(module, "MESSAGE_PANEL_FLAG_USERNAME_READONLY", REMMINA_MESSAGE_PANEL_FLAG_USERNAME_READONLY);
+ PyModule_AddIntConstant(module, "MESSAGE_PANEL_FLAG_DOMAIN", REMMINA_MESSAGE_PANEL_FLAG_DOMAIN);
+ PyModule_AddIntConstant(module, "MESSAGE_PANEL_FLAG_SAVEPASSWORD", REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD);
+
+ if (PyModule_AddObject(module, "Setting", (PyObject*)&python_protocol_setting_type) < 0)
+ {
+ Py_DECREF(&python_protocol_setting_type);
+ Py_DECREF(module);
+ PyErr_Print();
+ return NULL;
+ }
+
+ Py_INCREF(&python_protocol_feature_type);
+ if (PyModule_AddObject(module, "Feature", (PyObject*)&python_protocol_feature_type) < 0)
+ {
+ Py_DECREF(&python_protocol_setting_type);
+ Py_DECREF(&python_protocol_feature_type);
+ Py_DECREF(module);
+ PyErr_Print();
+ return NULL;
+ }
+
+ return module;
+}
+
+/**
+ * Initializes all globals and registers the 'remmina' module in the Python engine.
+ * @details This
+ */
+void python_wrapper_module_init(void)
+{
+ TRACE_CALL(__func__);
+
+ if (PyImport_AppendInittab("remmina", python_wrapper_module_initialize))
+ {
+ PyErr_Print();
+ exit(1);
+ }
+
+ python_wrapper_entry_init();
+ python_wrapper_protocol_init();
+ python_wrapper_tool_init();
+ python_wrapper_pref_init();
+ python_wrapper_secret_init();
+ python_wrapper_file_init();
+}
+
+gboolean python_wrapper_check_mandatory_member(PyObject* instance, const gchar* member)
+{
+ TRACE_CALL(__func__);
+
+ if (PyObject_HasAttrString(instance, member))
+ {
+ return TRUE;
+ }
+
+ g_printerr("Missing mandatory member '%s' in Python plugin instance!\n", member);
+ return FALSE;
+}
+
+static PyObject* remmina_register_plugin_wrapper(PyObject* self, PyObject* plugin_instance)
+{
+ TRACE_CALL(__func__);
+
+ if (plugin_instance)
+ {
+ if (!python_wrapper_check_mandatory_member(plugin_instance, "name")
+ || !python_wrapper_check_mandatory_member(plugin_instance, "version"))
+ {
+ return NULL;
+ }
+
+ /* Protocol plugin definition and features */
+ const gchar* pluginType = PyUnicode_AsUTF8(PyObject_GetAttrString(plugin_instance, "type"));
+
+ RemminaPlugin* remmina_plugin = NULL;
+
+ PyPlugin* plugin = (PyPlugin*)python_wrapper_malloc(sizeof(PyPlugin));
+ plugin->instance = plugin_instance;
+ Py_INCREF(plugin_instance);
+ plugin->protocol_plugin = NULL;
+ plugin->entry_plugin = NULL;
+ plugin->file_plugin = NULL;
+ plugin->pref_plugin = NULL;
+ plugin->secret_plugin = NULL;
+ plugin->tool_plugin = NULL;
+ g_print("New Python plugin registered: %ld\n", PyObject_Hash(plugin_instance));
+
+ if (g_str_equal(pluginType, "protocol"))
+ {
+ remmina_plugin = python_wrapper_create_protocol_plugin(plugin);
+ }
+ else if (g_str_equal(pluginType, "entry"))
+ {
+ remmina_plugin = python_wrapper_create_entry_plugin(plugin);
+ }
+ else if (g_str_equal(pluginType, "file"))
+ {
+ remmina_plugin = python_wrapper_create_file_plugin(plugin);
+ }
+ else if (g_str_equal(pluginType, "tool"))
+ {
+ remmina_plugin = python_wrapper_create_tool_plugin(plugin);
+ }
+ else if (g_str_equal(pluginType, "pref"))
+ {
+ remmina_plugin = python_wrapper_create_pref_plugin(plugin);
+ }
+ else if (g_str_equal(pluginType, "secret"))
+ {
+ remmina_plugin = python_wrapper_create_secret_plugin(plugin);
+ }
+ else
+ {
+ g_printerr("Unknown plugin type: %s\n", pluginType);
+ }
+
+ if (remmina_plugin)
+ {
+ if (remmina_plugin->type == REMMINA_PLUGIN_TYPE_PROTOCOL)
+ {
+ plugin->gp = python_wrapper_protocol_widget_create();
+ }
+
+ if (python_wrapper_get_service()->register_plugin((RemminaPlugin*)remmina_plugin)) {
+ g_print("%s: Successfully reigstered!\n", remmina_plugin->name);
+ } else {
+ g_print("%s: Failed to reigster!\n", remmina_plugin->name);
+ }
+ }
+ }
+
+ PyErr_Clear();
+ return Py_None;
+}
+
+static PyObject* remmina_file_get_datadir_wrapper(PyObject* self, PyObject* plugin)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* result = Py_None;
+ const gchar* datadir = python_wrapper_get_service()->file_get_user_datadir();
+
+ if (datadir)
+ {
+ result = PyUnicode_FromFormat("%s", datadir);
+ }
+
+ python_wrapper_check_error();
+ return result;
+}
+
+static PyObject* remmina_file_new_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ RemminaFile* file = python_wrapper_get_service()->file_new();
+ if (file)
+ {
+ return (PyObject*)python_wrapper_remmina_file_to_python(file);
+ }
+
+ python_wrapper_check_error();
+ return Py_None;
+}
+
+static PyObject* remmina_unlock_new_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "window", NULL};
+ GtkWindow* window = NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|0", kwlist, &window))
+ {
+ return Py_None;
+ }
+
+ return PyBool_FromLong(python_wrapper_get_service()->plugin_unlock_new(window));
+}
+
+static PyObject* remmina_pref_set_value_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "key", "value", NULL };
+ gchar* key, * value;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ss", kwlist, &key, &value))
+ {
+ return Py_None;
+ }
+
+ if (key)
+ {
+ python_wrapper_get_service()->pref_set_value(key, value);
+ }
+
+ python_wrapper_check_error();
+ return Py_None;
+}
+
+static PyObject* remmina_pref_get_value_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "key", NULL };
+ gchar* key;
+ PyObject* result = Py_None;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", kwlist, &key))
+ {
+ return Py_None;
+ }
+
+ if (key)
+ {
+ const gchar* value = python_wrapper_get_service()->pref_get_value(key);
+ if (value)
+ {
+ result = PyUnicode_FromFormat("%s", value);
+ }
+ }
+
+ python_wrapper_check_error();
+ return result;
+}
+
+static PyObject* remmina_pref_get_scale_quality_wrapper(PyObject* self, PyObject* plugin)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* result = PyLong_FromLong(python_wrapper_get_service()->pref_get_scale_quality());
+ python_wrapper_check_error();
+ return result;
+}
+
+static PyObject* remmina_pref_get_sshtunnel_port_wrapper(PyObject* self, PyObject* plugin)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* result = PyLong_FromLong(python_wrapper_get_service()->pref_get_sshtunnel_port());
+ python_wrapper_check_error();
+ return result;
+}
+
+static PyObject* remmina_pref_get_ssh_loglevel_wrapper(PyObject* self, PyObject* plugin)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* result = PyLong_FromLong(python_wrapper_get_service()->pref_get_ssh_loglevel());
+ python_wrapper_check_error();
+ return result;
+}
+
+static PyObject* remmina_pref_get_ssh_parseconfig_wrapper(PyObject* self, PyObject* plugin)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* result = PyLong_FromLong(python_wrapper_get_service()->pref_get_ssh_parseconfig());
+ python_wrapper_check_error();
+ return result;
+}
+
+static PyObject* remmina_pref_keymap_get_keyval_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "keymap", "keyval", NULL };
+ gchar* keymap;
+ guint keyval;
+ PyObject* result = Py_None;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sl", kwlist, &keymap, &keyval))
+ {
+ return PyLong_FromLong(-1);
+ }
+
+ if (keymap)
+ {
+ const guint value = python_wrapper_get_service()->pref_keymap_get_keyval(keymap, keyval);
+ result = PyLong_FromUnsignedLong(value);
+ }
+
+ python_wrapper_check_error();
+ return result;
+}
+
+static PyObject* python_wrapper_log_print_wrapper(PyObject* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+
+ gchar* text;
+ if (!PyArg_ParseTuple(args, "s", &text) || !text)
+ {
+ return Py_None;
+ }
+
+ python_wrapper_get_service()->log_print(text);
+ return Py_None;
+}
+
+static PyObject* python_wrapper_debug_wrapper(PyObject* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+
+ gchar* text;
+ if (!PyArg_ParseTuple(args, "s", &text) || !text)
+ {
+ return Py_None;
+ }
+
+ python_wrapper_get_service()->_remmina_debug("python", "%s", text);
+ return Py_None;
+}
+
+static PyObject* remmina_widget_pool_register_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "widget", NULL };
+ PyObject* widget;
+
+ if (PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &widget) && widget)
+ {
+ python_wrapper_get_service()->widget_pool_register(get_pywidget(widget));
+ }
+
+ return Py_None;
+}
+
+static PyObject* rcw_open_from_file_full_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "remminafile", "data", "handler", NULL };
+ PyObject* pyremminafile;
+ PyObject* data;
+
+ if (PyArg_ParseTupleAndKeywords(args, kwargs, "OOO", kwlist, &pyremminafile, &data) && pyremminafile && data)
+ {
+ python_wrapper_get_service()->rcw_open_from_file_full((RemminaFile*)pyremminafile, NULL, (void*)data, NULL);
+ }
+
+ return Py_None;
+}
+
+static PyObject* remmina_public_get_server_port_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "server", "defaultport", NULL };
+ gchar* server;
+ gint defaultport;
+
+ if (PyArg_ParseTupleAndKeywords(args, kwargs, "sl", kwlist, &server, &defaultport) && server)
+ {
+ gchar* host;
+ gint port;
+ python_wrapper_get_service()->get_server_port(server, defaultport, &host, &port);
+
+ PyObject* result = PyList_New(2);
+ PyList_SetItem(result, 0, PyUnicode_FromString(host));
+ PyList_SetItem(result, 1, PyLong_FromLong(port));
+
+ return PyList_AsTuple(result);
+ }
+
+ return Py_None;
+}
+
+static PyObject* remmina_masterthread_exec_is_main_thread_wrapper(PyObject* self, PyObject* plugin)
+{
+ TRACE_CALL(__func__);
+
+ return PyBool_FromLong(python_wrapper_get_service()->is_main_thread());
+}
+
+static PyObject* remmina_gtksocket_available_wrapper(PyObject* self, PyObject* plugin)
+{
+ TRACE_CALL(__func__);
+
+ return PyBool_FromLong(python_wrapper_get_service()->gtksocket_available());
+}
+
+static PyObject*
+remmina_protocol_widget_get_profile_remote_height_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "widget", NULL };
+ PyPlugin* plugin;
+
+ if (PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &plugin) && plugin && plugin->gp)
+ {
+ python_wrapper_get_service()->get_profile_remote_height(plugin->gp->gp);
+ }
+
+ return Py_None;
+}
+
+static PyObject*
+remmina_protocol_widget_get_profile_remote_width_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "widget", NULL };
+ PyPlugin* plugin;
+
+ if (PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &plugin) && plugin && plugin->gp)
+ {
+ python_wrapper_get_service()->get_profile_remote_width(plugin->gp->gp);
+ }
+
+ return Py_None;
+}
+
+void python_wrapper_to_protocol_setting(RemminaProtocolSetting* dest, PyObject* setting)
+{
+ TRACE_CALL(__func__);
+
+ PyRemminaProtocolSetting* src = (PyRemminaProtocolSetting*)setting;
+ Py_INCREF(setting);
+ dest->name = src->name;
+ dest->label = src->label;
+ dest->compact = src->compact;
+ dest->type = src->settingType;
+ dest->validator = NULL;
+ dest->validator_data = NULL;
+ python_wrapper_to_generic(src->opt1, &dest->opt1);
+ python_wrapper_to_generic(src->opt2, &dest->opt2);
+}
+
+void python_wrapper_to_protocol_feature(RemminaProtocolFeature* dest, PyObject* feature)
+{
+ TRACE_CALL(__func__);
+
+ PyRemminaProtocolFeature* src = (PyRemminaProtocolFeature*)feature;
+ Py_INCREF(feature);
+ dest->id = src->id;
+ dest->type = src->type;
+ dest->opt1 = src->opt1->raw;
+ dest->opt1_type_hint = src->opt1->type_hint;
+ dest->opt2 = src->opt2->raw;
+ dest->opt2_type_hint = src->opt2->type_hint;
+ dest->opt3 = src->opt3->raw;
+ dest->opt3_type_hint = src->opt3->type_hint;
+}
+
+PyObject* python_wrapper_show_dialog_wrapper(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+ TRACE_CALL(__func__);
+
+ static char* kwlist[] = { "type", "buttons", "message", NULL };
+ GtkMessageType msgType;
+ GtkButtonsType btnType;
+ gchar* message;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "lls", kwlist, &msgType, &btnType, &message))
+ {
+ return PyLong_FromLong(-1);
+ }
+
+ python_wrapper_get_service()->show_dialog(msgType, btnType, message);
+
+ return Py_None;
+}
+
+PyObject* python_wrapper_get_mainwindow_wrapper(PyObject* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+
+ GtkWindow* result = python_wrapper_get_service()->get_window();
+
+ if (!result)
+ {
+ return Py_None;
+ }
+
+ return (PyObject*)new_pywidget((GObject*)result);
+}
+
+static PyObject* remmina_protocol_plugin_signal_connection_closed_wrapper(PyObject* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* pygp = NULL;
+ if (!PyArg_ParseTuple(args, "O", &pygp) || !pygp)
+ {
+ g_printerr("Please provide the Remmina protocol widget instance!");
+ return Py_None;
+ }
+
+ python_wrapper_get_service()->protocol_plugin_signal_connection_closed(((PyRemminaProtocolWidget*)pygp)->gp);
+ return Py_None;
+}
+
+static PyObject* remmina_protocol_plugin_init_auth_wrapper(PyObject* module, PyObject* args, PyObject* kwds)
+{
+ TRACE_CALL(__func__);
+
+ static gchar* keyword_list[] = { "widget", "flags", "title", "default_username", "default_password",
+ "default_domain", "password_prompt" };
+
+ PyRemminaProtocolWidget* self;
+ gint pflags = 0;
+ gchar* title, * default_username, * default_password, * default_domain, * password_prompt;
+
+ if (PyArg_ParseTupleAndKeywords(args, kwds, "Oisssss", keyword_list, &self, &pflags, &title, &default_username,
+ &default_password, &default_domain, &password_prompt))
+ {
+ if (pflags != 0 && !(pflags & REMMINA_MESSAGE_PANEL_FLAG_USERNAME)
+ && !(pflags & REMMINA_MESSAGE_PANEL_FLAG_USERNAME_READONLY)
+ && !(pflags & REMMINA_MESSAGE_PANEL_FLAG_DOMAIN)
+ && !(pflags & REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD))
+ {
+ g_printerr("panel_auth(pflags, title, default_username, default_password, default_domain, password_prompt): "
+ "%d is not a known value for RemminaMessagePanelFlags!\n", pflags);
+ }
+ else
+ {
+ return Py_BuildValue("i", python_wrapper_get_service()->protocol_widget_panel_auth(self
+ ->gp, pflags, title, default_username, default_password, default_domain, password_prompt));
+ }
+ }
+ else
+ {
+ g_printerr("panel_auth(pflags, title, default_username, default_password, default_domain, password_prompt): Error parsing arguments!\n");
+ PyErr_Print();
+ }
+ return Py_None;
+}
+
+static PyObject* remmina_protocol_plugin_signal_connection_opened_wrapper(PyObject* self, PyObject* args)
+{
+ PyObject* pygp = NULL;
+ if (!PyArg_ParseTuple(args, "O", &pygp) || !pygp)
+ {
+ g_printerr("Please provide the Remmina protocol widget instance!");
+ return Py_None;
+ }
+
+ python_wrapper_get_service()->protocol_plugin_signal_connection_opened(((PyRemminaProtocolWidget*)pygp)->gp);
+ return Py_None;
+}
diff --git a/plugins/python_wrapper/python_wrapper_remmina.h b/plugins/python_wrapper/python_wrapper_remmina.h
new file mode 100644
index 0000000..4b3df13
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_remmina.h
@@ -0,0 +1,93 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_remmina.h
+ *
+ * @brief Contains the implementation of the Python module 'remmina', provided to interface with the application from
+ * the Python plugin source.
+ *
+ * @detail In contrast to the wrapper functions that exist in the plugin specialisation files (e.g.
+ * python_wrapper_protocol.c or python_wrapper_entry.c), this file contains the API for the
+ * communication in the direction, from Python to Remmina. This means, if in the Python plugin a function
+ * is called, that is defined in Remmina, C code, at least in this file, is executed.
+ */
+
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_common.h"
+#include "python_wrapper_protocol_widget.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+G_BEGIN_DECLS
+
+/**
+ * Initializes the 'remmina' module in the Python engine.
+ */
+void python_wrapper_module_init(void);
+
+/**
+ * @brief Returns a pointer to the Python instance, mapped to the RemminaProtocolWidget or null if not found.
+ *
+ * @param gp The widget that is owned by the plugin that should be found.
+ *
+ * @details Remmina expects this callback function to be part of one plugin, which is the reason no instance information
+ * is explicitly passed. To bridge that, this function can be used to retrieve the very plugin instance owning
+ * the given RemminaProtocolWidget.
+ */
+PyPlugin* python_wrapper_module_get_plugin(RemminaProtocolWidget* gp);
+
+/**
+ * @brief Converts the PyObject to RemminaProtocolSetting.
+ *
+ * @param dest A target for the converted value.
+ * @param setting The source value to convert.
+ */
+void python_wrapper_to_protocol_setting(RemminaProtocolSetting* dest, PyObject* setting);
+
+/**
+ * @brief Converts the PyObject to RemminaProtocolFeature.
+ *
+ * @param dest A target for the converted value.
+ * @param setting The source value to convert.
+ */
+void python_wrapper_to_protocol_feature(RemminaProtocolFeature* dest, PyObject* feature);
+
+G_END_DECLS
diff --git a/plugins/python_wrapper/python_wrapper_remmina_file.c b/plugins/python_wrapper/python_wrapper_remmina_file.c
new file mode 100644
index 0000000..001c940
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_remmina_file.c
@@ -0,0 +1,205 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N C L U D E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+#include "remmina/remmina_trace_calls.h"
+#include "python_wrapper_common.h"
+#include "python_wrapper_remmina_file.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// D E C L A R A T I O N S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static PyObject* file_get_path(PyRemminaFile* self, PyObject* args);
+static PyObject* file_set_setting(PyRemminaFile* self, PyObject* args, PyObject* kwds);
+static PyObject* file_get_setting(PyRemminaFile* self, PyObject* args, PyObject* kwds);
+static PyObject* file_get_secret(PyRemminaFile* self, PyObject* setting);
+static PyObject* file_unsave_passwords(PyRemminaFile* self, PyObject* args);
+
+static void file_dealloc(PyObject* self)
+{
+ PyObject_Del(self);
+}
+
+static PyMethodDef python_remmina_file_type_methods[] = {
+ { "get_path", (PyCFunction)file_get_path, METH_NOARGS, "" },
+ { "set_setting", (PyCFunction)file_set_setting, METH_VARARGS | METH_KEYWORDS, "Set file setting" },
+ { "get_setting", (PyCFunction)file_get_setting, METH_VARARGS | METH_KEYWORDS, "Get file setting" },
+ { "get_secret", (PyCFunction)file_get_secret, METH_VARARGS, "Get secret file setting" },
+ { "unsave_passwords", (PyCFunction)file_unsave_passwords, METH_NOARGS, "" },
+ { NULL }
+};
+
+/**
+ * @brief The definition of the Python module 'remmina'.
+ */
+static PyTypeObject python_remmina_file_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ .tp_name = "remmina.RemminaFile",
+ .tp_doc = "",
+ .tp_basicsize = sizeof(PyRemminaFile),
+ .tp_itemsize = 0,
+ .tp_flags = Py_TPFLAGS_DEFAULT,
+ .tp_methods = python_remmina_file_type_methods,
+ .tp_dealloc = file_dealloc
+};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+void python_wrapper_remmina_init_types(void)
+{
+ PyType_Ready(&python_remmina_file_type);
+}
+
+PyRemminaFile* python_wrapper_remmina_file_to_python(RemminaFile* file)
+{
+ TRACE_CALL(__func__);
+
+ PyRemminaFile* result = PyObject_New(PyRemminaFile, &python_remmina_file_type);
+ result->file = file;
+ Py_INCREF(result);
+ return result;
+}
+
+static PyObject* file_get_path(PyRemminaFile* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+
+ return Py_BuildValue("s", python_wrapper_get_service()->file_get_path(self->file));
+}
+
+static PyObject* file_set_setting(PyRemminaFile* self, PyObject* args, PyObject* kwds)
+{
+ TRACE_CALL(__func__);
+
+ static char* keyword_list[] = { "key", "value", NULL };
+ gchar* key;
+ PyObject* value;
+
+ if (PyArg_ParseTupleAndKeywords(args, kwds, "s|O", keyword_list, &key, &value))
+ {
+ if (PyUnicode_Check(value))
+ {
+ python_wrapper_get_service()->file_set_string(self->file, key, PyUnicode_AsUTF8(value));
+ }
+ else if (PyLong_Check(value))
+ {
+ python_wrapper_get_service()->file_set_int(self->file, key, PyLong_AsLong(value));
+ }
+ else
+ {
+ g_printerr("%s: Not a string or int value\n", PyUnicode_AsUTF8(PyObject_Str(value)));
+ }
+ return Py_None;
+ }
+ else
+ {
+ g_printerr("file.set_setting(key, value): Error parsing arguments!\n");
+ PyErr_Print();
+ return NULL;
+ }
+}
+
+static PyObject* file_get_setting(PyRemminaFile* self, PyObject* args, PyObject* kwds)
+{
+ TRACE_CALL(__func__);
+
+ static gchar* keyword_list[] = { "key", "default" };
+ gchar* key;
+ PyObject* def;
+
+ if (PyArg_ParseTupleAndKeywords(args, kwds, "sO", keyword_list, &key, &def))
+ {
+ if (PyUnicode_Check(def))
+ {
+ return Py_BuildValue("s", python_wrapper_get_service()->file_get_string(self->file, key));
+ }
+ else if (PyBool_Check(def))
+ {
+ return python_wrapper_get_service()->file_get_int(self->file, key, (gint)PyLong_AsLong(def)) ? Py_True : Py_False;
+ }
+ else if (PyLong_Check(def))
+ {
+ return Py_BuildValue("i", python_wrapper_get_service()->file_get_int(self->file, key, (gint)PyLong_AsLong(def)));
+ }
+ else
+ {
+ g_printerr("%s: Not a string or int value\n", PyUnicode_AsUTF8(PyObject_Str(def)));
+ }
+ return def;
+ }
+ else
+ {
+ g_printerr("file.get_setting(key, default): Error parsing arguments!\n");
+ PyErr_Print();
+ return Py_None;
+ }
+}
+
+static PyObject* file_get_secret(PyRemminaFile* self, PyObject* key)
+{
+ TRACE_CALL(__func__);
+
+ if (key && PyUnicode_Check(key))
+ {
+ return Py_BuildValue("s", python_wrapper_get_service()->file_get_secret(self->file, PyUnicode_AsUTF8(key)));
+ }
+ else
+ {
+ g_printerr("file.get_secret(key): Error parsing arguments!\n");
+ PyErr_Print();
+ return NULL;
+ }
+}
+
+static PyObject* file_unsave_passwords(PyRemminaFile* self, PyObject* args)
+{
+ TRACE_CALL(__func__);
+
+ if (self)
+ {
+ python_wrapper_get_service()->file_unsave_passwords(self->file);
+ return Py_None;
+ }
+ else
+ {
+ g_printerr("unsave_passwords(): self is null!\n");
+ return NULL;
+ }
+}
diff --git a/plugins/python_wrapper/python_wrapper_remmina_file.h b/plugins/python_wrapper/python_wrapper_remmina_file.h
new file mode 100644
index 0000000..2899ae0
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_remmina_file.h
@@ -0,0 +1,63 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+
+/**
+ * @file python_wrapper_protocol.h
+ *
+ * @brief Contains the implementation of the Python type remmina.RemminaFile.
+ */
+
+#pragma once
+
+#include "python_wrapper_common.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Wrapper for a Python object that contains a pointer to an instance of RemminaFile.
+ */
+typedef struct
+{
+ PyObject_HEAD
+ RemminaFile* file;
+} PyRemminaFile;
+
+void python_wrapper_remmina_init_types(void);
+
+/**
+ * Converts the instance of RemminaFile to a Python object that can be passed to the Python engine.
+ */
+PyRemminaFile* python_wrapper_remmina_file_to_python(RemminaFile* file);
diff --git a/plugins/python_wrapper/python_wrapper_secret.c b/plugins/python_wrapper/python_wrapper_secret.c
new file mode 100644
index 0000000..1f34176
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_secret.c
@@ -0,0 +1,138 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2021 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N L U C E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_common.h"
+#include "python_wrapper_secret.h"
+#include "python_wrapper_remmina_file.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+void python_wrapper_secret_init(void)
+{
+ TRACE_CALL(__func__);
+}
+
+gboolean python_wrapper_secret_init_wrapper(RemminaSecretPlugin* instance)
+{
+ TRACE_CALL(__func__);
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+ PyObject* result = CallPythonMethod(plugin->instance, "init", NULL);
+ return result == Py_None || result != Py_False;
+}
+
+gboolean python_wrapper_secret_is_service_available_wrapper(RemminaSecretPlugin* instance)
+{
+ TRACE_CALL(__func__);
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+ PyObject* result = CallPythonMethod(plugin->instance, "is_service_available", NULL);
+ return result == Py_None || result != Py_False;
+}
+
+void
+python_wrapper_secret_store_password_wrapper(RemminaSecretPlugin* instance, RemminaFile* file, const gchar* key, const gchar* password)
+{
+ TRACE_CALL(__func__);
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+ CallPythonMethod(plugin
+ ->instance, "store_password", "Oss", (PyObject*)python_wrapper_remmina_file_to_python(file), key, password);
+}
+
+gchar*
+python_wrapper_secret_get_password_wrapper(RemminaSecretPlugin* instance, RemminaFile* file, const gchar* key)
+{
+ TRACE_CALL(__func__);
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+ PyObject* result = CallPythonMethod(plugin
+ ->instance, "get_password", "Os", (PyObject*)python_wrapper_remmina_file_to_python(file), key);
+ Py_ssize_t len = PyUnicode_GetLength(result);
+ if (len == 0)
+ {
+ return NULL;
+ }
+
+ return python_wrapper_copy_string_from_python(result, len);
+}
+
+void
+python_wrapper_secret_delete_password_wrapper(RemminaSecretPlugin* instance, RemminaFile* file, const gchar* key)
+{
+ TRACE_CALL(__func__);
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+ CallPythonMethod(plugin
+ ->instance, "delete_password", "Os", (PyObject*)python_wrapper_remmina_file_to_python(file), key);
+}
+
+RemminaPlugin* python_wrapper_create_secret_plugin(PyPlugin* plugin)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* instance = plugin->instance;
+
+ if (!python_wrapper_check_attribute(instance, ATTR_NAME))
+ {
+ return NULL;
+ }
+
+ RemminaSecretPlugin* remmina_plugin = (RemminaSecretPlugin*)python_wrapper_malloc(sizeof(RemminaSecretPlugin));
+
+ remmina_plugin->type = REMMINA_PLUGIN_TYPE_SECRET;
+ remmina_plugin->domain = GETTEXT_PACKAGE;
+ remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME));
+ remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION));
+ remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION));
+ remmina_plugin->init_order = PyLong_AsLong(PyObject_GetAttrString(instance, ATTR_INIT_ORDER));
+
+ remmina_plugin->init = python_wrapper_secret_init_wrapper;
+ remmina_plugin->is_service_available = python_wrapper_secret_is_service_available_wrapper;
+ remmina_plugin->store_password = python_wrapper_secret_store_password_wrapper;
+ remmina_plugin->get_password = python_wrapper_secret_get_password_wrapper;
+ remmina_plugin->delete_password = python_wrapper_secret_delete_password_wrapper;
+
+ plugin->secret_plugin = remmina_plugin;
+ plugin->generic_plugin = (RemminaPlugin*)remmina_plugin;
+
+ python_wrapper_add_plugin(plugin);
+
+ return (RemminaPlugin*)remmina_plugin;
+} \ No newline at end of file
diff --git a/plugins/python_wrapper/python_wrapper_secret.h b/plugins/python_wrapper/python_wrapper_secret.h
new file mode 100644
index 0000000..b40e058
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_secret.h
@@ -0,0 +1,68 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2021 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_entry.h
+ *
+ * @brief Contains the specialisation of RemminaPluginEntry plugins in Python.
+ */
+
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N L U C E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_common.h"
+#include "remmina/plugin.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+G_BEGIN_DECLS
+
+/**
+ * Initializes the Python plugin specialisation for secret plugins.
+ */
+void python_wrapper_secret_init(void);
+
+/**
+ * @brief Creates a new instance of the RemminaPluginSecret, initializes its members and references the wrapper
+ * functions.
+ * @param instance The instance of the Python plugin.
+ * @return Returns a new instance of the RemminaPlugin (must be freed!).
+ */
+RemminaPlugin* python_wrapper_create_secret_plugin(PyPlugin* instance);
+
+G_END_DECLS
diff --git a/plugins/python_wrapper/python_wrapper_tool.c b/plugins/python_wrapper/python_wrapper_tool.c
new file mode 100644
index 0000000..db721b6
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_tool.c
@@ -0,0 +1,84 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2021 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N L U C E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "python_wrapper_common.h"
+#include "python_wrapper_tool.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+void python_wrapper_tool_init(void)
+{
+ TRACE_CALL(__func__);
+}
+
+void python_wrapper_tool_exec_func_wrapper(GtkMenuItem* self, RemminaToolPlugin* instance)
+{
+ TRACE_CALL(__func__);
+
+ PyPlugin* plugin = python_wrapper_get_plugin(instance->name);
+ CallPythonMethod(plugin->instance, "exec_func", NULL);
+}
+
+RemminaPlugin* python_wrapper_create_tool_plugin(PyPlugin* plugin)
+{
+ TRACE_CALL(__func__);
+
+ PyObject* instance = plugin->instance;
+
+ if (!python_wrapper_check_attribute(instance, ATTR_NAME))
+ {
+ return NULL;
+ }
+
+ RemminaToolPlugin* remmina_plugin = (RemminaToolPlugin*)python_wrapper_malloc(sizeof(RemminaToolPlugin));
+
+ remmina_plugin->type = REMMINA_PLUGIN_TYPE_TOOL;
+ remmina_plugin->domain = GETTEXT_PACKAGE;
+ remmina_plugin->name = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_NAME));
+ remmina_plugin->version = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_VERSION));
+ remmina_plugin->description = PyUnicode_AsUTF8(PyObject_GetAttrString(instance, ATTR_DESCRIPTION));
+ remmina_plugin->exec_func = python_wrapper_tool_exec_func_wrapper;
+
+ plugin->tool_plugin = remmina_plugin;
+ plugin->generic_plugin = (RemminaPlugin*)remmina_plugin;
+
+ python_wrapper_add_plugin(plugin);
+
+ return (RemminaPlugin*)remmina_plugin;
+}
diff --git a/plugins/python_wrapper/python_wrapper_tool.h b/plugins/python_wrapper/python_wrapper_tool.h
new file mode 100644
index 0000000..595e7b3
--- /dev/null
+++ b/plugins/python_wrapper/python_wrapper_tool.h
@@ -0,0 +1,69 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2014-2021 Antenore Gatta, Giovanni Panozzo, Mathias Winterhalter (ToolsDevler)
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ */
+
+/**
+ * @file python_wrapper_entry.h
+ *
+ * @brief Contains the specialisation of RemminaPluginEntry plugins in Python.
+ */
+
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// I N L U C E S
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "remmina/plugin.h"
+#include "python_wrapper_common.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// A P I
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+G_BEGIN_DECLS
+
+/**
+ * Initializes the Python plugin specialisation for tool plugins.
+ */
+void python_wrapper_tool_init(void);
+
+/**
+ * @brief Creates a new instance of the RemminaPluginTool, initializes its members and references the wrapper
+ * functions.
+ * @param instance The instance of the Python plugin.
+ * @return Returns a new instance of the RemminaPlugin (must be freed!).
+ */
+RemminaPlugin* python_wrapper_create_tool_plugin(PyPlugin* instance);
+
+G_END_DECLS
+
diff --git a/plugins/rdp/CMakeLists.txt b/plugins/rdp/CMakeLists.txt
new file mode 100644
index 0000000..da4798b
--- /dev/null
+++ b/plugins/rdp/CMakeLists.txt
@@ -0,0 +1,88 @@
+# remmina-plugin-rdp - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2011 Marc-Andre Moreau
+# Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. If you
+# do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+
+set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
+find_package(Threads REQUIRED)
+
+find_suggested_package(Cups)
+
+set(REMMINA_PLUGIN_RDP_SRCS
+ rdp_plugin.c
+ rdp_plugin.h
+ rdp_event.c
+ rdp_event.h
+ rdp_file.c
+ rdp_file.h
+ rdp_settings.c
+ rdp_settings.h
+ rdp_graphics.c
+ rdp_graphics.h
+ rdp_cliprdr.c
+ rdp_cliprdr.h
+ rdp_monitor.c
+ rdp_monitor.h
+ rdp_channels.c
+ rdp_channels.h
+ )
+
+add_definitions(-DFREERDP_REQUIRED_MAJOR=${FREERDP_REQUIRED_MAJOR})
+add_definitions(-DFREERDP_REQUIRED_MINOR=${FREERDP_REQUIRED_MINOR})
+add_definitions(-DFREERDP_REQUIRED_REVISION=${FREERDP_REQUIRED_REVISION})
+
+add_library(remmina-plugin-rdp MODULE ${REMMINA_PLUGIN_RDP_SRCS})
+set_target_properties(remmina-plugin-rdp PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-rdp PROPERTIES NO_SONAME 1)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${FreeRDP-Client_INCLUDE_DIR} ${FreeRDP_INCLUDE_DIR} ${WinPR_INCLUDE_DIR})
+target_link_libraries(remmina-plugin-rdp
+ ${REMMINA_COMMON_LIBRARIES} freerdp-client)
+
+if(CUPS_FOUND)
+ add_definitions(-DHAVE_CUPS)
+ include_directories(${CUPS_INCLUDE_DIR})
+ target_link_libraries(remmina-plugin-rdp
+ ${REMMINA_COMMON_LIBRARIES} freerdp-client ${CUPS_LIBRARIES})
+endif()
+
+install(TARGETS remmina-plugin-rdp DESTINATION ${REMMINA_PLUGINDIR})
+
+install(FILES
+ scalable/emblems/org.remmina.Remmina-rdp-ssh-symbolic.svg
+ scalable/emblems/org.remmina.Remmina-rdp-symbolic.svg
+ DESTINATION ${APPICONSCALE_EMBLEMS_DIR})
+
+if(WITH_ICON_CACHE)
+ gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor")
+endif()
diff --git a/plugins/rdp/rdp_channels.c b/plugins/rdp/rdp_channels.c
new file mode 100644
index 0000000..fabb25f
--- /dev/null
+++ b/plugins/rdp/rdp_channels.c
@@ -0,0 +1,96 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2012-2012 Jean-Louis Dupond
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "rdp_plugin.h"
+#include "rdp_cliprdr.h"
+#include "rdp_channels.h"
+#include "rdp_event.h"
+
+#include <freerdp/freerdp.h>
+#include <freerdp/channels/channels.h>
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/gdi/gfx.h>
+
+void remmina_rdp_OnChannelConnectedEventHandler(void *context, CONST_ARG ChannelConnectedEventArgs *e)
+{
+ TRACE_CALL(__func__);
+
+ rfContext* rfi = (rfContext*)context;
+
+ if (g_strcmp0(e->name, RDPEI_DVC_CHANNEL_NAME) == 0) {
+ g_print("Unimplemented: channel %s connected but we can’t use it\n", e->name);
+ // xfc->rdpei = (RdpeiClientContext*) e->pInterface;
+ }else if (g_strcmp0(e->name, TSMF_DVC_CHANNEL_NAME) == 0) {
+ g_print("Unimplemented: channel %s connected but we can’t use it\n", e->name);
+ // xf_tsmf_init(xfc, (TsmfClientContext*) e->pInterface);
+ }else if (g_strcmp0(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) {
+ if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SoftwareGdi)) {
+ rfi->rdpgfxchan = TRUE;
+ gdi_graphics_pipeline_init(rfi->clientContext.context.gdi, (RdpgfxClientContext*) e->pInterface);
+ }
+ else
+ g_print("Unimplemented: channel %s connected but libfreerdp is in HardwareGdi mode\n", e->name);
+ }else if (g_strcmp0(e->name, RAIL_SVC_CHANNEL_NAME) == 0) {
+ g_print("Unimplemented: channel %s connected but we can’t use it\n", e->name);
+ // xf_rail_init(xfc, (RailClientContext*) e->pInterface);
+ }else if (g_strcmp0(e->name, CLIPRDR_SVC_CHANNEL_NAME) == 0) {
+ remmina_rdp_cliprdr_init( rfi, (CliprdrClientContext*)e->pInterface);
+ }else if (g_strcmp0(e->name, ENCOMSP_SVC_CHANNEL_NAME) == 0) {
+ g_print("Unimplemented: channel %s connected but we can’t use it\n", e->name);
+ // xf_encomsp_init(xfc, (EncomspClientContext*) e->pInterface);
+ }else if (g_strcmp0(e->name, DISP_DVC_CHANNEL_NAME) == 0) {
+ // "disp" channel connected, save its context pointer
+ rfi->dispcontext = (DispClientContext*)e->pInterface;
+ // Notify rcw to unlock dynres capability
+ remmina_plugin_service->protocol_plugin_unlock_dynres(rfi->protocol_widget);
+ // Send monitor layout message here to ask for resize of remote desktop now
+ if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES) {
+ remmina_rdp_event_send_delayed_monitor_layout(rfi->protocol_widget);
+ }
+ }
+ REMMINA_PLUGIN_DEBUG("Channel %s has been opened", e->name);
+}
+
+void remmina_rdp_OnChannelDisconnectedEventHandler(void *context, CONST_ARG ChannelDisconnectedEventArgs *e) {
+ TRACE_CALL(__func__);
+ rfContext* rfi = (rfContext*)context;
+
+ if (strcmp(e->name, RDPGFX_DVC_CHANNEL_NAME) == 0) {
+ if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SoftwareGdi))
+ gdi_graphics_pipeline_uninit(rfi->clientContext.context.gdi, (RdpgfxClientContext*) e->pInterface);
+ }
+ REMMINA_PLUGIN_DEBUG("Channel %s has been closed", e->name);
+
+}
diff --git a/plugins/rdp/rdp_channels.h b/plugins/rdp/rdp_channels.h
new file mode 100644
index 0000000..f614610
--- /dev/null
+++ b/plugins/rdp/rdp_channels.h
@@ -0,0 +1,62 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2012-2012 Jean-Louis Dupond
+ * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+
+#pragma once
+
+#include <glib.h>
+#include <freerdp/version.h>
+#include <freerdp/freerdp.h>
+#include <freerdp/client/channels.h>
+#include <freerdp/client/rdpei.h>
+#include <freerdp/client/tsmf.h>
+#include <freerdp/client/rail.h>
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/client/rdpgfx.h>
+#include <freerdp/client/encomsp.h>
+
+G_BEGIN_DECLS
+
+#if FREERDP_VERSION_MAJOR >= 3
+#define CONST_ARG const
+#else
+#define CONST_ARG
+#endif
+
+void remmina_rdp_OnChannelConnectedEventHandler(void *context, CONST_ARG ChannelConnectedEventArgs *e);
+void remmina_rdp_OnChannelDisconnectedEventHandler(void *context, CONST_ARG ChannelDisconnectedEventArgs *e);
+
+
+G_END_DECLS
diff --git a/plugins/rdp/rdp_cliprdr.c b/plugins/rdp/rdp_cliprdr.c
new file mode 100644
index 0000000..d42aa35
--- /dev/null
+++ b/plugins/rdp/rdp_cliprdr.c
@@ -0,0 +1,1034 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2012-2012 Jean-Louis Dupond
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "rdp_plugin.h"
+#include "rdp_cliprdr.h"
+#include "rdp_event.h"
+
+#include <freerdp/freerdp.h>
+#include <freerdp/channels/channels.h>
+#include <freerdp/client/cliprdr.h>
+#include <sys/time.h>
+
+#define CLIPBOARD_TRANSFER_WAIT_TIME 6
+
+#define CB_FORMAT_HTML 0xD010
+#define CB_FORMAT_PNG 0xD011
+#define CB_FORMAT_JPEG 0xD012
+#define CB_FORMAT_GIF 0xD013
+#define CB_FORMAT_TEXTURILIST 0xD014
+
+UINT32 remmina_rdp_cliprdr_get_format_from_gdkatom(GdkAtom atom)
+{
+ TRACE_CALL(__func__);
+ UINT32 rc;
+ gchar *name = gdk_atom_name(atom);
+ rc = 0;
+ if (g_strcmp0("UTF8_STRING", name) == 0 || g_strcmp0("text/plain;charset=utf-8", name) == 0)
+ rc = CF_UNICODETEXT;
+ if (g_strcmp0("TEXT", name) == 0 || g_strcmp0("text/plain", name) == 0)
+ rc = CF_TEXT;
+ if (g_strcmp0("text/html", name) == 0)
+ rc = CB_FORMAT_HTML;
+ if (g_strcmp0("image/png", name) == 0)
+ rc = CB_FORMAT_PNG;
+ if (g_strcmp0("image/jpeg", name) == 0)
+ rc = CB_FORMAT_JPEG;
+ if (g_strcmp0("image/bmp", name) == 0)
+ rc = CF_DIB;
+ if (g_strcmp0("text/uri-list", name) == 0)
+ rc = CB_FORMAT_TEXTURILIST;
+ g_free(name);
+ return rc;
+}
+
+/* Never used? */
+void remmina_rdp_cliprdr_get_target_types(UINT32 **formats, UINT16 *size, GdkAtom *types, int count)
+{
+ TRACE_CALL(__func__);
+ int i;
+ *size = 1;
+ *formats = (UINT32 *)malloc(sizeof(UINT32) * (count + 1));
+
+ *formats[0] = 0;
+ for (i = 0; i < count; i++) {
+ UINT32 format = remmina_rdp_cliprdr_get_format_from_gdkatom(types[i]);
+ if (format != 0) {
+ (*formats)[*size] = format;
+ (*size)++;
+ }
+ }
+
+ *formats = realloc(*formats, sizeof(UINT32) * (*size));
+}
+
+static UINT8 *lf2crlf(UINT8 *data, int *size)
+{
+ TRACE_CALL(__func__);
+ UINT8 c;
+ UINT8 *outbuf;
+ UINT8 *out;
+ UINT8 *in_end;
+ UINT8 *in;
+ int out_size;
+
+ out_size = (*size) * 2 + 1;
+ outbuf = (UINT8 *)malloc(out_size);
+ out = outbuf;
+ in = data;
+ in_end = data + (*size);
+
+ while (in < in_end) {
+ c = *in++;
+ if (c == '\n') {
+ *out++ = '\r';
+ *out++ = '\n';
+ } else {
+ *out++ = c;
+ }
+ }
+
+ *out++ = 0;
+ *size = out - outbuf;
+
+ return outbuf;
+}
+
+static void crlf2lf(UINT8 *data, size_t *size)
+{
+ TRACE_CALL(__func__);
+ UINT8 c;
+ UINT8 *out;
+ UINT8 *in;
+ UINT8 *in_end;
+
+ out = data;
+ in = data;
+ in_end = data + (*size);
+
+ while (in < in_end) {
+ c = *in++;
+ if (c != '\r')
+ *out++ = c;
+ }
+
+ *size = out - data;
+}
+
+/* Never used? */
+int remmina_rdp_cliprdr_server_file_contents_request(CliprdrClientContext *context, CLIPRDR_FILE_CONTENTS_REQUEST *fileContentsRequest)
+{
+ TRACE_CALL(__func__);
+ return -1;
+}
+
+/* Never used? */
+int remmina_rdp_cliprdr_server_file_contents_response(CliprdrClientContext *context, CLIPRDR_FILE_CONTENTS_RESPONSE *fileContentsResponse)
+{
+ TRACE_CALL(__func__);
+ return 1;
+}
+
+void remmina_rdp_cliprdr_send_client_format_list(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginRdpUiObject *ui;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ rfClipboard *clipboard;
+ CLIPRDR_FORMAT_LIST *pFormatList;
+ RemminaPluginRdpEvent rdp_event = { 0 };
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting)
+ return;
+
+ clipboard = &(rfi->clipboard);
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_CLIPBOARD;
+ ui->clipboard.clipboard = clipboard;
+ ui->clipboard.type = REMMINA_RDP_UI_CLIPBOARD_FORMATLIST;
+ pFormatList = remmina_rdp_event_queue_ui_sync_retptr(gp, ui);
+
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_LIST;
+ rdp_event.clipboard_formatlist.pFormatList = pFormatList;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+}
+
+static void remmina_rdp_cliprdr_send_client_capabilities(rfClipboard *clipboard)
+{
+ TRACE_CALL(__func__);
+ CLIPRDR_CAPABILITIES capabilities;
+ CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet;
+
+ capabilities.cCapabilitiesSets = 1;
+ capabilities.capabilitySets = (CLIPRDR_CAPABILITY_SET *)&(generalCapabilitySet);
+
+ generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL;
+ generalCapabilitySet.capabilitySetLength = 12;
+
+ generalCapabilitySet.version = CB_CAPS_VERSION_2;
+ generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES;
+
+ clipboard->context->ClientCapabilities(clipboard->context, &capabilities);
+}
+
+static UINT remmina_rdp_cliprdr_monitor_ready(CliprdrClientContext *context, const CLIPRDR_MONITOR_READY *monitorReady)
+{
+ TRACE_CALL(__func__);
+ rfClipboard *clipboard = (rfClipboard *)context->custom;
+ RemminaProtocolWidget *gp;
+
+ remmina_rdp_cliprdr_send_client_capabilities(clipboard);
+ gp = clipboard->rfi->protocol_widget;
+ remmina_rdp_cliprdr_send_client_format_list(gp);
+
+ return CHANNEL_RC_OK;
+}
+
+static UINT remmina_rdp_cliprdr_server_capabilities(CliprdrClientContext *context, const CLIPRDR_CAPABILITIES *capabilities)
+{
+ TRACE_CALL(__func__);
+ return CHANNEL_RC_OK;
+}
+
+
+void remmina_rdp_cliprdr_cached_clipboard_free(rfClipboard *clipboard)
+{
+ TRACE_CALL(__func__);
+ guint fmt;
+
+ pthread_mutex_lock(&clipboard->srv_data_mutex);
+ if (clipboard->srv_data != NULL) {
+ fmt = clipboard->format;
+ if (fmt == CB_FORMAT_PNG || fmt == CF_DIB || fmt == CF_DIBV5 || fmt == CB_FORMAT_JPEG) {
+ g_object_unref(clipboard->srv_data);
+ } else {
+ free(clipboard->srv_data);
+ }
+ clipboard->srv_data = NULL;
+ }
+ pthread_mutex_unlock(&clipboard->srv_data_mutex);
+}
+
+
+static UINT remmina_rdp_cliprdr_server_format_list(CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST *formatList)
+{
+ TRACE_CALL(__func__);
+
+ /* Called when a user do a "Copy" on the server: we collect all formats
+ * the server send us and then setup the local clipboard with the appropriate
+ * targets to request server data */
+
+ RemminaPluginRdpUiObject *ui;
+ RemminaProtocolWidget *gp;
+ rfClipboard *clipboard;
+ CLIPRDR_FORMAT *format;
+ CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse;
+ UINT rc;
+
+ int has_dib_level = 0;
+
+ int i;
+
+ clipboard = (rfClipboard *)context->custom;
+ gp = clipboard->rfi->protocol_widget;
+
+ REMMINA_PLUGIN_DEBUG("gp=%p: Received a new ServerFormatList from server clipboard. Remmina version = %s",
+ gp, VERSION);
+
+ GtkTargetList *list = gtk_target_list_new(NULL, 0);
+
+ if (clipboard->srv_clip_data_wait == SCDW_BUSY_WAIT) {
+ REMMINA_PLUGIN_DEBUG("gp=%p: we already have a FormatDataRequest in progress to the server, aborting", gp);
+ remmina_rdp_clipboard_abort_client_format_data_request(clipboard->rfi);
+ }
+
+ remmina_rdp_cliprdr_cached_clipboard_free(clipboard);
+ clipboard->server_html_format_id = 0;
+
+ REMMINA_PLUGIN_DEBUG("gp=%p: format list from the server:", gp);
+ for (i = 0; i < formatList->numFormats; i++) {
+ format = &formatList->formats[i];
+ const char *serverFormatName = format->formatName;
+ gchar *gtkFormatName = NULL;
+ if (format->formatId == CF_UNICODETEXT) {
+ serverFormatName = "CF_UNICODETEXT";
+ gtkFormatName = "text/plain;charset=utf-8";
+ GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
+ gtk_target_list_add(list, atom, 0, CF_UNICODETEXT);
+ /* Add also the older UTF8_STRING format for older applications */
+ atom = gdk_atom_intern("UTF8_STRING", TRUE);
+ gtk_target_list_add(list, atom, 0, CF_UNICODETEXT);
+ } else if (format->formatId == CF_TEXT) {
+ serverFormatName = "CF_TEXT";
+ gtkFormatName = "text/plain";
+ GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
+ gtk_target_list_add(list, atom, 0, CF_TEXT);
+ /* Add also the older TEXT format for older applications */
+ atom = gdk_atom_intern("TEXT", TRUE);
+ gtk_target_list_add(list, atom, 0, CF_TEXT);
+ } else if (format->formatId == CF_DIB) {
+ serverFormatName = "CF_DIB";
+ if (has_dib_level < 1)
+ has_dib_level = 1;
+ } else if (format->formatId == CF_DIBV5) {
+ serverFormatName = "CF_DIBV5";
+ if (has_dib_level < 5)
+ has_dib_level = 5;
+ } else if (format->formatId == CB_FORMAT_JPEG) {
+ serverFormatName = "CB_FORMAT_JPEG";
+ gtkFormatName = "image/jpeg";
+ GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
+ gtk_target_list_add(list, atom, 0, CB_FORMAT_JPEG);
+ } else if (format->formatId == CB_FORMAT_PNG) {
+ serverFormatName = "CB_FORMAT_PNG";
+ gtkFormatName = "image/png";
+ GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
+ gtk_target_list_add(list, atom, 0, CB_FORMAT_PNG);
+ } else if (format->formatId == CB_FORMAT_HTML) {
+ serverFormatName = "CB_FORMAT_HTML";
+ gtkFormatName = "text/html";
+ GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
+ gtk_target_list_add(list, atom, 0, CB_FORMAT_HTML);
+ } else if (format->formatId == CB_FORMAT_TEXTURILIST) {
+ serverFormatName = "CB_FORMAT_TEXTURILIST";
+ gtkFormatName = "text/uri-list";
+ GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
+ gtk_target_list_add(list, atom, 0, CB_FORMAT_TEXTURILIST);
+ } else if (format->formatId == CF_LOCALE) {
+ serverFormatName = "CF_LOCALE";
+ } else if (format->formatId == CF_METAFILEPICT) {
+ serverFormatName = "CF_METAFILEPICT";
+ } else if (serverFormatName != NULL && strcmp(serverFormatName, "HTML Format") == 0) {
+ gtkFormatName = "text/html";
+ GdkAtom atom = gdk_atom_intern(gtkFormatName, TRUE);
+ gtk_target_list_add(list, atom, 0, format->formatId);
+ clipboard->server_html_format_id = format->formatId;
+ }
+ REMMINA_PLUGIN_DEBUG("the server has clipboard format %d: %s -> GTK %s", format->formatId, serverFormatName, gtkFormatName);
+ }
+
+ /* Keep only one DIB format, if present */
+ if (has_dib_level) {
+ GdkAtom atom = gdk_atom_intern("image/bmp", TRUE);
+ if (has_dib_level == 5)
+ gtk_target_list_add(list, atom, 0, CF_DIBV5);
+ else
+ gtk_target_list_add(list, atom, 0, CF_DIB);
+ }
+
+
+ REMMINA_PLUGIN_DEBUG("gp=%p: sending ClientFormatListResponse to server", gp);
+#if FREERDP_VERSION_MAJOR >= 3
+ formatListResponse.common.msgType = CB_FORMAT_LIST_RESPONSE;
+ formatListResponse.common.msgFlags = CB_RESPONSE_OK; // Can be CB_RESPONSE_FAIL in case of error
+ formatListResponse.common.dataLen = 0;
+#else
+ formatListResponse.msgType = CB_FORMAT_LIST_RESPONSE;
+ formatListResponse.msgFlags = CB_RESPONSE_OK; // Can be CB_RESPONSE_FAIL in case of error
+ formatListResponse.dataLen = 0;
+#endif
+ rc = clipboard->context->ClientFormatListResponse(clipboard->context, &formatListResponse);
+ /* Schedule GTK event to tell GTK to change the local clipboard calling gtk_clipboard_set_with_owner
+ * via REMMINA_RDP_UI_CLIPBOARD_SET_DATA
+ * GTK will immediately fire an "owner-change" event, that we should ignore.
+ * And if we are putting text on the clipboard, mutter or other clipboard
+ * managers may try immediately request a clipboard transfer. */
+
+ /* Ensure we have at least one target into list, or Gtk will not change owner
+ when setting clipboard owner later.
+ We put it directly in the clipboard cache (clipboard->srv_data),
+ so remmina will never ask it to the server via ClientFormatDataRequest */
+ gint n_targets;
+ GtkTargetEntry *target_table = gtk_target_table_new_from_list(list, &n_targets);
+ if (target_table)
+ gtk_target_table_free(target_table, n_targets);
+ if (n_targets == 0) {
+ REMMINA_PLUGIN_DEBUG("gp=%p adding a dummy text target (empty text) for local clipboard, because we have no interesting targets from the server. Putting it in the local clipboard cache.");
+ GdkAtom atom = gdk_atom_intern("text/plain;charset=utf-8", TRUE);
+ gtk_target_list_add(list, atom, 0, CF_UNICODETEXT);
+ pthread_mutex_lock(&clipboard->srv_data_mutex);
+ clipboard->srv_data = malloc(1);
+ ((char *)(clipboard->srv_data))[0] = 0;
+ pthread_mutex_unlock(&clipboard->srv_data_mutex);
+ clipboard->format = CF_UNICODETEXT;
+ }
+
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_CLIPBOARD;
+ ui->clipboard.clipboard = clipboard;
+ ui->clipboard.type = REMMINA_RDP_UI_CLIPBOARD_SET_DATA;
+ ui->clipboard.targetlist = list;
+ remmina_rdp_event_queue_ui_async(gp, ui);
+
+ REMMINA_PLUGIN_DEBUG("gp=%p: processing of ServerFormatList ended, returning rc=%u to libfreerdp", gp, rc);
+ return rc;
+}
+
+static UINT remmina_rdp_cliprdr_server_format_list_response(CliprdrClientContext *context, const CLIPRDR_FORMAT_LIST_RESPONSE *formatListResponse)
+{
+ TRACE_CALL(__func__);
+ return CHANNEL_RC_OK;
+}
+
+static UINT remmina_rdp_cliprdr_server_format_data_request(CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_REQUEST *formatDataRequest)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginRdpUiObject *ui;
+ RemminaProtocolWidget *gp;
+ rfClipboard *clipboard;
+
+ clipboard = (rfClipboard *)context->custom;
+ gp = clipboard->rfi->protocol_widget;
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_CLIPBOARD;
+ ui->clipboard.clipboard = clipboard;
+ ui->clipboard.type = REMMINA_RDP_UI_CLIPBOARD_GET_DATA;
+ ui->clipboard.format = formatDataRequest->requestedFormatId;
+ remmina_rdp_event_queue_ui_sync_retint(gp, ui);
+
+ return CHANNEL_RC_OK;
+}
+
+int timeval_diff(struct timeval *start, struct timeval *end)
+{
+ /* Returns the time elapsed from start to end, in ms */
+ int64_t ms_end = (end->tv_sec * 1000) + (end->tv_usec / 1000);
+ int64_t ms_start = (start->tv_sec * 1000) + (start->tv_usec / 1000);
+
+ return (int)(ms_end - ms_start);
+}
+
+
+static UINT remmina_rdp_cliprdr_server_format_data_response(CliprdrClientContext *context, const CLIPRDR_FORMAT_DATA_RESPONSE *formatDataResponse)
+{
+ TRACE_CALL(__func__);
+
+ const UINT8 *data;
+ size_t size;
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+ rfClipboard *clipboard;
+ gpointer output = NULL;
+ struct timeval now;
+ int mstrans;
+
+ clipboard = (rfClipboard *)context->custom;
+ gp = clipboard->rfi->protocol_widget;
+ rfi = GET_PLUGIN_DATA(gp);
+
+ data = formatDataResponse->requestedFormatData;
+#if FREERDP_VERSION_MAJOR >= 3
+ size = formatDataResponse->common.dataLen;
+#else
+ size = formatDataResponse->dataLen;
+#endif
+
+ REMMINA_PLUGIN_DEBUG("gp=%p server FormatDataResponse received: clipboard data arrived form server.", gp);
+ gettimeofday(&now, NULL);
+ remmina_rdp_cliprdr_cached_clipboard_free(clipboard);
+
+ /* Calculate stats */
+ mstrans = timeval_diff(&(clipboard->clientformatdatarequest_tv), &now);
+ REMMINA_PLUGIN_DEBUG("gp=%p %zu bytes transferred from server in %d ms. Speed is %d bytes/sec",
+ gp, (size_t)size, mstrans, mstrans != 0 ? (int)((int64_t)size * 1000 / mstrans) : 0);
+
+ if (size > 0) {
+ switch (rfi->clipboard.format) {
+ case CF_UNICODETEXT:
+ {
+ output =
+ g_utf16_to_utf8((const WCHAR *)data, size / sizeof(WCHAR), NULL, NULL, NULL);
+ if (output) {
+ size = strlen(output) + 1;
+ crlf2lf(output, &size);
+ }
+ break;
+ }
+
+ case CF_TEXT:
+ case CB_FORMAT_HTML:
+ {
+ output = (gpointer)calloc(1, size + 1);
+ if (output) {
+ memcpy(output, data, size);
+ crlf2lf(output, &size);
+ }
+ break;
+ }
+
+ case CF_DIBV5:
+ case CF_DIB:
+ {
+ wStream *s;
+ UINT32 offset;
+ GError *perr;
+ BITMAPINFOHEADER *pbi;
+ BITMAPV5HEADER *pbi5;
+
+ pbi = (BITMAPINFOHEADER *)data;
+
+ // offset calculation inspired by http://downloads.poolelan.com/MSDN/MSDNLibrary6/Disk1/Samples/VC/OS/WindowsXP/GetImage/BitmapUtil.cpp
+ offset = 14 + pbi->biSize;
+ if (pbi->biClrUsed != 0)
+ offset += sizeof(RGBQUAD) * pbi->biClrUsed;
+ else if (pbi->biBitCount <= 8)
+ offset += sizeof(RGBQUAD) * (1 << pbi->biBitCount);
+ if (pbi->biSize == sizeof(BITMAPINFOHEADER)) {
+ if (pbi->biCompression == 3) // BI_BITFIELDS is 3
+ offset += 12;
+ } else if (pbi->biSize >= sizeof(BITMAPV5HEADER)) {
+ pbi5 = (BITMAPV5HEADER *)pbi;
+ if (pbi5->bV5ProfileData <= offset)
+ offset += pbi5->bV5ProfileSize;
+ }
+ s = Stream_New(NULL, 14 + size);
+ Stream_Write_UINT8(s, 'B');
+ Stream_Write_UINT8(s, 'M');
+ Stream_Write_UINT32(s, 14 + size);
+ Stream_Write_UINT32(s, 0);
+ Stream_Write_UINT32(s, offset);
+ Stream_Write(s, data, size);
+
+ data = Stream_Buffer(s);
+ size = Stream_Length(s);
+
+ GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
+ perr = NULL;
+ if (!gdk_pixbuf_loader_write(loader, data, size, &perr)) {
+ Stream_Free(s, TRUE);
+ g_warning("[RDP] rdp_cliprdr: gdk_pixbuf_loader_write() returned error %s\n", perr->message);
+ } else {
+ if (!gdk_pixbuf_loader_close(loader, &perr)) {
+ g_warning("[RDP] rdp_cliprdr: gdk_pixbuf_loader_close() returned error %s\n", perr->message);
+ perr = NULL;
+ }
+ Stream_Free(s, TRUE);
+ output = g_object_ref(gdk_pixbuf_loader_get_pixbuf(loader));
+ }
+ g_object_unref(loader);
+ break;
+ }
+
+ case CB_FORMAT_PNG:
+ case CB_FORMAT_JPEG:
+ {
+ GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
+ gdk_pixbuf_loader_write(loader, data, size, NULL);
+ output = g_object_ref(gdk_pixbuf_loader_get_pixbuf(loader));
+ gdk_pixbuf_loader_close(loader, NULL);
+ g_object_unref(loader);
+ break;
+ }
+ default:
+ {
+ if (rfi->clipboard.format == clipboard->server_html_format_id) {
+ /* Converting from Microsoft HTML Clipboard Format to pure text/html
+ * https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format */
+ int p = 0, lstart = 0;
+ size_t osize;
+ char c;
+ /* Find the 1st starting with '<' */
+ while(p < size && (c = data[p]) != 0) {
+ if (c == '<' && p == lstart) break;
+ if (c == '\n') lstart = p + 1;
+ p++;
+ }
+ if (p < size) {
+ osize = size - lstart;
+ output = (gpointer)calloc(1, osize + 1);
+ if (output) {
+ memcpy(output, data + lstart, osize);
+ ((char *)output)[osize] = 0;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ pthread_mutex_lock(&clipboard->srv_data_mutex);
+ clipboard->srv_data = output;
+ pthread_mutex_unlock(&clipboard->srv_data_mutex);
+
+ if (output != NULL)
+ REMMINA_PLUGIN_DEBUG("gp=%p: clipboard local cache data has been loaded", gp);
+ else
+ REMMINA_PLUGIN_DEBUG("gp=%p: data from server is not valid (size=%zu format=%d), cannot load into cache", gp, size, rfi->clipboard.format);
+
+
+ REMMINA_PLUGIN_DEBUG("gp=%p: signalling main GTK thread that we have some clipboard data.", gp);
+
+ pthread_mutex_lock(&clipboard->transfer_clip_mutex);
+ pthread_cond_signal(&clipboard->transfer_clip_cond);
+ if (clipboard->srv_clip_data_wait == SCDW_BUSY_WAIT) {
+ REMMINA_PLUGIN_DEBUG("gp=%p: clipboard transfer from server completed.", gp);
+ } else {
+ // Clipboard data arrived from server when we are not busy waiting on main loop
+ // Unfortunately, we must discard it
+ REMMINA_PLUGIN_DEBUG("gp=%p: clipboard transfer from server completed, but no local application is requesting it. Data is on local cache now, try to paste later.", gp);
+ }
+ clipboard->srv_clip_data_wait = SCDW_NONE;
+ pthread_mutex_unlock(&clipboard->transfer_clip_mutex);
+
+ return CHANNEL_RC_OK;
+}
+
+
+void remmina_rdp_cliprdr_request_data(GtkClipboard *gtkClipboard, GtkSelectionData *selection_data, guint info, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ /* Called by GTK when someone presses "Paste" on the client side.
+ * We ask to the server the data we need */
+
+ CLIPRDR_FORMAT_DATA_REQUEST *pFormatDataRequest;
+ rfClipboard *clipboard;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaPluginRdpEvent rdp_event = { 0 };
+ struct timespec to;
+ struct timeval tv;
+ int rc;
+ time_t tlimit, tlimit1s, tnow, tstart;
+
+ REMMINA_PLUGIN_DEBUG("gp=%p: A local application has requested remote clipboard data for remote format id %d", gp, info);
+
+ clipboard = &(rfi->clipboard);
+ if (clipboard->srv_clip_data_wait != SCDW_NONE) {
+ g_message("[RDP] Cannot paste now, I’m already transferring clipboard data from server. Try again later\n");
+ return;
+ }
+
+ if (clipboard->format != info || clipboard->srv_data == NULL) {
+ /* We do not have a local cached clipoard, so we have to start a remote request */
+ remmina_rdp_cliprdr_cached_clipboard_free(clipboard);
+
+ clipboard->format = info;
+
+ pthread_mutex_lock(&clipboard->transfer_clip_mutex);
+
+ pFormatDataRequest = (CLIPRDR_FORMAT_DATA_REQUEST *)malloc(sizeof(CLIPRDR_FORMAT_DATA_REQUEST));
+ ZeroMemory(pFormatDataRequest, sizeof(CLIPRDR_FORMAT_DATA_REQUEST));
+ pFormatDataRequest->requestedFormatId = clipboard->format;
+
+ clipboard->srv_clip_data_wait = SCDW_BUSY_WAIT; // Annotate that we are waiting for ServerFormatDataResponse
+
+ REMMINA_PLUGIN_DEBUG("gp=%p Requesting clipboard data with format %d from the server via ServerFormatDataRequest", gp, clipboard->format);
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_REQUEST;
+ rdp_event.clipboard_formatdatarequest.pFormatDataRequest = pFormatDataRequest;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+
+ /* Busy wait clipboard data for CLIPBOARD_TRANSFER_WAIT_TIME seconds.
+ * In the meanwhile allow GTK event loop to proceed */
+
+ tstart = time(NULL);
+ tlimit = tstart + CLIPBOARD_TRANSFER_WAIT_TIME;
+ rc = 100000;
+ tlimit1s = tstart + 1;
+ while ((tnow = time(NULL)) < tlimit && rc != 0 && clipboard->srv_clip_data_wait == SCDW_BUSY_WAIT) {
+
+ if (tnow >= tlimit1s) {
+ REMMINA_PLUGIN_DEBUG("gp=%p, clipboard data is still not here after %u seconds", gp, (unsigned)(tnow - tstart));
+ tlimit1s = time(NULL) + 1;
+ }
+
+ gettimeofday(&tv, NULL);
+ to.tv_sec = tv.tv_sec;
+ to.tv_nsec = tv.tv_usec * 1000 + 5000000; // wait for data for 5ms
+ if (to.tv_nsec >= 1000000000) {
+ to.tv_nsec -= 1000000000;
+ to.tv_sec++;
+ }
+ rc = pthread_cond_timedwait(&clipboard->transfer_clip_cond, &clipboard->transfer_clip_mutex, &to);
+ if (rc == 0)
+ break;
+
+ gtk_main_iteration_do(FALSE);
+ }
+
+ if (rc != 0) {
+ /* Timeout, just log it and hope that data will arrive later */
+ if (clipboard->srv_clip_data_wait == SCDW_ABORTING) {
+ g_warning("[RDP] gp=%p Clipboard data wait aborted.",gp);
+ } else {
+ if (rc == ETIMEDOUT)
+ g_warning("[RDP] gp=%p Clipboard data from the server is not available in %d seconds. No data will be available to user.",
+ gp, CLIPBOARD_TRANSFER_WAIT_TIME);
+ else
+ g_warning("[RDP] gp=%p internal error: pthread_cond_timedwait() returned %d\n", gp, rc);
+ }
+ }
+ pthread_mutex_unlock(&clipboard->transfer_clip_mutex);
+
+ }
+
+ pthread_mutex_lock(&clipboard->srv_data_mutex);
+ if (clipboard->srv_data != NULL) {
+ REMMINA_PLUGIN_DEBUG("gp=%p pasting data to local application", gp);
+ /* We have data in cache, just paste it */
+ if (info == CB_FORMAT_PNG || info == CF_DIB || info == CF_DIBV5 || info == CB_FORMAT_JPEG) {
+ gtk_selection_data_set_pixbuf(selection_data, clipboard->srv_data);
+ } else if (info == CB_FORMAT_HTML || info == clipboard->server_html_format_id) {
+ REMMINA_PLUGIN_DEBUG("gp=%p returning %zu bytes of HTML in clipboard to requesting application", gp, strlen(clipboard->srv_data));
+ GdkAtom atom = gdk_atom_intern("text/html", TRUE);
+ gtk_selection_data_set(selection_data, atom, 8, clipboard->srv_data, strlen(clipboard->srv_data));
+ } else {
+ REMMINA_PLUGIN_DEBUG("gp=%p returning %zu bytes of text in clipboard to requesting application", gp, strlen(clipboard->srv_data));
+ gtk_selection_data_set_text(selection_data, clipboard->srv_data, -1);
+ }
+ clipboard->srv_clip_data_wait = SCDW_NONE;
+ } else {
+ REMMINA_PLUGIN_DEBUG("gp=%p cannot paste data to local application because ->srv_data is NULL", gp);
+ }
+ pthread_mutex_unlock(&clipboard->srv_data_mutex);
+
+}
+
+void remmina_rdp_cliprdr_empty_clipboard(GtkClipboard *gtkClipboard, rfClipboard *clipboard)
+{
+ TRACE_CALL(__func__);
+ /* No need to do anything here */
+}
+
+CLIPRDR_FORMAT_LIST *remmina_rdp_cliprdr_get_client_format_list(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GtkClipboard *gtkClipboard;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ GdkAtom *targets;
+ gboolean result = 0;
+ gint loccount, srvcount;
+ gint formatId, i;
+ CLIPRDR_FORMAT *formats;
+ struct retp_t {
+ CLIPRDR_FORMAT_LIST pFormatList;
+ CLIPRDR_FORMAT formats[];
+ } *retp;
+
+ formats = NULL;
+ retp = NULL;
+ loccount = 0;
+
+ gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD);
+ if (gtkClipboard)
+ result = gtk_clipboard_wait_for_targets(gtkClipboard, &targets, &loccount);
+ REMMINA_PLUGIN_DEBUG("gp=%p sending to server the following local clipboard content formats", gp);
+ if (result && loccount > 0) {
+ formats = (CLIPRDR_FORMAT *)malloc(loccount * sizeof(CLIPRDR_FORMAT));
+ srvcount = 0;
+ for (i = 0; i < loccount; i++) {
+ formatId = remmina_rdp_cliprdr_get_format_from_gdkatom(targets[i]);
+ if (formatId != 0) {
+ gchar *name = gdk_atom_name(targets[i]);
+ REMMINA_PLUGIN_DEBUG(" local clipboard format %s will be sent to remote as %d", name, formatId);
+ g_free(name);
+ formats[srvcount].formatId = formatId;
+ formats[srvcount].formatName = NULL;
+ srvcount++;
+ }
+ }
+ if (srvcount > 0) {
+ retp = (struct retp_t *)malloc(sizeof(struct retp_t) + sizeof(CLIPRDR_FORMAT) * srvcount);
+ retp->pFormatList.formats = retp->formats;
+ retp->pFormatList.numFormats = srvcount;
+ memcpy(retp->formats, formats, sizeof(CLIPRDR_FORMAT) * srvcount);
+ } else {
+ retp = (struct retp_t *)malloc(sizeof(struct retp_t));
+ retp->pFormatList.formats = NULL;
+ retp->pFormatList.numFormats = 0;
+ }
+ free(formats);
+ } else {
+ retp = (struct retp_t *)malloc(sizeof(struct retp_t) + sizeof(CLIPRDR_FORMAT));
+ retp->pFormatList.formats = NULL;
+ retp->pFormatList.numFormats = 0;
+ }
+
+ if (result)
+ g_free(targets);
+
+#if FREERDP_VERSION_MAJOR >= 3
+ retp->pFormatList.common.msgType = CB_FORMAT_LIST;
+ retp->pFormatList.common.msgFlags = 0;
+#else
+ retp->pFormatList.msgType = CB_FORMAT_LIST;
+ retp->pFormatList.msgFlags = 0;
+#endif
+
+ return (CLIPRDR_FORMAT_LIST *)retp;
+}
+
+static void remmina_rdp_cliprdr_mt_get_format_list(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ ui->retptr = (void *)remmina_rdp_cliprdr_get_client_format_list(gp);
+}
+
+void remmina_rdp_cliprdr_get_clipboard_data(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ GtkClipboard *gtkClipboard;
+ UINT8 *inbuf = NULL;
+ UINT8 *outbuf = NULL;
+#if FREERDP_VERSION_MAJOR >= 3
+ WCHAR *outbuf_wchar = NULL;
+#endif
+ GdkPixbuf *image = NULL;
+ size_t size = 0;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaPluginRdpEvent rdp_event = { 0 };
+
+ gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD);
+ if (gtkClipboard) {
+ switch (ui->clipboard.format) {
+ case CF_TEXT:
+ case CF_UNICODETEXT:
+ case CB_FORMAT_HTML:
+ {
+ inbuf = (UINT8 *)gtk_clipboard_wait_for_text(gtkClipboard);
+ break;
+ }
+
+ case CB_FORMAT_PNG:
+ case CB_FORMAT_JPEG:
+ case CF_DIB:
+ case CF_DIBV5:
+ {
+ image = gtk_clipboard_wait_for_image(gtkClipboard);
+ break;
+ }
+ }
+ }
+
+ /* No data received, send nothing */
+ if (inbuf != NULL || image != NULL) {
+ switch (ui->clipboard.format) {
+ case CF_TEXT:
+ case CB_FORMAT_HTML:
+ {
+ size = strlen((char *)inbuf);
+ outbuf = lf2crlf(inbuf, (int *) &size);
+ break;
+ }
+ case CF_UNICODETEXT:
+ {
+ size = strlen((const char *)inbuf);
+ inbuf = lf2crlf(inbuf, (int *) &size);
+#if FREERDP_VERSION_MAJOR >= 3
+ size_t len = 0;
+ outbuf_wchar = ConvertUtf8NToWCharAlloc((const char *)inbuf, (size_t)size, &len);
+ size = (len + 1) * sizeof(WCHAR);
+#else
+ const int rc = (ConvertToUnicode(CP_UTF8, 0, (CHAR *)inbuf, -1, (WCHAR **)&outbuf, 0)) * sizeof(WCHAR);
+ size = 0;
+ if (rc >= 0) {
+ size = (size_t)rc;
+ }
+#endif
+ g_free(inbuf);
+ break;
+ }
+ case CB_FORMAT_PNG:
+ {
+ gchar *data;
+ gsize buffersize;
+ gdk_pixbuf_save_to_buffer(image, &data, &buffersize, "png", NULL, NULL);
+ outbuf = (UINT8 *)malloc(buffersize);
+ memcpy(outbuf, data, buffersize);
+ size = buffersize;
+ g_object_unref(image);
+ break;
+ }
+ case CB_FORMAT_JPEG:
+ {
+ gchar *data;
+ gsize buffersize;
+ gdk_pixbuf_save_to_buffer(image, &data, &buffersize, "jpeg", NULL, NULL);
+ outbuf = (UINT8 *)malloc(buffersize);
+ memcpy(outbuf, data, buffersize);
+ size = buffersize;
+ g_object_unref(image);
+ break;
+ }
+ case CF_DIB:
+ case CF_DIBV5:
+ {
+ gchar *data;
+ gsize buffersize;
+ gdk_pixbuf_save_to_buffer(image, &data, &buffersize, "bmp", NULL, NULL);
+ size = buffersize - 14;
+ outbuf = (UINT8 *)malloc(size);
+ memcpy(outbuf, data + 14, size);
+ g_object_unref(image);
+ break;
+ }
+ }
+ }
+
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_RESPONSE;
+ rdp_event.clipboard_formatdataresponse.size = (int)MIN(size, INT32_MAX);
+
+#if FREERDP_VERSION_MAJOR >= 3
+ // For unicode, use the wchar buffer
+ if (outbuf == NULL && outbuf_wchar != NULL) {
+ rdp_event.clipboard_formatdataresponse.data = (unsigned char *)outbuf_wchar;
+ }
+ else {
+ rdp_event.clipboard_formatdataresponse.data = (unsigned char *)outbuf;
+ }
+#else
+ rdp_event.clipboard_formatdataresponse.data = (unsigned char *)outbuf;
+#endif
+
+ remmina_rdp_event_event_push(gp, &rdp_event);
+}
+
+void remmina_rdp_cliprdr_set_clipboard_data(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ GtkClipboard *gtkClipboard;
+ gint n_targets;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD);
+ if (gtkClipboard) {
+ GtkTargetEntry *targets = gtk_target_table_new_from_list(ui->clipboard.targetlist, &n_targets);
+ if (!targets) {
+ /* If no targets exists, this is an internal error, because
+ * remmina_rdp_cliprdr_server_format_list() must have produced
+ * at least one target before calling REMMINA_RDP_UI_CLIPBOARD_SET_DATA */
+ g_warning("[RDP] internal error: no targets to insert into the local clipboard");
+ }
+
+ REMMINA_PLUGIN_DEBUG("setting clipboard with owner to me: %p", gp);
+ gtk_clipboard_set_with_owner(gtkClipboard, targets, n_targets,
+ (GtkClipboardGetFunc)remmina_rdp_cliprdr_request_data,
+ (GtkClipboardClearFunc)remmina_rdp_cliprdr_empty_clipboard, G_OBJECT(gp));
+ gtk_target_table_free(targets, n_targets);
+ }
+}
+
+void remmina_rdp_cliprdr_detach_owner(RemminaProtocolWidget *gp)
+{
+ /* When closing a rdp connection, we should check if gp is a clipboard owner.
+ * If it’s an owner, detach it from the clipboard */
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ GtkClipboard *gtkClipboard;
+
+ if (!rfi || !rfi->drawing_area) return;
+
+ gtkClipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD);
+ if (gtkClipboard && gtk_clipboard_get_owner(gtkClipboard) == (GObject *)gp)
+ gtk_clipboard_clear(gtkClipboard);
+
+}
+
+void remmina_rdp_event_process_clipboard(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+
+ switch (ui->clipboard.type) {
+ case REMMINA_RDP_UI_CLIPBOARD_FORMATLIST:
+ remmina_rdp_cliprdr_mt_get_format_list(gp, ui);
+ break;
+
+ case REMMINA_RDP_UI_CLIPBOARD_GET_DATA:
+ remmina_rdp_cliprdr_get_clipboard_data(gp, ui);
+ break;
+
+ case REMMINA_RDP_UI_CLIPBOARD_SET_DATA:
+ remmina_rdp_cliprdr_set_clipboard_data(gp, ui);
+ break;
+
+ }
+}
+
+void remmina_rdp_clipboard_init(rfContext *rfi)
+{
+ TRACE_CALL(__func__);
+ // Future: initialize rfi->clipboard
+}
+void remmina_rdp_clipboard_free(rfContext *rfi)
+{
+ TRACE_CALL(__func__);
+
+ remmina_rdp_cliprdr_cached_clipboard_free(&(rfi->clipboard));
+
+}
+
+void remmina_rdp_clipboard_abort_client_format_data_request(rfContext *rfi)
+{
+ TRACE_CALL(__func__);
+ if (rfi && rfi->clipboard.srv_clip_data_wait == SCDW_BUSY_WAIT) {
+ REMMINA_PLUGIN_DEBUG("requesting clipboard data transfer from server to be ignored and busywait loop to exit");
+ /* Allow clipboard transfer from server to terminate */
+ rfi->clipboard.srv_clip_data_wait = SCDW_ABORTING;
+ usleep(100000);
+ }
+}
+
+void remmina_rdp_cliprdr_init(rfContext *rfi, CliprdrClientContext *cliprdr)
+{
+ TRACE_CALL(__func__);
+
+ rfClipboard *clipboard;
+ clipboard = &(rfi->clipboard);
+
+ rfi->clipboard.rfi = rfi;
+ cliprdr->custom = (void *)clipboard;
+
+ clipboard->context = cliprdr;
+ pthread_mutex_init(&clipboard->transfer_clip_mutex, NULL);
+ pthread_cond_init(&clipboard->transfer_clip_cond, NULL);
+ clipboard->srv_clip_data_wait = SCDW_NONE;
+
+ pthread_mutex_init(&clipboard->srv_data_mutex, NULL);
+
+ cliprdr->MonitorReady = remmina_rdp_cliprdr_monitor_ready;
+ cliprdr->ServerCapabilities = remmina_rdp_cliprdr_server_capabilities;
+ cliprdr->ServerFormatList = remmina_rdp_cliprdr_server_format_list;
+ cliprdr->ServerFormatListResponse = remmina_rdp_cliprdr_server_format_list_response;
+ cliprdr->ServerFormatDataRequest = remmina_rdp_cliprdr_server_format_data_request;
+ cliprdr->ServerFormatDataResponse = remmina_rdp_cliprdr_server_format_data_response;
+
+// cliprdr->ServerFileContentsRequest = remmina_rdp_cliprdr_server_file_contents_request;
+// cliprdr->ServerFileContentsResponse = remmina_rdp_cliprdr_server_file_contents_response;
+}
diff --git a/plugins/rdp/rdp_cliprdr.h b/plugins/rdp/rdp_cliprdr.h
new file mode 100644
index 0000000..56b58a8
--- /dev/null
+++ b/plugins/rdp/rdp_cliprdr.h
@@ -0,0 +1,51 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2012-2012 Jean-Louis Dupond
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+
+#include <freerdp/freerdp.h>
+#include "rdp_plugin.h"
+
+void remmina_rdp_clipboard_init(rfContext *rfi);
+void remmina_rdp_clipboard_free(rfContext *rfi);
+void remmina_rdp_cliprdr_init(rfContext *rfi, CliprdrClientContext *cliprdr);
+void remmina_rdp_channel_cliprdr_process(RemminaProtocolWidget *gp, wMessage *event);
+void remmina_rdp_event_process_clipboard(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui);
+CLIPRDR_FORMAT_LIST *remmina_rdp_cliprdr_get_client_format_list(RemminaProtocolWidget *gp);
+void remmina_rdp_cliprdr_detach_owner(RemminaProtocolWidget *gp);
+void remmina_rdp_clipboard_abort_client_format_data_request(rfContext *rfi);
diff --git a/plugins/rdp/rdp_event.c b/plugins/rdp/rdp_event.c
new file mode 100644
index 0000000..19b349c
--- /dev/null
+++ b/plugins/rdp/rdp_event.c
@@ -0,0 +1,1494 @@
+/*
+ * Remmina - The GTK Remote Desktop Client
+ * Copyright (C) 2010 Jay Sorg
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com>
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2022 Antenore Gatta, Giovanni Panozzo
+ * Copyright (C) 2022-2023 Antenore Gatta, Giovanni Panozzo, Hiroyuki Tanaka
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "rdp_cliprdr.h"
+#include "rdp_event.h"
+#include "rdp_monitor.h"
+#include "rdp_settings.h"
+#include <gdk/gdkkeysyms.h>
+#ifdef GDK_WINDOWING_X11
+#include <cairo/cairo-xlib.h>
+#else
+#include <cairo/cairo.h>
+#endif
+#include <freerdp/locale/keyboard.h>
+
+gboolean remmina_rdp_event_on_map(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ rdpGdi *gdi;
+
+ if (rfi == NULL)
+ return false;
+
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ int do_suppress = !remmina_plugin_service->file_get_int(remminafile, "no-suppress", FALSE);
+
+ if (do_suppress) {
+ gdi = ((rdpContext *)rfi)->gdi;
+
+ REMMINA_PLUGIN_DEBUG("Map event received, disabling TS_SUPPRESS_OUTPUT_PDU ");
+ gdi_send_suppress_output(gdi, FALSE);
+ }
+
+ return FALSE;
+}
+
+gboolean remmina_rdp_event_on_unmap(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ rdpGdi *gdi;
+
+ if (rfi == NULL)
+ return false;
+
+ GtkWidget *toplevel = gtk_widget_get_toplevel(GTK_WIDGET(gp));
+ GdkWindow *window = gtk_widget_get_window(toplevel);
+
+ if (gdk_window_get_fullscreen_mode(window) == GDK_FULLSCREEN_ON_ALL_MONITORS) {
+ REMMINA_PLUGIN_DEBUG("Unmap event received, but cannot enable TS_SUPPRESS_OUTPUT_PDU when in fullscreen");
+ return FALSE;
+ }
+
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ int do_suppress = !remmina_plugin_service->file_get_int(remminafile, "no-suppress", FALSE);
+
+ if (do_suppress) {
+ gdi = ((rdpContext *)rfi)->gdi;
+
+ REMMINA_PLUGIN_DEBUG("Unmap event received, enabling TS_SUPPRESS_OUTPUT_PDU ");
+ gdi_send_suppress_output(gdi, TRUE);
+ }
+
+ return FALSE;
+}
+
+static gboolean remmina_rdp_event_on_focus_in(GtkWidget *widget, GdkEventKey *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ rdpInput *input;
+ GdkModifierType state;
+
+#if GTK_CHECK_VERSION(3, 20, 0)
+ GdkSeat *seat;
+#else
+ GdkDeviceManager *manager;
+#endif
+ GdkDevice *keyboard = NULL;
+
+ const gchar *wname = gtk_widget_get_name(gtk_widget_get_toplevel(widget));
+ REMMINA_PLUGIN_DEBUG("Top level name is: %s", wname);
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting)
+ return FALSE;
+
+ input = rfi->clientContext.context.input;
+ UINT32 toggle_keys_state = 0;
+
+#if GTK_CHECK_VERSION(3, 20, 0)
+ seat = gdk_display_get_default_seat(gdk_display_get_default());
+ keyboard = gdk_seat_get_pointer(seat);
+#else
+ manager = gdk_display_get_device_manager(gdk_display_get_default());
+ keyboard = gdk_device_manager_get_client_pointer(manager);
+#endif
+ gdk_window_get_device_position(gdk_get_default_root_window(), keyboard, NULL, NULL, &state);
+
+ if (state & GDK_LOCK_MASK)
+ toggle_keys_state |= KBD_SYNC_CAPS_LOCK;
+ if (state & GDK_MOD2_MASK)
+ toggle_keys_state |= KBD_SYNC_NUM_LOCK;
+ if (state & GDK_MOD5_MASK)
+ toggle_keys_state |= KBD_SYNC_SCROLL_LOCK;
+
+ input->SynchronizeEvent(input, toggle_keys_state);
+ input->KeyboardEvent(input, KBD_FLAGS_RELEASE, 0x0F);
+
+ return FALSE;
+}
+
+void remmina_rdp_event_event_push(RemminaProtocolWidget *gp, const RemminaPluginRdpEvent *e)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaPluginRdpEvent *event;
+
+ /* Called by the main GTK thread to send an event to the libfreerdp thread */
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting)
+ return;
+
+ if (rfi->event_queue) {
+#if GLIB_CHECK_VERSION(2,67,3)
+ event = g_memdup2(e, sizeof(RemminaPluginRdpEvent));
+#else
+ event = g_memdup(e, sizeof(RemminaPluginRdpEvent));
+#endif
+ g_async_queue_push(rfi->event_queue, event);
+
+ if (write(rfi->event_pipe[1], "\0", 1)) {
+ }
+ }
+}
+
+static void remmina_rdp_event_release_all_keys(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaPluginRdpEvent rdp_event = { 0 };
+ int i;
+
+ /* Send all release key events for previously pressed keys */
+ for (i = 0; i < rfi->pressed_keys->len; i++) {
+ rdp_event = g_array_index(rfi->pressed_keys, RemminaPluginRdpEvent, i);
+ if ((rdp_event.type == REMMINA_RDP_EVENT_TYPE_SCANCODE ||
+ rdp_event.type == REMMINA_RDP_EVENT_TYPE_SCANCODE_UNICODE) &&
+ rdp_event.key_event.up == false) {
+ rdp_event.key_event.up = true;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ }
+ }
+
+ g_array_set_size(rfi->pressed_keys, 0);
+}
+
+static void remmina_rdp_event_release_key(RemminaProtocolWidget *gp, RemminaPluginRdpEvent rdp_event)
+{
+ TRACE_CALL(__func__);
+ gint i;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaPluginRdpEvent rdp_event_2 = { 0 };
+
+ rdp_event_2.type = REMMINA_RDP_EVENT_TYPE_SCANCODE;
+
+ if ((rdp_event.type == REMMINA_RDP_EVENT_TYPE_SCANCODE ||
+ rdp_event.type == REMMINA_RDP_EVENT_TYPE_SCANCODE_UNICODE) &&
+ rdp_event.key_event.up) {
+ /* Unregister the keycode only */
+ for (i = 0; i < rfi->pressed_keys->len; i++) {
+ rdp_event_2 = g_array_index(rfi->pressed_keys, RemminaPluginRdpEvent, i);
+
+ if (rdp_event_2.key_event.key_code == rdp_event.key_event.key_code &&
+ rdp_event_2.key_event.unicode_code == rdp_event.key_event.unicode_code &&
+ rdp_event_2.key_event.extended == rdp_event.key_event.extended &&
+ rdp_event_2.key_event.extended1 == rdp_event.key_event.extended1) {
+ g_array_remove_index_fast(rfi->pressed_keys, i);
+ break;
+ }
+ }
+ }
+}
+
+static void keypress_list_add(RemminaProtocolWidget *gp, RemminaPluginRdpEvent rdp_event)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ if (!rdp_event.key_event.key_code)
+ return;
+
+ if (rdp_event.key_event.up)
+ remmina_rdp_event_release_key(gp, rdp_event);
+ else
+ g_array_append_val(rfi->pressed_keys, rdp_event);
+}
+
+
+static void remmina_rdp_event_scale_area(RemminaProtocolWidget *gp, gint *x, gint *y, gint *w, gint *h)
+{
+ TRACE_CALL(__func__);
+ gint width, height;
+ gint sx, sy, sw, sh;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting || !rfi->surface)
+ return;
+
+ width = remmina_plugin_service->protocol_plugin_get_width(gp);
+ height = remmina_plugin_service->protocol_plugin_get_height(gp);
+
+ if ((width == 0) || (height == 0))
+ return;
+
+ if ((rfi->scale_width == width) && (rfi->scale_height == height)) {
+ /* Same size, just copy the pixels */
+ *x = MIN(MAX(0, *x), width - 1);
+ *y = MIN(MAX(0, *y), height - 1);
+ *w = MIN(width - *x, *w);
+ *h = MIN(height - *y, *h);
+ return;
+ }
+
+ /* We have to extend the scaled region one scaled pixel, to avoid gaps */
+
+ sx = MIN(MAX(0, (*x) * rfi->scale_width / width
+ - rfi->scale_width / width - 2), rfi->scale_width - 1);
+
+ sy = MIN(MAX(0, (*y) * rfi->scale_height / height
+ - rfi->scale_height / height - 2), rfi->scale_height - 1);
+
+ sw = MIN(rfi->scale_width - sx, (*w) * rfi->scale_width / width
+ + rfi->scale_width / width + 4);
+
+ sh = MIN(rfi->scale_height - sy, (*h) * rfi->scale_height / height
+ + rfi->scale_height / height + 4);
+
+ *x = sx;
+ *y = sy;
+ *w = sw;
+ *h = sh;
+}
+
+void remmina_rdp_event_update_regions(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ gint x, y, w, h, i;
+
+ for (i = 0; i < ui->reg.ninvalid; i++) {
+ x = ui->reg.ureg[i].x;
+ y = ui->reg.ureg[i].y;
+ w = ui->reg.ureg[i].w;
+ h = ui->reg.ureg[i].h;
+
+ if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED)
+ remmina_rdp_event_scale_area(gp, &x, &y, &w, &h);
+
+ gtk_widget_queue_draw_area(rfi->drawing_area, x, y, w, h);
+ }
+ g_free(ui->reg.ureg);
+}
+
+void remmina_rdp_event_update_rect(RemminaProtocolWidget *gp, gint x, gint y, gint w, gint h)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED)
+ remmina_rdp_event_scale_area(gp, &x, &y, &w, &h);
+
+ gtk_widget_queue_draw_area(rfi->drawing_area, x, y, w, h);
+}
+
+static void remmina_rdp_event_update_scale_factor(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GtkAllocation a;
+ gint rdwidth, rdheight;
+ gint gpwidth, gpheight;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ gtk_widget_get_allocation(GTK_WIDGET(gp), &a);
+ gpwidth = a.width;
+ gpheight = a.height;
+
+ if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) {
+ if ((gpwidth > 1) && (gpheight > 1)) {
+ rdwidth = remmina_plugin_service->protocol_plugin_get_width(gp);
+ rdheight = remmina_plugin_service->protocol_plugin_get_height(gp);
+
+ rfi->scale_width = gpwidth;
+ rfi->scale_height = gpheight;
+
+ rfi->scale_x = (gdouble)rfi->scale_width / (gdouble)rdwidth;
+ rfi->scale_y = (gdouble)rfi->scale_height / (gdouble)rdheight;
+ }
+ } else {
+ rfi->scale_width = 0;
+ rfi->scale_height = 0;
+ rfi->scale_x = 0;
+ rfi->scale_y = 0;
+ }
+}
+
+static gboolean remmina_rdp_event_on_draw(GtkWidget *widget, cairo_t *context, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ guint width, height;
+ gchar *msg;
+ cairo_text_extents_t extents;
+
+ if (!rfi || !rfi->connected)
+ return FALSE;
+
+
+ if (rfi->is_reconnecting) {
+ /* FreeRDP is reconnecting, just show a message to the user */
+
+ width = gtk_widget_get_allocated_width(widget);
+ height = gtk_widget_get_allocated_height(widget);
+
+ /* Draw text */
+ msg = g_strdup_printf(_("Reconnection attempt %d of %d…"),
+ rfi->reconnect_nattempt, rfi->reconnect_maxattempts);
+
+ cairo_select_font_face(context, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
+ cairo_set_font_size(context, 24);
+ cairo_set_source_rgb(context, 0.9, 0.9, 0.9);
+ cairo_text_extents(context, msg, &extents);
+ cairo_move_to(context, (width - (extents.width + extents.x_bearing)) / 2, (height - (extents.height + extents.y_bearing)) / 2);
+ cairo_show_text(context, msg);
+ g_free(msg);
+ } else {
+ /* Standard drawing: We copy the surface from RDP */
+
+ if (!rfi->surface)
+ return FALSE;
+
+ if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED)
+ cairo_scale(context, rfi->scale_x, rfi->scale_y);
+
+ cairo_surface_flush(rfi->surface);
+ cairo_set_source_surface(context, rfi->surface, 0, 0);
+ cairo_surface_mark_dirty(rfi->surface);
+
+ cairo_set_operator(context, CAIRO_OPERATOR_SOURCE); // Ignore alpha channel from FreeRDP
+ cairo_paint(context);
+ }
+
+ return TRUE;
+}
+
+static gboolean remmina_rdp_event_delayed_monitor_layout(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ RemminaPluginRdpEvent rdp_event = { 0 };
+ GtkAllocation a;
+ gint desktopOrientation, desktopScaleFactor, deviceScaleFactor;
+
+ RemminaFile *remminafile;
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting)
+ return FALSE;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ if (rfi->scale != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES)
+ return FALSE;
+
+ rfi->delayed_monitor_layout_handler = 0;
+ gint gpwidth, gpheight, prevwidth, prevheight;
+
+ gchar *monitorids = NULL;
+ guint32 maxwidth = 0;
+ guint32 maxheight = 0;
+
+ remmina_rdp_monitor_get(rfi, &monitorids, &maxwidth, &maxheight);
+
+ REMMINA_PLUGIN_DEBUG("Sending preconfigured monitor layout");
+ if (rfi->dispcontext && rfi->dispcontext->SendMonitorLayout) {
+ remmina_rdp_settings_get_orientation_scale_prefs(&desktopOrientation, &desktopScaleFactor, &deviceScaleFactor);
+ gtk_widget_get_allocation(GTK_WIDGET(gp), &a);
+ gpwidth = a.width;
+ gpheight = a.height;
+ prevwidth = remmina_plugin_service->protocol_plugin_get_width(gp);
+ prevheight = remmina_plugin_service->protocol_plugin_get_height(gp);
+
+ if ((gpwidth != prevwidth || gpheight != prevheight) && gpwidth >= 200 && gpheight >= 200) {
+ if (rfi->rdpgfxchan) {
+ /* Workaround for FreeRDP issue #5417 */
+ if (gpwidth < AVC_MIN_DESKTOP_WIDTH)
+ gpwidth = AVC_MIN_DESKTOP_WIDTH;
+ if (gpheight < AVC_MIN_DESKTOP_HEIGHT)
+ gpheight = AVC_MIN_DESKTOP_HEIGHT;
+ }
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_SEND_MONITOR_LAYOUT;
+ if (remmina_plugin_service->file_get_int(remminafile, "multimon", FALSE)) {
+ const rdpMonitor *base = freerdp_settings_get_pointer(rfi->clientContext.context.settings, FreeRDP_MonitorDefArray);
+ for (gint i = 0; i < freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_MonitorCount); ++i) {
+ const rdpMonitor *current = &base[i];
+ REMMINA_PLUGIN_DEBUG("Sending display layout n° %d", i);
+ rdp_event.monitor_layout.Flags = current->is_primary;
+ REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - Flags: %i", rdp_event.monitor_layout.Flags);
+ rdp_event.monitor_layout.Left = current->x;
+ REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - Left: %i", rdp_event.monitor_layout.Left);
+ rdp_event.monitor_layout.Top = current->y;
+ REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - Top: %i", rdp_event.monitor_layout.Top);
+ rdp_event.monitor_layout.width = current->width;
+ REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - width: %i", rdp_event.monitor_layout.width);
+ rdp_event.monitor_layout.height = current->height;
+ REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - height: %i", rdp_event.monitor_layout.height);
+ rdp_event.monitor_layout.physicalWidth = current->attributes.physicalWidth;
+ REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - physicalWidth: %i", rdp_event.monitor_layout.physicalWidth);
+ rdp_event.monitor_layout.physicalHeight = current->attributes.physicalHeight;
+ REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - PhysicalHeight: %i", rdp_event.monitor_layout.physicalHeight);
+ if (current->attributes.orientation)
+ rdp_event.monitor_layout.desktopOrientation = current->attributes.orientation;
+ else
+ rdp_event.monitor_layout.desktopOrientation = rdp_event.monitor_layout.desktopOrientation;
+ REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - desktopOrientation: %i", rdp_event.monitor_layout.desktopOrientation);
+ rdp_event.monitor_layout.desktopScaleFactor = rdp_event.monitor_layout.desktopScaleFactor;
+ REMMINA_PLUGIN_DEBUG("EVNT MON LAYOUT - ScaleFactorflag: %i", rdp_event.monitor_layout.desktopScaleFactor);
+ rdp_event.monitor_layout.deviceScaleFactor = rdp_event.monitor_layout.deviceScaleFactor;
+ }
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ } else {
+ rdp_event.monitor_layout.width = gpwidth;
+ rdp_event.monitor_layout.height = gpheight;
+ rdp_event.monitor_layout.desktopOrientation = desktopOrientation;
+ rdp_event.monitor_layout.desktopScaleFactor = desktopScaleFactor;
+ rdp_event.monitor_layout.deviceScaleFactor = deviceScaleFactor;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ }
+ }
+ }
+
+ g_free(monitorids);
+
+ return FALSE;
+}
+
+void remmina_rdp_event_send_delayed_monitor_layout(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting)
+ return;
+ if (rfi->delayed_monitor_layout_handler) {
+ g_source_remove(rfi->delayed_monitor_layout_handler);
+ rfi->delayed_monitor_layout_handler = 0;
+ }
+ if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES)
+ rfi->delayed_monitor_layout_handler = g_timeout_add(500, (GSourceFunc)remmina_rdp_event_delayed_monitor_layout, gp);
+}
+
+static gboolean remmina_rdp_event_on_configure(GtkWidget *widget, GdkEventConfigure *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ /* Called when gp changes its size or position */
+
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting)
+ return FALSE;
+
+ remmina_rdp_event_update_scale_factor(gp);
+
+ /* If the scaler is not active, schedule a delayed remote resolution change */
+ remmina_rdp_event_send_delayed_monitor_layout(gp);
+
+
+ return FALSE;
+}
+
+static void remmina_rdp_event_translate_pos(RemminaProtocolWidget *gp, int ix, int iy, UINT16 *ox, UINT16 *oy)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ /*
+ * Translate a position from local window coordinates (ix,iy) to
+ * RDP coordinates and put result on (*ox,*uy)
+ * */
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting)
+ return;
+
+ if ((rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) && (rfi->scale_width >= 1) && (rfi->scale_height >= 1)) {
+ *ox = (UINT16)(ix * remmina_plugin_service->protocol_plugin_get_width(gp) / rfi->scale_width);
+ *oy = (UINT16)(iy * remmina_plugin_service->protocol_plugin_get_height(gp) / rfi->scale_height);
+ } else {
+ *ox = (UINT16)ix;
+ *oy = (UINT16)iy;
+ }
+}
+
+static void remmina_rdp_event_reverse_translate_pos_reverse(RemminaProtocolWidget *gp, int ix, int iy, int *ox, int *oy)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ /*
+ * Translate a position from RDP coordinates (ix,iy) to
+ * local window coordinates and put result on (*ox,*uy)
+ * */
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting)
+ return;
+
+ if ((rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) && (rfi->scale_width >= 1) && (rfi->scale_height >= 1)) {
+ *ox = (ix * rfi->scale_width) / remmina_plugin_service->protocol_plugin_get_width(gp);
+ *oy = (iy * rfi->scale_height) / remmina_plugin_service->protocol_plugin_get_height(gp);
+ } else {
+ *ox = ix;
+ *oy = iy;
+ }
+}
+
+void remmina_rdp_mouse_jitter(RemminaProtocolWidget *gp){
+ TRACE_CALL(__func__);
+ RemminaPluginRdpEvent rdp_event = { 0 };
+ RemminaFile *remminafile;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE))
+ return;
+
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_MOUSE;
+ rdp_event.mouse_event.flags = PTR_FLAGS_MOVE;
+ rdp_event.mouse_event.extended = FALSE;
+ rdp_event.mouse_event.x = rfi->last_x;
+ rdp_event.mouse_event.y = rfi->last_y;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+}
+
+static gboolean remmina_rdp_event_on_motion(GtkWidget *widget, GdkEventMotion *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginRdpEvent rdp_event = { 0 };
+ RemminaFile *remminafile;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE))
+ return FALSE;
+
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_MOUSE;
+ rdp_event.mouse_event.flags = PTR_FLAGS_MOVE;
+ rdp_event.mouse_event.extended = FALSE;
+
+ remmina_rdp_event_translate_pos(gp, event->x, event->y, &rdp_event.mouse_event.x, &rdp_event.mouse_event.y);
+ if (rfi != NULL){
+ rfi->last_x = rdp_event.mouse_event.x;
+ rfi->last_y = rdp_event.mouse_event.y;
+ }
+
+ remmina_rdp_event_event_push(gp, &rdp_event);
+
+ return TRUE;
+}
+
+static gboolean remmina_rdp_event_on_button(GtkWidget *widget, GdkEventButton *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gint flag;
+ gboolean extended = FALSE;
+ RemminaPluginRdpEvent rdp_event = { 0 };
+ gint primary, secondary;
+
+ RemminaFile *remminafile;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE))
+ return FALSE;
+
+ /* We bypass 2button-press and 3button-press events */
+ if ((event->type != GDK_BUTTON_PRESS) && (event->type != GDK_BUTTON_RELEASE))
+ return TRUE;
+
+ flag = 0;
+
+ if (remmina_plugin_service->file_get_int(remminafile, "left-handed", FALSE)) {
+ primary = PTR_FLAGS_BUTTON2;
+ secondary = PTR_FLAGS_BUTTON1;
+ } else {
+ primary = PTR_FLAGS_BUTTON1;
+ secondary = PTR_FLAGS_BUTTON2;
+ }
+
+ switch (event->button) {
+ case 1:
+ flag |= primary;
+ break;
+ case 2:
+ flag |= PTR_FLAGS_BUTTON3;
+ break;
+ case 3:
+ flag |= secondary;
+ break;
+ case 8: /* back */
+ case 97: /* Xming */
+ extended = TRUE;
+ flag |= PTR_XFLAGS_BUTTON1;
+ break;
+ case 9: /* forward */
+ case 112: /* Xming */
+ extended = TRUE;
+ flag |= PTR_XFLAGS_BUTTON2;
+ break;
+ default:
+ return FALSE;
+ }
+
+ if (event->type == GDK_BUTTON_PRESS) {
+ if (extended)
+ flag |= PTR_XFLAGS_DOWN;
+ else
+ flag |= PTR_FLAGS_DOWN;
+ }
+
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_MOUSE;
+ remmina_rdp_event_translate_pos(gp, event->x, event->y, &rdp_event.mouse_event.x, &rdp_event.mouse_event.y);
+
+ if (flag != 0) {
+ rdp_event.mouse_event.flags = flag;
+ rdp_event.mouse_event.extended = extended;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ }
+
+ return TRUE;
+}
+
+static gboolean remmina_rdp_event_on_scroll(GtkWidget *widget, GdkEventScroll *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gint flag;
+ RemminaPluginRdpEvent rdp_event = { 0 };
+ float windows_delta;
+ RemminaFile *remminafile;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE))
+ return FALSE;
+
+ flag = 0;
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_MOUSE;
+
+ /* See [MS-RDPBCGR] TS_POINTER_EVENT and WM_MOUSEWHEEL message */
+
+ switch (event->direction) {
+ case GDK_SCROLL_UP:
+ flag = PTR_FLAGS_WHEEL | 0x0078; // 120 is one scroll unit defined in WM_MOUSEWHEEL
+ break;
+
+ case GDK_SCROLL_DOWN:
+ flag = PTR_FLAGS_WHEEL | 0x0188; // -120 (one scroll unit) in 9 bits two's complement
+ break;
+
+#if GTK_CHECK_VERSION(3, 4, 0)
+ case GDK_SCROLL_SMOOTH:
+
+ if (event->delta_y == 0.0)
+ return FALSE;
+
+ windows_delta = event->delta_y * -120;
+
+ if (windows_delta > 255)
+ windows_delta = 255;
+ if (windows_delta < -256)
+ windows_delta = -256;
+
+ flag = PTR_FLAGS_WHEEL | ((short)windows_delta & WheelRotationMask);
+
+ break;
+#endif
+
+ default:
+ return FALSE;
+ }
+
+ rdp_event.mouse_event.flags = flag;
+ rdp_event.mouse_event.extended = FALSE;
+ remmina_rdp_event_translate_pos(gp, event->x, event->y, &rdp_event.mouse_event.x, &rdp_event.mouse_event.y);
+ remmina_rdp_event_event_push(gp, &rdp_event);
+
+ return TRUE;
+}
+
+static void remmina_rdp_event_init_keymap(rfContext *rfi, const gchar *strmap)
+{
+ long int v1, v2;
+ const char *s;
+ char *endptr;
+ RemminaPluginRdpKeymapEntry ke;
+
+ if (strmap == NULL || strmap[0] == 0) {
+ rfi->keymap = NULL;
+ return;
+ }
+ s = strmap;
+ rfi->keymap = g_array_new(FALSE, TRUE, sizeof(RemminaPluginRdpKeymapEntry));
+ while (1) {
+ v1 = strtol(s, &endptr, 10);
+ if (endptr == s) break;
+ s = endptr;
+ if (*s != ':') break;
+ s++;
+ v2 = strtol(s, &endptr, 10);
+ if (endptr == s) break;
+ s = endptr;
+ ke.orig_keycode = v1 & 0x7fffffff;
+ ke.translated_keycode = v2 & 0x7fffffff;
+ g_array_append_val(rfi->keymap, ke);
+ if (*s != ',') break;
+ s++;
+ }
+ if (rfi->keymap->len == 0) {
+ g_array_unref(rfi->keymap);
+ rfi->keymap = NULL;
+ }
+}
+
+static gboolean remmina_rdp_event_on_key(GtkWidget *widget, GdkEventKey *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ guint32 unicode_keyval;
+ guint16 hardware_keycode;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaPluginRdpEvent rdp_event;
+ RemminaPluginRdpKeymapEntry *kep;
+ RemminaFile *remminafile;
+ DWORD scancode = 0;
+ int ik;
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting)
+ return FALSE;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE))
+ return FALSE;
+
+#ifdef ENABLE_GTK_INSPECTOR_KEY
+ /* GTK inspector key is propagated up. Disabled by default.
+ * enable it by defining ENABLE_GTK_INSPECTOR_KEY */
+ if ((event->state & GDK_CONTROL_MASK) != 0 && (event->keyval == GDK_KEY_I || event->keyval == GDK_KEY_D))
+ return FALSE;
+
+#endif
+
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_SCANCODE;
+ rdp_event.key_event.up = (event->type == GDK_KEY_PRESS ? false : true);
+ rdp_event.key_event.extended = false;
+ rdp_event.key_event.extended1 = false;
+
+ switch (event->keyval) {
+ case GDK_KEY_Pause:
+ /*
+ * See https://msdn.microsoft.com/en-us/library/cc240584.aspx
+ * 2.2.8.1.1.3.1.1.1 Keyboard Event (TS_KEYBOARD_EVENT)
+ * for pause key management
+ */
+ rdp_event.key_event.key_code = 0x1D;
+ rdp_event.key_event.up = false;
+ rdp_event.key_event.extended1 = TRUE;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ rdp_event.key_event.key_code = 0x45;
+ rdp_event.key_event.up = false;
+ rdp_event.key_event.extended1 = FALSE;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ rdp_event.key_event.key_code = 0x1D;
+ rdp_event.key_event.up = true;
+ rdp_event.key_event.extended1 = TRUE;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ rdp_event.key_event.key_code = 0x45;
+ rdp_event.key_event.up = true;
+ rdp_event.key_event.extended1 = FALSE;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ break;
+
+ default:
+ if (!rfi->use_client_keymap) {
+ hardware_keycode = event->hardware_keycode;
+ if (rfi->keymap) {
+ for (ik = 0; ik < rfi->keymap->len; ik++) {
+ kep = &g_array_index(rfi->keymap, RemminaPluginRdpKeymapEntry, ik);
+ if (hardware_keycode == kep->orig_keycode) {
+ hardware_keycode = kep->translated_keycode;
+ break;
+ }
+ }
+ }
+ scancode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(hardware_keycode);
+ if (scancode) {
+ rdp_event.key_event.key_code = scancode & 0xFF;
+ rdp_event.key_event.extended = scancode & 0x100;
+ rdp_event.key_event.extended1 = FALSE;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ keypress_list_add(gp, rdp_event);
+ }
+ } else {
+ unicode_keyval = gdk_keyval_to_unicode(event->keyval);
+ /* Decide when whe should send a keycode or a Unicode character.
+ * - All non char keys (Shift, Alt, Super) should be sent as keycode
+ * - Space should be sent as keycode (see issue #1364)
+ * - All special keys (F1-F10, numeric pad, Home/End/Arrows/PgUp/PgDn/Insert/Delete) keycode
+ * - All key pressed while Ctrl or Alt or Super is down are not decoded by gdk_keyval_to_unicode(), so send it as keycode
+ * - All keycodes not translatable to unicode chars, as keycode
+ * - The rest as Unicode char
+ */
+ if (event->keyval >= 0xfe00 || // Arrows, Shift, Alt, Fn, num keypad…
+ event->hardware_keycode == 0x41 || // Spacebar
+ unicode_keyval == 0 || // Impossible to translate
+ (event->state & (GDK_MOD1_MASK | GDK_CONTROL_MASK | GDK_SUPER_MASK)) != 0 // A modifier not recognized by gdk_keyval_to_unicode()
+ ) {
+ scancode = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(event->hardware_keycode);
+ rdp_event.key_event.key_code = scancode & 0xFF;
+ rdp_event.key_event.extended = scancode & 0x100;
+ rdp_event.key_event.extended1 = FALSE;
+ if (rdp_event.key_event.key_code) {
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ keypress_list_add(gp, rdp_event);
+ }
+ } else {
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_SCANCODE_UNICODE;
+ rdp_event.key_event.unicode_code = unicode_keyval;
+ rdp_event.key_event.extended = false;
+ rdp_event.key_event.extended1 = FALSE;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ keypress_list_add(gp, rdp_event);
+ }
+ }
+ break;
+ }
+
+ return TRUE;
+}
+
+gboolean remmina_rdp_event_on_clipboard(GtkClipboard *gtkClipboard, GdkEvent *event, RemminaProtocolWidget *gp)
+{
+ /* Signal handler for GTK clipboard owner-change */
+ TRACE_CALL(__func__);
+ RemminaPluginRdpEvent rdp_event = { 0 };
+ CLIPRDR_FORMAT_LIST *pFormatList;
+ GObject *new_owner;
+
+ /* Usually "owner-change" is fired when a user presses "COPY" on the client
+ * OR when this plugin calls gtk_clipboard_set_with_owner()
+ * after receiving a RDP server format list in remmina_rdp_cliprdr_server_format_list()
+ * In the latter case, we must ignore owner change */
+
+ REMMINA_PLUGIN_DEBUG("gp=%p: owner-change event received", gp);
+
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ if (rfi)
+ remmina_rdp_clipboard_abort_client_format_data_request(rfi);
+
+ new_owner = gtk_clipboard_get_owner(gtkClipboard);
+ if (new_owner != (GObject *)gp) {
+ /* To do: avoid this when the new owner is another remmina protocol widget of
+ * the same remmina application */
+ REMMINA_PLUGIN_DEBUG("gp=%p owner-change: new owner is different than me: new=%p me=%p",
+ gp, new_owner, gp);
+
+ REMMINA_PLUGIN_DEBUG("gp=%p owner-change: new owner is not me: Sending local clipboard format list to server.",
+ gp, new_owner, gp);
+ pFormatList = remmina_rdp_cliprdr_get_client_format_list(gp);
+ rdp_event.type = REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_LIST;
+ rdp_event.clipboard_formatlist.pFormatList = pFormatList;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+ } else {
+ REMMINA_PLUGIN_DEBUG(" ... but I'm the owner!");
+ }
+ return TRUE;
+}
+
+void remmina_rdp_event_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+ gint flags;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ GtkClipboard *clipboard;
+ RemminaFile *remminafile;
+
+ gboolean disable_smooth_scrolling = FALSE;
+
+ if (!rfi) return;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ /* we get first the global preferences */
+ s = remmina_plugin_service->pref_get_value("rdp_disable_smooth_scrolling");
+ disable_smooth_scrolling = (s && s[0] == '1' ? TRUE : FALSE);
+ g_free(s), s = NULL;
+ /* Otherwise we use the connection profile specific setting */
+ disable_smooth_scrolling = remmina_plugin_service->file_get_int(remminafile, "disable-smooth-scrolling", disable_smooth_scrolling);
+
+ REMMINA_PLUGIN_DEBUG("Disable smooth scrolling is set to %d", disable_smooth_scrolling);
+
+ rfi->drawing_area = gtk_drawing_area_new();
+ gtk_widget_show(rfi->drawing_area);
+ gtk_container_add(GTK_CONTAINER(gp), rfi->drawing_area);
+
+ gtk_widget_add_events(rfi->drawing_area, GDK_POINTER_MOTION_MASK
+ | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
+ | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK
+ | GDK_SCROLL_MASK | GDK_FOCUS_CHANGE_MASK);
+
+ if (!disable_smooth_scrolling) {
+ REMMINA_PLUGIN_DEBUG("Adding GDK_SMOOTH_SCROLL_MASK");
+ gtk_widget_add_events(rfi->drawing_area, GDK_SMOOTH_SCROLL_MASK);
+ }
+
+ gtk_widget_set_can_focus(rfi->drawing_area, TRUE);
+
+ remmina_plugin_service->protocol_plugin_register_hostkey(gp, rfi->drawing_area);
+
+ s = remmina_plugin_service->pref_get_value("rdp_use_client_keymap");
+ rfi->use_client_keymap = (s && s[0] == '1' ? TRUE : FALSE);
+ g_free(s), s = NULL;
+
+ /* Read special keymap from profile file, if exists */
+ remmina_rdp_event_init_keymap(rfi, remmina_plugin_service->pref_get_value("rdp_map_keycode"));
+
+ if (rfi->use_client_keymap && rfi->keymap)
+ fprintf(stderr, "RDP profile error: you cannot define both rdp_map_hardware_keycode and have 'Use client keyboard mapping' enabled\n");
+
+ g_signal_connect(G_OBJECT(rfi->drawing_area), "draw",
+ G_CALLBACK(remmina_rdp_event_on_draw), gp);
+ g_signal_connect(G_OBJECT(rfi->drawing_area), "configure-event",
+ G_CALLBACK(remmina_rdp_event_on_configure), gp);
+ g_signal_connect(G_OBJECT(rfi->drawing_area), "motion-notify-event",
+ G_CALLBACK(remmina_rdp_event_on_motion), gp);
+ g_signal_connect(G_OBJECT(rfi->drawing_area), "button-press-event",
+ G_CALLBACK(remmina_rdp_event_on_button), gp);
+ g_signal_connect(G_OBJECT(rfi->drawing_area), "button-release-event",
+ G_CALLBACK(remmina_rdp_event_on_button), gp);
+ g_signal_connect(G_OBJECT(rfi->drawing_area), "scroll-event",
+ G_CALLBACK(remmina_rdp_event_on_scroll), gp);
+ g_signal_connect(G_OBJECT(rfi->drawing_area), "key-press-event",
+ G_CALLBACK(remmina_rdp_event_on_key), gp);
+ g_signal_connect(G_OBJECT(rfi->drawing_area), "key-release-event",
+ G_CALLBACK(remmina_rdp_event_on_key), gp);
+ g_signal_connect(G_OBJECT(rfi->drawing_area), "focus-in-event",
+ G_CALLBACK(remmina_rdp_event_on_focus_in), gp);
+ /** Fixme: This comment
+ * needed for TS_SUPPRESS_OUTPUT_PDU
+ * But it works only when we stay in the same window mode, if we switch to
+ * fullscreen, for instance, the object refernce is lost, so we loose these
+ * events.
+ */
+ //g_signal_connect(G_OBJECT(gtk_widget_get_toplevel(rfi->drawing_area)), "map-event",
+ // G_CALLBACK(remmina_rdp_event_on_map), gp);
+ //g_signal_connect(G_OBJECT(gtk_widget_get_toplevel(rfi->drawing_area)), "unmap-event",
+ // G_CALLBACK(remmina_rdp_event_on_unmap), gp);
+
+ if (!remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE)) {
+ clipboard = gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD);
+ rfi->clipboard.clipboard_handler = g_signal_connect(clipboard, "owner-change", G_CALLBACK(remmina_rdp_event_on_clipboard), gp);
+ }
+
+ rfi->pressed_keys = g_array_new(FALSE, TRUE, sizeof(RemminaPluginRdpEvent));
+ rfi->event_queue = g_async_queue_new_full(g_free);
+ rfi->ui_queue = g_async_queue_new();
+ pthread_mutex_init(&rfi->ui_queue_mutex, NULL);
+
+ if (pipe(rfi->event_pipe)) {
+ g_print("Error creating pipes.\n");
+ rfi->event_pipe[0] = -1;
+ rfi->event_pipe[1] = -1;
+ rfi->event_handle = NULL;
+ } else {
+ flags = fcntl(rfi->event_pipe[0], F_GETFL, 0);
+ fcntl(rfi->event_pipe[0], F_SETFL, flags | O_NONBLOCK);
+ rfi->event_handle = CreateFileDescriptorEvent(NULL, FALSE, FALSE, rfi->event_pipe[0], WINPR_FD_READ);
+ if (!rfi->event_handle)
+ g_print("CreateFileDescriptorEvent() failed\n");
+ }
+
+ rfi->object_table = g_hash_table_new_full(NULL, NULL, NULL, g_free);
+
+ rfi->display = gdk_display_get_default();
+
+#if GTK_CHECK_VERSION(3, 22, 0)
+ GdkVisual *visual = gdk_screen_get_system_visual(
+ gdk_display_get_default_screen(rfi->display));
+ rfi->bpp = gdk_visual_get_depth(visual);
+#else
+ rfi->bpp = gdk_visual_get_best_depth();
+#endif
+}
+
+void remmina_rdp_event_free_event(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *obj)
+{
+ TRACE_CALL(__func__);
+
+ switch (obj->type) {
+ case REMMINA_RDP_UI_NOCODEC:
+ free(obj->nocodec.bitmap);
+ break;
+
+ default:
+ break;
+ }
+
+ g_free(obj);
+}
+
+void remmina_rdp_event_uninit(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaPluginRdpUiObject *ui;
+
+ if (!rfi) return;
+
+ /* unregister the clipboard monitor */
+ if (rfi->clipboard.clipboard_handler) {
+ g_signal_handler_disconnect(G_OBJECT(gtk_widget_get_clipboard(rfi->drawing_area, GDK_SELECTION_CLIPBOARD)), rfi->clipboard.clipboard_handler);
+ rfi->clipboard.clipboard_handler = 0;
+ }
+ if (rfi->delayed_monitor_layout_handler) {
+ g_source_remove(rfi->delayed_monitor_layout_handler);
+ rfi->delayed_monitor_layout_handler = 0;
+ }
+ if (rfi->ui_handler) {
+ g_source_remove(rfi->ui_handler);
+ rfi->ui_handler = 0;
+ }
+ while ((ui = (RemminaPluginRdpUiObject *)g_async_queue_try_pop(rfi->ui_queue)) != NULL)
+ remmina_rdp_event_free_event(gp, ui);
+ if (rfi->surface) {
+ cairo_surface_mark_dirty(rfi->surface);
+ cairo_surface_destroy(rfi->surface);
+ rfi->surface = NULL;
+ }
+
+ g_hash_table_destroy(rfi->object_table);
+
+ g_array_free(rfi->pressed_keys, TRUE);
+ if (rfi->keymap) {
+ g_array_free(rfi->keymap, TRUE);
+ rfi->keymap = NULL;
+ }
+ g_async_queue_unref(rfi->event_queue);
+ rfi->event_queue = NULL;
+ g_async_queue_unref(rfi->ui_queue);
+ rfi->ui_queue = NULL;
+ pthread_mutex_destroy(&rfi->ui_queue_mutex);
+
+ if (rfi->event_handle) {
+ CloseHandle(rfi->event_handle);
+ rfi->event_handle = NULL;
+ }
+
+ close(rfi->event_pipe[0]);
+ close(rfi->event_pipe[1]);
+}
+
+static void remmina_rdp_event_create_cairo_surface(rfContext *rfi)
+{
+ int stride;
+ rdpGdi *gdi;
+
+ if (!rfi)
+ return;
+
+ gdi = ((rdpContext *)rfi)->gdi;
+
+ if (!gdi)
+ return;
+
+ if (rfi->surface) {
+ cairo_surface_mark_dirty(rfi->surface);
+ cairo_surface_destroy(rfi->surface);
+ rfi->surface = NULL;
+ }
+ stride = cairo_format_stride_for_width(rfi->cairo_format, gdi->width);
+ rfi->surface = cairo_image_surface_create_for_data((unsigned char *)gdi->primary_buffer, rfi->cairo_format, gdi->width, gdi->height, stride);
+ cairo_surface_flush(rfi->surface);
+}
+
+void remmina_rdp_event_update_scale(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gint width, height;
+ rdpGdi *gdi;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ width = remmina_plugin_service->protocol_plugin_get_width(gp);
+ height = remmina_plugin_service->protocol_plugin_get_height(gp);
+
+ gdi = ((rdpContext *)rfi)->gdi;
+
+ rfi->scale = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp);
+
+ /* See if we also must rellocate rfi->surface with different width and height,
+ * this usually happens after a DesktopResize RDP event*/
+
+ if (rfi->surface && (cairo_image_surface_get_width(rfi->surface) != gdi->width ||
+ cairo_image_surface_get_height(rfi->surface) != gdi->height)) {
+ /* Destroys and recreate rfi->surface with new width and height */
+ cairo_surface_mark_dirty(rfi->surface);
+ cairo_surface_destroy(rfi->surface);
+ rfi->surface = NULL;
+ remmina_rdp_event_create_cairo_surface(rfi);
+ } else if (rfi->surface == NULL) {
+ remmina_rdp_event_create_cairo_surface(rfi);
+ }
+
+ /* Send gdi->width and gdi->height obtained from remote server to gp plugin,
+ * so they will be saved when closing connection */
+ if (width != gdi->width)
+ remmina_plugin_service->protocol_plugin_set_width(gp, gdi->width);
+ if (height != gdi->height)
+ remmina_plugin_service->protocol_plugin_set_height(gp, gdi->height);
+
+ remmina_rdp_event_update_scale_factor(gp);
+
+ if (rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED || rfi->scale == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES)
+ /* In scaled mode and autores mode, drawing_area will get its dimensions from its parent */
+ gtk_widget_set_size_request(rfi->drawing_area, -1, -1);
+ else
+ /* In non scaled mode, the plugins forces dimensions of drawing area */
+ gtk_widget_set_size_request(rfi->drawing_area, width, height);
+ remmina_plugin_service->protocol_plugin_update_align(gp);
+}
+
+static void remmina_rdp_event_connected(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ rdpGdi *gdi;
+
+ gdi = ((rdpContext *)rfi)->gdi;
+
+ gtk_widget_realize(rfi->drawing_area);
+
+ remmina_rdp_event_create_cairo_surface(rfi);
+ gtk_widget_queue_draw_area(rfi->drawing_area, 0, 0, gdi->width, gdi->height);
+
+ remmina_rdp_event_update_scale(gp);
+
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+ const gchar *host = freerdp_settings_get_string (rfi->clientContext.context.settings, FreeRDP_ServerHostname);
+ // TRANSLATORS: the placeholder may be either an IP/FQDN or a server hostname
+ REMMINA_PLUGIN_AUDIT(_("Connected to %s via RDP"), host);
+}
+
+static void remmina_rdp_event_reconnect_progress(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ gdk_window_invalidate_rect(gtk_widget_get_window(rfi->drawing_area), NULL, TRUE);
+}
+
+static BOOL remmina_rdp_event_create_cursor(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ GdkPixbuf *pixbuf;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ rdpPointer *pointer = (rdpPointer *)ui->cursor.pointer;
+ cairo_surface_t *surface;
+ UINT8 *data = malloc(pointer->width * pointer->height * 4);
+
+ if (!freerdp_image_copy_from_pointer_data(
+ (BYTE *)data, PIXEL_FORMAT_BGRA32,
+ pointer->width * 4, 0, 0, pointer->width, pointer->height,
+ pointer->xorMaskData, pointer->lengthXorMask,
+ pointer->andMaskData, pointer->lengthAndMask,
+ pointer->xorBpp, &(ui->cursor.context->gdi->palette))) {
+ free(data);
+ return FALSE;
+ }
+
+ surface = cairo_image_surface_create_for_data(data, CAIRO_FORMAT_ARGB32, pointer->width, pointer->height, cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, pointer->width));
+ cairo_surface_flush(surface);
+ pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, pointer->width, pointer->height);
+ cairo_surface_mark_dirty(surface);
+ cairo_surface_destroy(surface);
+ free(data);
+ ((rfPointer *)ui->cursor.pointer)->cursor = gdk_cursor_new_from_pixbuf(rfi->display, pixbuf, pointer->xPos, pointer->yPos);
+ g_object_unref(pixbuf);
+
+ return TRUE;
+}
+
+static void remmina_rdp_event_free_cursor(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ g_object_unref(ui->cursor.pointer->cursor);
+ ui->cursor.pointer->cursor = NULL;
+}
+
+static BOOL remmina_rdp_event_set_pointer_position(RemminaProtocolWidget *gp, gint x, gint y)
+{
+ TRACE_CALL(__func__);
+ GdkWindow *w, *nw;
+ gint nx, ny, wx, wy;
+
+#if GTK_CHECK_VERSION(3, 20, 0)
+ GdkSeat *seat;
+#else
+ GdkDeviceManager *manager;
+#endif
+ GdkDevice *dev;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ if (rfi == NULL)
+ return FALSE;
+
+ w = gtk_widget_get_window(rfi->drawing_area);
+#if GTK_CHECK_VERSION(3, 20, 0)
+ seat = gdk_display_get_default_seat(gdk_display_get_default());
+ dev = gdk_seat_get_pointer(seat);
+#else
+ manager = gdk_display_get_device_manager(gdk_display_get_default());
+ dev = gdk_device_manager_get_client_pointer(manager);
+#endif
+
+ nw = gdk_device_get_window_at_position(dev, NULL, NULL);
+
+ if (nw == w) {
+ nx = 0;
+ ny = 0;
+ remmina_rdp_event_reverse_translate_pos_reverse(gp, x, y, &nx, &ny);
+ gdk_window_get_root_coords(w, nx, ny, &wx, &wy);
+ gdk_device_warp(dev, gdk_window_get_screen(w), wx, wy);
+ }
+ return TRUE;
+}
+
+static void remmina_rdp_event_cursor(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ switch (ui->cursor.type) {
+ case REMMINA_RDP_POINTER_NEW:
+ ui->retval = remmina_rdp_event_create_cursor(gp, ui) ? 1 : 0;
+ break;
+
+ case REMMINA_RDP_POINTER_FREE:
+ remmina_rdp_event_free_cursor(gp, ui);
+ break;
+
+ case REMMINA_RDP_POINTER_SET:
+ gdk_window_set_cursor(gtk_widget_get_window(rfi->drawing_area), ui->cursor.pointer->cursor);
+ ui->retval = 1;
+ break;
+
+ case REMMINA_RDP_POINTER_SETPOS:
+ ui->retval = remmina_rdp_event_set_pointer_position(gp, ui->pos.x, ui->pos.y) ? 1 : 0;
+ break;
+
+ case REMMINA_RDP_POINTER_NULL:
+ gdk_window_set_cursor(gtk_widget_get_window(rfi->drawing_area),
+ gdk_cursor_new_for_display(gdk_display_get_default(),
+ GDK_BLANK_CURSOR));
+ ui->retval = 1;
+ break;
+
+ case REMMINA_RDP_POINTER_DEFAULT:
+ gdk_window_set_cursor(gtk_widget_get_window(rfi->drawing_area), NULL);
+ ui->retval = 1;
+ break;
+ }
+}
+
+static void remmina_rdp_ui_event_update_scale(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ remmina_rdp_event_update_scale(gp);
+}
+
+void remmina_rdp_event_unfocus(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ if (!rfi || !rfi->connected || rfi->is_reconnecting)
+ return;
+ remmina_rdp_event_release_all_keys(gp);
+}
+
+static void remmina_rdp_ui_event_destroy_cairo_surface(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ cairo_surface_mark_dirty(rfi->surface);
+ cairo_surface_destroy(rfi->surface);
+ rfi->surface = NULL;
+}
+
+static void remmina_rdp_event_process_event(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ switch (ui->event.type) {
+ case REMMINA_RDP_UI_EVENT_UPDATE_SCALE:
+ remmina_rdp_ui_event_update_scale(gp, ui);
+ break;
+ case REMMINA_RDP_UI_EVENT_DESTROY_CAIRO_SURFACE:
+ remmina_rdp_ui_event_destroy_cairo_surface(gp, ui);
+ break;
+ }
+}
+
+static void remmina_rdp_event_process_ui_event(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ switch (ui->type) {
+ case REMMINA_RDP_UI_UPDATE_REGIONS:
+ remmina_rdp_event_update_regions(gp, ui);
+ break;
+
+ case REMMINA_RDP_UI_CONNECTED:
+ remmina_rdp_event_connected(gp, ui);
+ break;
+
+ case REMMINA_RDP_UI_RECONNECT_PROGRESS:
+ remmina_rdp_event_reconnect_progress(gp, ui);
+ break;
+
+ case REMMINA_RDP_UI_CURSOR:
+ remmina_rdp_event_cursor(gp, ui);
+ break;
+
+ case REMMINA_RDP_UI_CLIPBOARD:
+ remmina_rdp_event_process_clipboard(gp, ui);
+ break;
+
+ case REMMINA_RDP_UI_EVENT:
+ remmina_rdp_event_process_event(gp, ui);
+ break;
+
+ default:
+ break;
+ }
+}
+
+static gboolean remmina_rdp_event_process_ui_queue(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaPluginRdpUiObject *ui;
+
+ pthread_mutex_lock(&rfi->ui_queue_mutex);
+ ui = (RemminaPluginRdpUiObject *)g_async_queue_try_pop(rfi->ui_queue);
+ if (ui) {
+ pthread_mutex_lock(&ui->sync_wait_mutex);
+ if (!rfi->thread_cancelled)
+ remmina_rdp_event_process_ui_event(gp, ui);
+ // Should we signal the caller thread to unlock ?
+ if (ui->sync) {
+ ui->complete = TRUE;
+ pthread_cond_signal(&ui->sync_wait_cond);
+ pthread_mutex_unlock(&ui->sync_wait_mutex);
+ } else {
+ remmina_rdp_event_free_event(gp, ui);
+ }
+
+ pthread_mutex_unlock(&rfi->ui_queue_mutex);
+ return TRUE;
+ } else {
+ rfi->ui_handler = 0;
+ pthread_mutex_unlock(&rfi->ui_queue_mutex);
+ return FALSE;
+ }
+}
+
+static void remmina_rdp_event_queue_ui(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ gboolean ui_sync_save;
+ int oldcanceltype;
+
+ if (!rfi || rfi->thread_cancelled)
+ return;
+
+ if (remmina_plugin_service->is_main_thread()) {
+ remmina_rdp_event_process_ui_event(gp, ui);
+ return;
+ }
+
+ pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldcanceltype);
+
+ pthread_mutex_lock(&rfi->ui_queue_mutex);
+
+ ui_sync_save = ui->sync;
+ ui->complete = FALSE;
+
+ if (ui_sync_save) {
+ pthread_mutex_init(&ui->sync_wait_mutex, NULL);
+ pthread_cond_init(&ui->sync_wait_cond, NULL);
+ }
+
+ ui->complete = FALSE;
+
+ g_async_queue_push(rfi->ui_queue, ui);
+
+ if (!rfi->ui_handler)
+ rfi->ui_handler = IDLE_ADD((GSourceFunc)remmina_rdp_event_process_ui_queue, gp);
+
+ if (ui_sync_save) {
+ /* Wait for main thread function completion before returning */
+ pthread_mutex_lock(&ui->sync_wait_mutex);
+ pthread_mutex_unlock(&rfi->ui_queue_mutex);
+ while (!ui->complete)
+ pthread_cond_wait(&ui->sync_wait_cond, &ui->sync_wait_mutex);
+ pthread_cond_destroy(&ui->sync_wait_cond);
+ pthread_mutex_destroy(&ui->sync_wait_mutex);
+ } else {
+ pthread_mutex_unlock(&rfi->ui_queue_mutex);
+ }
+ pthread_setcanceltype(oldcanceltype, NULL);
+}
+
+void remmina_rdp_event_queue_ui_async(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ ui->sync = FALSE;
+ remmina_rdp_event_queue_ui(gp, ui);
+}
+
+int remmina_rdp_event_queue_ui_sync_retint(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ int retval;
+
+ ui->sync = TRUE;
+ remmina_rdp_event_queue_ui(gp, ui);
+ retval = ui->retval;
+ remmina_rdp_event_free_event(gp, ui);
+ return retval;
+}
+
+void *remmina_rdp_event_queue_ui_sync_retptr(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui)
+{
+ TRACE_CALL(__func__);
+ void *rp;
+
+ ui->sync = TRUE;
+ remmina_rdp_event_queue_ui(gp, ui);
+ rp = ui->retptr;
+ remmina_rdp_event_free_event(gp, ui);
+ return rp;
+}
diff --git a/plugins/rdp/rdp_event.h b/plugins/rdp/rdp_event.h
new file mode 100644
index 0000000..8a7f08a
--- /dev/null
+++ b/plugins/rdp/rdp_event.h
@@ -0,0 +1,58 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#include <glib.h>
+#include "common/remmina_plugin.h"
+#include "rdp_plugin.h"
+
+G_BEGIN_DECLS
+
+
+void remmina_rdp_event_init(RemminaProtocolWidget *gp);
+void remmina_rdp_event_uninit(RemminaProtocolWidget *gp);
+void remmina_rdp_event_update_scale(RemminaProtocolWidget *gp);
+void remmina_rdp_event_unfocus(RemminaProtocolWidget *gp);
+void remmina_rdp_event_send_delayed_monitor_layout(RemminaProtocolWidget *gp);
+void remmina_rdp_event_update_rect(RemminaProtocolWidget *gp, gint x, gint y, gint w, gint h);
+void remmina_rdp_event_queue_ui_async(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui);
+int remmina_rdp_event_queue_ui_sync_retint(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui);
+void *remmina_rdp_event_queue_ui_sync_retptr(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *ui);
+gboolean remmina_rdp_event_on_map(RemminaProtocolWidget *gp);
+gboolean remmina_rdp_event_on_unmap(RemminaProtocolWidget *gp);
+void remmina_rdp_mouse_jitter(RemminaProtocolWidget *gp);
+
+G_END_DECLS
diff --git a/plugins/rdp/rdp_file.c b/plugins/rdp/rdp_file.c
new file mode 100644
index 0000000..fc161e8
--- /dev/null
+++ b/plugins/rdp/rdp_file.c
@@ -0,0 +1,299 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "remmina/plugin.h"
+#include "rdp_plugin.h"
+#include "rdp_file.h"
+
+gboolean remmina_rdp_file_import_test(RemminaFilePlugin *plugin, const gchar *from_file)
+{
+ TRACE_CALL(__func__);
+ gchar *ext;
+
+ ext = strrchr(from_file, '.');
+
+ if (!ext)
+ return FALSE;
+
+ ext++;
+
+ if (g_strcmp0(ext, "RDP") == 0)
+ return TRUE;
+
+ if (g_strcmp0(ext, "rdp") == 0)
+ return TRUE;
+
+ return FALSE;
+}
+
+static void remmina_rdp_file_import_field(RemminaFile *remminafile, const gchar *key, const gchar *value)
+{
+ TRACE_CALL(__func__);
+ if (g_strcmp0(key, "desktopwidth") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "resolution_width", value);
+ } else if (g_strcmp0(key, "desktopheight") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "resolution_height", value);
+ } else if (g_strcmp0(key, "session bpp") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "colordepth", value);
+ } else if (g_strcmp0(key, "keyboardhook") == 0) {
+ remmina_plugin_service->file_set_int(remminafile, "keyboard_grab", (atoi(value) == 1));
+ } else if (g_strcmp0(key, "full address") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "server", value);
+ } else if (g_strcmp0(key, "audiomode") == 0) {
+ switch (atoi(value)) {
+ case 0:
+ remmina_plugin_service->file_set_string(remminafile, "sound", "local");
+ break;
+ case 1:
+ remmina_plugin_service->file_set_string(remminafile, "sound", "remote");
+ break;
+ }
+ } else if (g_strcmp0(key, "redirectprinters") == 0) {
+ remmina_plugin_service->file_set_int(remminafile, "shareprinter", (atoi(value) == 1));
+ } else if (g_strcmp0(key, "redirectsmartcard") == 0) {
+ remmina_plugin_service->file_set_int(remminafile, "sharesmartcard", (atoi(value) == 1));
+ } else if (g_strcmp0(key, "redirectclipboard") == 0) {
+ remmina_plugin_service->file_set_int(remminafile, "disableclipboard", (atoi(value) != 1));
+ } else if (g_strcmp0(key, "alternate shell") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "exec", value);
+ } else if (g_strcmp0(key, "shell working directory") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "execpath", value);
+ } else if (g_strcmp0(key, "loadbalanceinfo") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "loadbalanceinfo", value);
+ } else if (g_strcmp0(key, "gatewayhostname") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "gateway_server", value);
+ } else if (g_strcmp0(key, "gatewayusagemethod") == 0) {
+ remmina_plugin_service->file_set_int(remminafile, "gateway_usage", (atoi(value) == TSC_PROXY_MODE_DETECT));
+ } else if (g_strcmp0(key, "gatewayaccesstoken") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "gatewayaccesstoken", value);
+ } else if (g_strcmp0(key, "authentication level") == 0) {
+ remmina_plugin_service->file_set_int(remminafile, "authentication level", atoi(value));
+ }
+ /* tsclient fields, import only */
+ else if (g_strcmp0(key, "client hostname") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "clientname", value);
+ } else if (g_strcmp0(key, "domain") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "domain", value);
+ } else if (g_strcmp0(key, "username") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "username", value);
+ } else if (g_strcmp0(key, "password") == 0) {
+ remmina_plugin_service->file_set_string(remminafile, "password", value);
+ }
+}
+
+static RemminaFile *remmina_rdp_file_import_channel(GIOChannel *channel)
+{
+ TRACE_CALL(__func__);
+ gchar *p;
+ const gchar *enc;
+ gchar *line = NULL;
+ GError *error = NULL;
+ gsize bytes_read = 0;
+ RemminaFile *remminafile;
+ guchar magic[2] = { 0 };
+
+ if (g_io_channel_set_encoding(channel, NULL, &error) != G_IO_STATUS_NORMAL) {
+ g_print("g_io_channel_set_encoding: %s\n", error->message);
+ return NULL;
+ }
+
+ /* Try to detect the UTF-16 encoding */
+ if (g_io_channel_read_chars(channel, (gchar *)magic, 2, &bytes_read, &error) != G_IO_STATUS_NORMAL) {
+ g_print("g_io_channel_read_chars: %s\n", error->message);
+ return NULL;
+ }
+
+ if (magic[0] == 0xFF && magic[1] == 0xFE) {
+ enc = "UTF-16LE";
+ } else if (magic[0] == 0xFE && magic[1] == 0xFF) {
+ enc = "UTF-16BE";
+ } else {
+ enc = "UTF-8";
+ if (g_io_channel_seek_position(channel, 0, G_SEEK_SET, &error) != G_IO_STATUS_NORMAL) {
+ g_print("g_io_channel_seek: failed\n");
+ return NULL;
+ }
+ }
+
+ if (g_io_channel_set_encoding(channel, enc, &error) != G_IO_STATUS_NORMAL) {
+ g_print("g_io_channel_set_encoding: %s\n", error->message);
+ return NULL;
+ }
+
+ remminafile = remmina_plugin_service->file_new();
+
+ while (g_io_channel_read_line(channel, &line, NULL, &bytes_read, &error) == G_IO_STATUS_NORMAL) {
+ if (line == NULL)
+ break;
+
+ line[bytes_read] = '\0';
+ p = strchr(line, ':');
+
+ if (p) {
+ *p++ = '\0';
+ p = strchr(p, ':');
+
+ if (p) {
+ p++;
+ remmina_rdp_file_import_field(remminafile, line, p);
+ }
+ }
+
+ g_free(line);
+ }
+
+ remmina_plugin_service->file_set_string(remminafile, "name",
+ remmina_plugin_service->file_get_string(remminafile, "server"));
+ remmina_plugin_service->file_set_string(remminafile, "protocol", "RDP");
+
+ return remminafile;
+}
+
+RemminaFile *remmina_rdp_file_import(RemminaFilePlugin *plugin,const gchar *from_file)
+{
+ TRACE_CALL(__func__);
+ GIOChannel *channel;
+ GError *error = NULL;
+ RemminaFile *remminafile;
+
+ channel = g_io_channel_new_file(from_file, "r", &error);
+
+ if (channel == NULL) {
+ g_print("Failed to import %s: %s\n", from_file, error->message);
+ return NULL;
+ }
+
+ remminafile = remmina_rdp_file_import_channel(channel);
+ g_io_channel_shutdown(channel, TRUE, &error);
+
+ return remminafile;
+}
+
+gboolean remmina_rdp_file_export_test(RemminaFilePlugin *plugin, RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ if (g_strcmp0(remmina_plugin_service->file_get_string(remminafile, "protocol"), "RDP") == 0)
+ return TRUE;
+
+ return FALSE;
+}
+
+gboolean remmina_rdp_file_export_channel(RemminaFile *remminafile, FILE *fp)
+{
+ TRACE_CALL(__func__);
+ const gchar *cs;
+ int w, h;
+
+ fprintf(fp, "screen mode id:i:2\r\n");
+ w = remmina_plugin_service->file_get_int(remminafile, "resolution_width", -1);
+ h = remmina_plugin_service->file_get_int(remminafile, "resolution_height", -1);
+ if (w > 0 && h > 0) {
+ fprintf(fp, "desktopwidth:i:%d\r\n", w);
+ fprintf(fp, "desktopheight:i:%d\r\n", h);
+ }
+
+ fprintf(fp, "session bpp:i:%i\r\n", remmina_plugin_service->file_get_int(remminafile, "colordepth", 8));
+ //fprintf(fp, "winposstr:s:0,1,123,34,931,661\r\n");
+ fprintf(fp, "compression:i:1\r\n");
+ fprintf(fp, "keyboardhook:i:2\r\n");
+ fprintf(fp, "displayconnectionbar:i:1\r\n");
+ fprintf(fp, "disable wallpaper:i:1\r\n");
+ fprintf(fp, "disable full window drag:i:1\r\n");
+ fprintf(fp, "allow desktop composition:i:0\r\n");
+ fprintf(fp, "allow font smoothing:i:0\r\n");
+ fprintf(fp, "disable menu anims:i:1\r\n");
+ fprintf(fp, "disable themes:i:0\r\n");
+ fprintf(fp, "disable cursor setting:i:0\r\n");
+ fprintf(fp, "bitmapcachepersistenable:i:1\r\n");
+ cs = remmina_plugin_service->file_get_string(remminafile, "server");
+ fprintf(fp, "full address:s:%s\r\n", cs ? cs : "");
+ if (g_strcmp0(remmina_plugin_service->file_get_string(remminafile, "sound"), "local") == 0)
+ fprintf(fp, "audiomode:i:0\r\n");
+ else if (g_strcmp0(remmina_plugin_service->file_get_string(remminafile, "sound"), "remote") == 0)
+ fprintf(fp, "audiomode:i:1\r\n");
+ else
+ fprintf(fp, "audiomode:i:2\r\n");
+ if (g_strcmp0(remmina_plugin_service->file_get_string(remminafile, "microphone"), "") == 0)
+ fprintf(fp, "audiocapturemode:i:0\r\n");
+ else if (g_strcmp0(remmina_plugin_service->file_get_string(remminafile, "microphone"), "0") == 0)
+ fprintf(fp, "audiocapturemode:i:1\r\n");
+ else
+ fprintf(fp, "audiocapturemode:i:1\r\n");
+ fprintf(fp, "redirectprinters:i:%i\r\n", remmina_plugin_service->file_get_int(remminafile, "shareprinter", FALSE) ? 1 : 0);
+ fprintf(fp, "redirectsmartcard:i:%i\r\n", remmina_plugin_service->file_get_int(remminafile, "sharesmartcard", FALSE) ? 1 : 0);
+ fprintf(fp, "redirectcomports:i:0\r\n");
+ fprintf(fp, "redirectsmartcards:i:0\r\n");
+ fprintf(fp, "redirectclipboard:i:1\r\n");
+ fprintf(fp, "redirectposdevices:i:0\r\n");
+ fprintf(fp, "autoreconnection enabled:i:1\r\n");
+ fprintf(fp, "authentication level:i:0\r\n");
+ fprintf(fp, "prompt for credentials:i:1\r\n");
+ fprintf(fp, "negotiate security layer:i:1\r\n");
+ fprintf(fp, "remoteapplicationmode:i:0\r\n");
+ cs = remmina_plugin_service->file_get_string(remminafile, "exec");
+ fprintf(fp, "alternate shell:s:%s\r\n", cs ? cs : "");
+ cs = remmina_plugin_service->file_get_string(remminafile, "execpath");
+ fprintf(fp, "shell working directory:s:%s\r\n", cs ? cs : "");
+ cs = remmina_plugin_service->file_get_string(remminafile, "gateway_server");
+ fprintf(fp, "gatewayhostname:s:%s\r\n", cs ? cs : "");
+ fprintf(fp, "gatewayusagemethod:i:4\r\n");
+ fprintf(fp, "gatewaycredentialssource:i:4\r\n");
+ fprintf(fp, "gatewayprofileusagemethod:i:0\r\n");
+ fprintf(fp, "precommand:s:\r\n");
+ fprintf(fp, "promptcredentialonce:i:1\r\n");
+ fprintf(fp, "drivestoredirect:s:\r\n");
+
+ return TRUE;
+}
+
+gboolean remmina_rdp_file_export(RemminaFilePlugin *plugin, RemminaFile *remminafile, const gchar *to_file)
+{
+ TRACE_CALL(__func__);
+ FILE *fp;
+ gboolean ret;
+
+ fp = g_fopen(to_file, "w+");
+
+ if (fp == NULL) {
+ g_print("Failed to export %s\n", to_file);
+ return FALSE;
+ }
+
+ ret = remmina_rdp_file_export_channel(remminafile, fp);
+ fclose(fp);
+
+ return ret;
+}
diff --git a/plugins/rdp/rdp_file.h b/plugins/rdp/rdp_file.h
new file mode 100644
index 0000000..0196293
--- /dev/null
+++ b/plugins/rdp/rdp_file.h
@@ -0,0 +1,48 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#include <glib.h>
+#include "common/remmina_plugin.h"
+
+G_BEGIN_DECLS
+
+gboolean remmina_rdp_file_import_test(RemminaFilePlugin *plugin, const gchar *from_file);
+RemminaFile *remmina_rdp_file_import(RemminaFilePlugin *plugin, const gchar *from_file);
+gboolean remmina_rdp_file_export_test(RemminaFilePlugin *plugin, RemminaFile *remminafile);
+gboolean remmina_rdp_file_export(RemminaFilePlugin *plugin, RemminaFile *remminafile, const gchar *to_file);
+
+G_END_DECLS
diff --git a/plugins/rdp/rdp_graphics.c b/plugins/rdp/rdp_graphics.c
new file mode 100644
index 0000000..b97f30e
--- /dev/null
+++ b/plugins/rdp/rdp_graphics.c
@@ -0,0 +1,163 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010 Jay Sorg
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com>
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "rdp_plugin.h"
+#include "rdp_event.h"
+#include "rdp_graphics.h"
+
+#include <freerdp/codec/color.h>
+#include <freerdp/codec/bitmap.h>
+#include <winpr/memory.h>
+
+//#define RF_BITMAP
+//#define RF_GLYPH
+
+/* Pointer Class */
+#if FREERDP_VERSION_MAJOR < 3
+#define CONST_ARG const
+#else
+#define CONST_ARG
+#endif
+
+static BOOL rf_Pointer_New(rdpContext* context, rdpPointer* pointer)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginRdpUiObject* ui;
+ rfContext* rfi = (rfContext*)context;
+
+ if (pointer->xorMaskData != 0) {
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_CURSOR;
+ ui->cursor.context = context;
+ ui->cursor.pointer = (rfPointer*)pointer;
+ ui->cursor.type = REMMINA_RDP_POINTER_NEW;
+ return remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui) ? TRUE : FALSE;
+ }
+ return FALSE;
+}
+
+static void rf_Pointer_Free(rdpContext* context, rdpPointer* pointer)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginRdpUiObject* ui;
+ rfContext* rfi = (rfContext*)context;
+
+ if (G_IS_OBJECT(((rfPointer*)pointer)->cursor))
+ {
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_CURSOR;
+ ui->cursor.context = context;
+ ui->cursor.pointer = (rfPointer*)pointer;
+ ui->cursor.type = REMMINA_RDP_POINTER_FREE;
+ remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui);
+ }
+}
+
+static BOOL rf_Pointer_Set(rdpContext* context, CONST_ARG rdpPointer* pointer)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginRdpUiObject* ui;
+ rfContext* rfi = (rfContext*)context;
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_CURSOR;
+ ui->cursor.pointer = (rfPointer*)pointer;
+ ui->cursor.type = REMMINA_RDP_POINTER_SET;
+
+ return remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui) ? TRUE : FALSE;
+
+}
+
+static BOOL rf_Pointer_SetNull(rdpContext* context)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginRdpUiObject* ui;
+ rfContext* rfi = (rfContext*)context;
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_CURSOR;
+ ui->cursor.type = REMMINA_RDP_POINTER_NULL;
+
+ return remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui) ? TRUE : FALSE;
+}
+
+static BOOL rf_Pointer_SetDefault(rdpContext* context)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginRdpUiObject* ui;
+ rfContext* rfi = (rfContext*)context;
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_CURSOR;
+ ui->cursor.type = REMMINA_RDP_POINTER_DEFAULT;
+
+ return remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui) ? TRUE : FALSE;
+}
+
+static BOOL rf_Pointer_SetPosition(rdpContext* context, UINT32 x, UINT32 y)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginRdpUiObject* ui;
+ rfContext* rfi = (rfContext*)context;
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_CURSOR;
+ ui->cursor.type = REMMINA_RDP_POINTER_SETPOS;
+ ui->pos.x = x;
+ ui->pos.y = y;
+
+ return remmina_rdp_event_queue_ui_sync_retint(rfi->protocol_widget, ui) ? TRUE : FALSE;
+}
+
+/* Graphics Module */
+
+void rf_register_graphics(rdpGraphics* graphics)
+{
+ TRACE_CALL(__func__);
+ rdpPointer pointer={0};
+
+ pointer.size = sizeof(rfPointer);
+
+ pointer.New = rf_Pointer_New;
+ pointer.Free = rf_Pointer_Free;
+ pointer.Set = rf_Pointer_Set;
+ pointer.SetNull = rf_Pointer_SetNull;
+ pointer.SetDefault = rf_Pointer_SetDefault;
+ pointer.SetPosition = rf_Pointer_SetPosition;
+
+ graphics_register_pointer(graphics, &pointer);
+}
diff --git a/plugins/rdp/rdp_graphics.h b/plugins/rdp/rdp_graphics.h
new file mode 100644
index 0000000..1c290f9
--- /dev/null
+++ b/plugins/rdp/rdp_graphics.h
@@ -0,0 +1,40 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#include "rdp_plugin.h"
+
+void rf_register_graphics(rdpGraphics *graphics);
diff --git a/plugins/rdp/rdp_monitor.c b/plugins/rdp/rdp_monitor.c
new file mode 100644
index 0000000..4aaf413
--- /dev/null
+++ b/plugins/rdp/rdp_monitor.c
@@ -0,0 +1,231 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2020 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "rdp_plugin.h"
+#include "rdp_monitor.h"
+
+/** @ToDo Utility functions should be moved somewhere else */
+gint remmina_rdp_utils_strpos(const gchar *haystack, const gchar *needle)
+{
+ TRACE_CALL(__func__);
+ const gchar *sub;
+
+ if (!*needle)
+ return -1;
+
+ sub = strstr(haystack, needle);
+ if (!sub)
+ return -1;
+
+ return sub - haystack;
+}
+
+/* https://github.com/adlocode/xfwm4/blob/1d21be9ffc0fa1cea91905a07d1446c5227745f4/common/xfwm-common.c */
+
+
+/**
+ * Set the MonitorIDs, the maxwidth and maxheight
+ *
+ * - number of monitors
+ * - Geometry of each
+ * - Choosen by the user
+ * - Primary monitor
+ * - Otherwise use current monitor as the origin
+ *
+ * The origin must be 0,0
+ */
+void remmina_rdp_monitor_get (rfContext *rfi, gchar **monitorids, guint32 *maxwidth, guint32 *maxheight)
+{
+ TRACE_CALL(__func__);
+
+ GdkDisplay *display;
+ GdkMonitor *monitor;
+ gboolean has_custom_monitors = FALSE;
+
+ gboolean primary_found = FALSE;
+
+ gint n_monitors;
+ gint scale;
+ gint index = 0;
+ gint count = 0;
+
+ static gchar buffer[256];
+ gint buffer_offset = 0;
+
+ GdkRectangle geometry = { 0, 0, 0, 0 };
+ GdkRectangle tempgeom = { 0, 0, 0, 0 };
+ GdkRectangle destgeom = { 0, 0, 0, 0 };
+ rdpSettings* settings;
+ if (!rfi || !rfi->clientContext.context.settings)
+ return;
+
+ settings = rfi->clientContext.context.settings;
+
+ *maxwidth = freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth);
+ *maxheight = freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight);
+
+ display = gdk_display_get_default ();
+ n_monitors = gdk_display_get_n_monitors(display);
+
+ /* Get monitor at windows curently in use */
+ //w = gtk_widget_get_window(rfi->drawing_area);
+
+ //current_monitor = gdk_display_get_monitor_at_window (display, w);
+
+ /* we got monitorids as options */
+ if (*monitorids)
+ has_custom_monitors = TRUE;
+
+ rdpMonitor* base = (rdpMonitor *)freerdp_settings_get_pointer(settings, FreeRDP_MonitorDefArray);
+ for (gint i = 0; i < n_monitors; ++i) {
+ rdpMonitor* current;
+ if (has_custom_monitors) {
+ REMMINA_PLUGIN_DEBUG("We have custom monitors");
+ gchar itoc[11];
+ snprintf(itoc, sizeof(itoc), "%d", i);
+ if (remmina_rdp_utils_strpos(*monitorids, itoc) < 0 ) {
+ REMMINA_PLUGIN_DEBUG("Monitor n %d it's out of the provided list", i);
+ index += 1;
+ continue;
+ }
+ }
+
+ monitor = gdk_display_get_monitor(display, i);
+ if (monitor == NULL) {
+ REMMINA_PLUGIN_DEBUG("Monitor n %d does not exist or is not active", i);
+ index +=1;
+ continue;
+ }
+
+ monitor = gdk_display_get_monitor(display, index);
+ current = &base[index];
+ REMMINA_PLUGIN_DEBUG("Monitor n %d", index);
+ /* If the desktop env in use doesn't have the working area concept
+ * gdk_monitor_get_workarea will return the monitor geometry*/
+ //gdk_monitor_get_workarea (monitor, &geometry);
+ gdk_monitor_get_geometry (monitor, &geometry);
+ current->x = geometry.x;
+ REMMINA_PLUGIN_DEBUG("Monitor n %d x: %d", index, geometry.x);
+ current->y = geometry.y;
+ REMMINA_PLUGIN_DEBUG("Monitor n %d y: %d", index, geometry.y);
+ /* geometry contain the application geometry, to obtain the real one
+ * we must multiply by the scale factor */
+ scale = gdk_monitor_get_scale_factor (monitor);
+ REMMINA_PLUGIN_DEBUG("Monitor n %d scale: %d", index, scale);
+ geometry.x *= scale;
+ geometry.y *= scale;
+ geometry.width *= scale;
+ geometry.height *= scale;
+ REMMINA_PLUGIN_DEBUG("Monitor n %d width: %d", index, geometry.width);
+ REMMINA_PLUGIN_DEBUG("Monitor n %d height: %d", index, geometry.height);
+ current->width = geometry.width;
+ current->height = geometry.height;
+ current->attributes.physicalHeight = gdk_monitor_get_height_mm (monitor);
+ REMMINA_PLUGIN_DEBUG("Monitor n %d physical height: %d", i, current->attributes.physicalHeight);
+ current->attributes.physicalWidth = gdk_monitor_get_width_mm (monitor);
+ REMMINA_PLUGIN_DEBUG("Monitor n %d physical width: %d", i, current->attributes.physicalWidth);
+ current->orig_screen = index;
+ if (!primary_found) {
+ freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftX, current->x);
+ freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftY, current->y);
+ }
+ if (gdk_monitor_is_primary(monitor)) {
+ REMMINA_PLUGIN_DEBUG ("Primary monitor found with id: %d", index);
+ current->is_primary = TRUE;
+ primary_found = TRUE;
+ if (current->x != 0 || current->y != 0)
+ {
+ REMMINA_PLUGIN_DEBUG ("Primary monitor not at 0,0 coordinates: %d", index);
+ freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftX, current->x);
+ freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftY, current->y);
+ }
+ } else {
+ if (!primary_found && current->x == 0 &&
+ current->y == 0)
+ {
+ REMMINA_PLUGIN_DEBUG ("Monitor %d has 0,0 coordinates", index);
+ current->is_primary = TRUE;
+ freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftX, current->x);
+ freerdp_settings_set_uint32(settings, FreeRDP_MonitorLocalShiftY, current->y);
+ primary_found = TRUE;
+ REMMINA_PLUGIN_DEBUG ("Primary monitor set to id: %d", index);
+ }
+ }
+ REMMINA_PLUGIN_DEBUG ("Local X Shift: %d", freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftX));
+ REMMINA_PLUGIN_DEBUG ("Local Y Shift: %d", freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftY));
+ //current->x =
+ //current->x - freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftX);
+ //REMMINA_PLUGIN_DEBUG("Monitor n %d calculated x: %d", index, current->x);
+ //current->y =
+ //current->y - freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftY);
+ //REMMINA_PLUGIN_DEBUG("Monitor n %d calculated y: %d", index, current->y);
+
+ if (buffer_offset == 0)
+ buffer_offset = g_sprintf(buffer + buffer_offset, "%d", i);
+ else
+ buffer_offset = g_sprintf(buffer + buffer_offset, ",%d", i);
+ REMMINA_PLUGIN_DEBUG("Monitor IDs buffer: %s", buffer);
+ gdk_rectangle_union(&tempgeom, &geometry, &destgeom);
+ memcpy(&tempgeom, &destgeom, sizeof tempgeom);
+ count++;
+ index++;
+
+ }
+ freerdp_settings_set_uint32(settings, FreeRDP_MonitorCount, index);
+ /* Subtract monitor shift from monitor variables for server-side use.
+ * We maintain monitor shift value as Window requires the primary monitor to have a
+ * coordinate of 0,0 In some X configurations, no monitor may have a coordinate of 0,0. This
+ * can also be happen if the user requests specific monitors from the command-line as well.
+ * So, we make sure to translate our primary monitor's upper-left corner to 0,0 on the
+ * server.
+ */
+ for (gint i = 0; i < freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount); i++)
+ {
+ rdpMonitor* current = &base[i];
+ current->x =
+ current->x - freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftX);
+ REMMINA_PLUGIN_DEBUG("Monitor n %d calculated x: %d", i, current->x);
+ current->y =
+ current->y - freerdp_settings_get_uint32(settings, FreeRDP_MonitorLocalShiftY);
+ REMMINA_PLUGIN_DEBUG("Monitor n %d calculated y: %d", i, current->y);
+ }
+
+ REMMINA_PLUGIN_DEBUG("%d monitors on %d have been configured", freerdp_settings_get_uint32(settings, FreeRDP_MonitorCount), count);
+ *maxwidth = destgeom.width;
+ *maxheight = destgeom.height;
+ REMMINA_PLUGIN_DEBUG("maxw and maxh: %ux%u", *maxwidth, *maxheight);
+ if (n_monitors > 1)
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportMonitorLayoutPdu, TRUE);
+ *monitorids = g_strdup(buffer);
+}
diff --git a/plugins/rdp/rdp_monitor.h b/plugins/rdp/rdp_monitor.h
new file mode 100644
index 0000000..1c8f08d
--- /dev/null
+++ b/plugins/rdp/rdp_monitor.h
@@ -0,0 +1,45 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2020 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+
+#include <freerdp/freerdp.h>
+#include "rdp_plugin.h"
+
+G_BEGIN_DECLS
+
+void remmina_rdp_monitor_get (rfContext *rfi, gchar **monitorids, guint32 *maxwidth, guint32 *maxheight);
+
+G_END_DECLS
diff --git a/plugins/rdp/rdp_plugin.c b/plugins/rdp/rdp_plugin.c
new file mode 100644
index 0000000..68ea505
--- /dev/null
+++ b/plugins/rdp/rdp_plugin.c
@@ -0,0 +1,3363 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2022 Antenore Gatta, Giovanni Panozzo
+ * Copyright (C) 2022-2023 Antenore Gatta, Giovanni Panozzo, Hiroyuki Tanaka
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+
+#define _GNU_SOURCE
+
+#include "remmina/plugin.h"
+#include "rdp_plugin.h"
+#include "rdp_event.h"
+#include "rdp_graphics.h"
+#include "rdp_file.h"
+#include "rdp_settings.h"
+#include "rdp_cliprdr.h"
+#include "rdp_monitor.h"
+#include "rdp_channels.h"
+
+#include <errno.h>
+#include <pthread.h>
+#include <time.h>
+#include <sys/time.h>
+#ifdef GDK_WINDOWING_X11
+#include <cairo/cairo-xlib.h>
+#else
+#include <cairo/cairo.h>
+#endif
+#include <ctype.h>
+#include <freerdp/addin.h>
+#include <freerdp/assistance.h>
+#if FREERDP_VERSION_MAJOR >= 3
+#include <freerdp/channels/rdp2tcp.h>
+#else
+#define RDP2TCP_DVC_CHANNEL_NAME "rdp2tcp"
+#endif
+#include <freerdp/client/channels.h>
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/client/cmdline.h>
+#include <freerdp/constants.h>
+#include <freerdp/error.h>
+#include <freerdp/event.h>
+#include <freerdp/freerdp.h>
+#include <freerdp/settings.h>
+#include <winpr/cmdline.h>
+#include <winpr/memory.h>
+
+#ifdef HAVE_CUPS
+#include <cups/cups.h>
+#endif
+
+#include <unistd.h>
+#include <string.h>
+
+#ifdef GDK_WINDOWING_X11
+#include <X11/Xlib.h>
+#include <X11/XKBlib.h>
+#include <gdk/gdkx.h>
+#elif defined(GDK_WINDOWING_WAYLAND)
+#include <gdk/gdkwayland.h>
+#endif
+
+#if defined(__FreeBSD__)
+#include <pthread_np.h>
+#endif
+
+#include <freerdp/locale/keyboard.h>
+
+#define REMMINA_RDP_FEATURE_TOOL_REFRESH 1
+#define REMMINA_RDP_FEATURE_SCALE 2
+#define REMMINA_RDP_FEATURE_UNFOCUS 3
+#define REMMINA_RDP_FEATURE_TOOL_SENDCTRLALTDEL 4
+#define REMMINA_RDP_FEATURE_DYNRESUPDATE 5
+#define REMMINA_RDP_FEATURE_MULTIMON 6
+#define REMMINA_RDP_FEATURE_VIEWONLY 7
+
+#define REMMINA_CONNECTION_TYPE_NONE 0
+
+#if FREERDP_VERSION_MAJOR >= 3
+ #define CLPARAM const char
+#else
+ #define CLPARAM char
+#endif
+
+#if FREERDP_VERSION_MAJOR < 3
+static HANDLE freerdp_abort_event(rdpContext* context) {
+ WINPR_ASSERT(context);
+ return context->abortEvent;
+}
+
+static BOOL freerdp_settings_set_pointer_len(rdpSettings* settings, size_t id, const void* data, size_t len)
+{
+ switch(id) {
+ case FreeRDP_LoadBalanceInfo:
+ free(settings->LoadBalanceInfo);
+ settings->LoadBalanceInfo = _strdup(data);
+ settings->LoadBalanceInfoLength = len;
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static void freerdp_abort_connect_context(rdpContext* context) {
+ WINPR_ASSERT(context);
+ freerdp_abort_connect(context->instance);
+}
+#endif
+
+RemminaPluginService *remmina_plugin_service = NULL;
+
+static BOOL gfx_h264_available = FALSE;
+// keep track of last interaction time for keep alive
+static time_t last_time;
+
+/* Compatibility: these functions have been introduced with https://github.com/FreeRDP/FreeRDP/commit/8c5d96784d
+ * and are missing on older FreeRDP, so we add them here.
+ * They should be removed from here after all distributed versions of FreeRDP (libwinpr) will have
+ * CommandLineParseCommaSeparatedValuesEx() onboard.
+ *
+ * (C) Copyright goes to the FreeRDP authors.
+ */
+static CLPARAM **remmina_rdp_CommandLineParseCommaSeparatedValuesEx(const char *name, const char *list, size_t *count)
+{
+ TRACE_CALL(__func__);
+#if FREERDP_CHECK_VERSION(2, 0, 0)
+ return (CLPARAM **)CommandLineParseCommaSeparatedValuesEx(name, list, count);
+#else
+ char **p;
+ char *str;
+ size_t nArgs;
+ size_t index;
+ size_t nCommas;
+ size_t prefix, len;
+
+ nCommas = 0;
+
+ if (count == NULL)
+ return NULL;
+
+ *count = 0;
+
+ if (!list) {
+ if (name) {
+ size_t len = strlen(name);
+ p = (char **)calloc(2UL + len, sizeof(char *));
+
+ if (p) {
+ char *dst = (char *)&p[1];
+ p[0] = dst;
+ sprintf_s(dst, len + 1, "%s", name);
+ *count = 1;
+ return p;
+ }
+ }
+
+ return NULL;
+ }
+
+ {
+ const char *it = list;
+
+ while ((it = strchr(it, ',')) != NULL) {
+ it++;
+ nCommas++;
+ }
+ }
+
+ nArgs = nCommas + 1;
+
+ if (name)
+ nArgs++;
+
+ prefix = (nArgs + 1UL) * sizeof(char *);
+ len = strlen(list);
+ p = (char **)calloc(len + prefix + 1, sizeof(char *));
+
+ if (!p)
+ return NULL;
+
+ str = &((char *)p)[prefix];
+ memcpy(str, list, len);
+
+ if (name)
+ p[0] = (char *)name;
+
+ for (index = name ? 1 : 0; index < nArgs; index++) {
+ char *comma = strchr(str, ',');
+ p[index] = str;
+
+ if (comma) {
+ str = comma + 1;
+ *comma = '\0';
+ }
+ }
+
+ *count = nArgs;
+ return p;
+#endif
+}
+
+static CLPARAM **remmina_rdp_CommandLineParseCommaSeparatedValues(const char *list, size_t *count)
+{
+ TRACE_CALL(__func__);
+ return remmina_rdp_CommandLineParseCommaSeparatedValuesEx(NULL, list, count);
+}
+
+/*
+ * End of CommandLineParseCommaSeparatedValuesEx() compatibility and copyright
+ */
+static BOOL rf_process_event_queue(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ UINT16 flags;
+ rdpInput *input;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaPluginRdpEvent *event;
+ DISPLAY_CONTROL_MONITOR_LAYOUT *dcml;
+ CLIPRDR_FORMAT_DATA_RESPONSE response = { 0 };
+ RemminaFile *remminafile;
+
+ if (rfi->event_queue == NULL)
+ return true;
+
+ input = rfi->clientContext.context.input;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ while ((event = (RemminaPluginRdpEvent *)g_async_queue_try_pop(rfi->event_queue)) != NULL) {
+ time(&last_time); //update last user interaction time
+ switch (event->type) {
+ case REMMINA_RDP_EVENT_TYPE_SCANCODE:
+
+ if (event->key_event.extended1){
+ flags = KBD_FLAGS_EXTENDED1;
+ }
+ else{
+ flags = event->key_event.extended ? KBD_FLAGS_EXTENDED : 0;
+ }
+ flags |= event->key_event.up ? KBD_FLAGS_RELEASE : KBD_FLAGS_DOWN;
+ input->KeyboardEvent(input, flags, event->key_event.key_code);
+ break;
+
+ case REMMINA_RDP_EVENT_TYPE_SCANCODE_UNICODE:
+ /*
+ * TS_UNICODE_KEYBOARD_EVENT RDP message, see https://msdn.microsoft.com/en-us/library/cc240585.aspx
+ */
+ flags = event->key_event.up ? KBD_FLAGS_RELEASE : KBD_FLAGS_DOWN;
+ input->UnicodeKeyboardEvent(input, flags, event->key_event.unicode_code);
+ break;
+
+ case REMMINA_RDP_EVENT_TYPE_MOUSE:
+ if (event->mouse_event.extended)
+ input->ExtendedMouseEvent(input, event->mouse_event.flags,
+ event->mouse_event.x, event->mouse_event.y);
+ else
+ input->MouseEvent(input, event->mouse_event.flags,
+ event->mouse_event.x, event->mouse_event.y);
+ break;
+
+ case REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_LIST:
+ rfi->clipboard.context->ClientFormatList(rfi->clipboard.context, event->clipboard_formatlist.pFormatList);
+ free(event->clipboard_formatlist.pFormatList);
+ break;
+
+ case REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_RESPONSE:
+ {
+ UINT32 msgFlags = (event->clipboard_formatdataresponse.data) ? CB_RESPONSE_OK : CB_RESPONSE_FAIL;
+#if FREERDP_VERSION_MAJOR >= 3
+ response.common.msgFlags = msgFlags;
+ response.common.dataLen = event->clipboard_formatdataresponse.size;
+#else
+ response.msgFlags = msgFlags;
+ response.dataLen = event->clipboard_formatdataresponse.size;
+#endif
+ response.requestedFormatData = event->clipboard_formatdataresponse.data;
+ rfi->clipboard.context->ClientFormatDataResponse(rfi->clipboard.context, &response);
+ }
+ break;
+
+ case REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_REQUEST:
+ REMMINA_PLUGIN_DEBUG("Sending client FormatDataRequest to server");
+ gettimeofday(&(rfi->clipboard.clientformatdatarequest_tv), NULL);
+ rfi->clipboard.context->ClientFormatDataRequest(rfi->clipboard.context, event->clipboard_formatdatarequest.pFormatDataRequest);
+ free(event->clipboard_formatdatarequest.pFormatDataRequest);
+ break;
+
+ case REMMINA_RDP_EVENT_TYPE_SEND_MONITOR_LAYOUT:
+ if (remmina_plugin_service->file_get_int(remminafile, "multimon", FALSE)) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_UseMultimon, TRUE);
+ /* TODO Add an option for this */
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ForceMultimon, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_Fullscreen, TRUE);
+ /* got some crashes with g_malloc0, to be investigated */
+ dcml = calloc(freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_MonitorCount), sizeof(DISPLAY_CONTROL_MONITOR_LAYOUT));
+ REMMINA_PLUGIN_DEBUG("REMMINA_RDP_EVENT_TYPE_SEND_MONITOR_LAYOUT:");
+ if (!dcml)
+ break;
+
+ const rdpMonitor *base = freerdp_settings_get_pointer(rfi->clientContext.context.settings, FreeRDP_MonitorDefArray);
+ for (gint i = 0; i < freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_MonitorCount); ++i) {
+ const rdpMonitor *current = &base[i];
+ REMMINA_PLUGIN_DEBUG("Sending display layout for monitor n° %d", i);
+ dcml[i].Flags = (current->is_primary ? DISPLAY_CONTROL_MONITOR_PRIMARY : 0);
+ REMMINA_PLUGIN_DEBUG("Monitor %d is primary: %d", i, dcml[i].Flags);
+ dcml[i].Left = current->x;
+ REMMINA_PLUGIN_DEBUG("Monitor %d x: %d", i, dcml[i].Left);
+ dcml[i].Top = current->y;
+ REMMINA_PLUGIN_DEBUG("Monitor %d y: %d", i, dcml[i].Top);
+ dcml[i].Width = current->width;
+ REMMINA_PLUGIN_DEBUG("Monitor %d width: %d", i, dcml[i].Width);
+ dcml[i].Height = current->height;
+ REMMINA_PLUGIN_DEBUG("Monitor %d height: %d", i, dcml[i].Height);
+ dcml[i].PhysicalWidth = current->attributes.physicalWidth;
+ REMMINA_PLUGIN_DEBUG("Monitor %d physical width: %d", i, dcml[i].PhysicalWidth);
+ dcml[i].PhysicalHeight = current->attributes.physicalHeight;
+ REMMINA_PLUGIN_DEBUG("Monitor %d physical height: %d", i, dcml[i].PhysicalHeight);
+ if (current->attributes.orientation)
+ dcml[i].Orientation = current->attributes.orientation;
+ else
+ dcml[i].Orientation = event->monitor_layout.desktopOrientation;
+ REMMINA_PLUGIN_DEBUG("Monitor %d orientation: %d", i, dcml[i].Orientation);
+ dcml[i].DesktopScaleFactor = event->monitor_layout.desktopScaleFactor;
+ dcml[i].DeviceScaleFactor = event->monitor_layout.deviceScaleFactor;
+ }
+ rfi->dispcontext->SendMonitorLayout(rfi->dispcontext, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_MonitorCount), dcml);
+ g_free(dcml);
+ } else {
+ dcml = g_malloc0(sizeof(DISPLAY_CONTROL_MONITOR_LAYOUT));
+ if (dcml) {
+ dcml->Flags = DISPLAY_CONTROL_MONITOR_PRIMARY;
+ dcml->Width = event->monitor_layout.width;
+ dcml->Height = event->monitor_layout.height;
+ dcml->Orientation = event->monitor_layout.desktopOrientation;
+ dcml->DesktopScaleFactor = event->monitor_layout.desktopScaleFactor;
+ dcml->DeviceScaleFactor = event->monitor_layout.deviceScaleFactor;
+ rfi->dispcontext->SendMonitorLayout(rfi->dispcontext, 1, dcml);
+ g_free(dcml); \
+ }
+ }
+ break;
+ case REMMINA_RDP_EVENT_DISCONNECT:
+ /* Disconnect requested via GUI (i.e: tab destroy/close) */
+ freerdp_abort_connect_context(&rfi->clientContext.context);
+ break;
+ }
+
+ g_free(event);
+ }
+
+ return true;
+}
+
+static gboolean remmina_rdp_tunnel_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ /* Opens the optional SSH tunnel if needed.
+ * Used also when reopening the same tunnel for a FreeRDP reconnect.
+ * Returns TRUE if all OK, and setups correct rfi->Settings values
+ * with connection and certificate parameters */
+
+ gchar *hostport;
+ gchar *s;
+ gchar *host;
+ gchar *cert_host;
+ gint cert_port;
+ gint port;
+
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ REMMINA_PLUGIN_DEBUG("Tunnel init");
+ hostport = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, 3389, FALSE);
+ if (hostport == NULL)
+ return FALSE;
+
+ remmina_plugin_service->get_server_port(hostport, 3389, &host, &port);
+
+ if (host[0] == 0)
+ return FALSE;
+
+ REMMINA_PLUGIN_DEBUG("protocol_plugin_start_direct_tunnel() returned %s", hostport);
+
+ cert_host = host;
+ cert_port = port;
+
+ if (!rfi->is_reconnecting) {
+ /* settings->CertificateName and settings->ServerHostname is created
+ * only on 1st connect, not on reconnections */
+
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname, host);
+
+ if (cert_port == 3389) {
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_CertificateName, cert_host);
+ } else {
+ s = g_strdup_printf("%s:%d", cert_host, cert_port);
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_CertificateName, s);
+ g_free(s);
+ }
+ }
+
+ REMMINA_PLUGIN_DEBUG("Tunnel has been optionally initialized. Now connecting to %s:%d", host, port);
+
+ if (cert_host != host) g_free(cert_host);
+ g_free(host);
+ g_free(hostport);
+
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ServerPort, port);
+
+ return TRUE;
+}
+
+static BOOL rf_auto_reconnect(rfContext *rfi)
+{
+ TRACE_CALL(__func__);
+ rdpSettings *settings = rfi->clientContext.context.settings;
+ RemminaPluginRdpUiObject *ui;
+ time_t treconn;
+ gchar *cval;
+ gint maxattempts;
+
+ RemminaProtocolWidget *gp = rfi->protocol_widget;
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ rfi->is_reconnecting = TRUE;
+ rfi->stop_reconnecting_requested = FALSE;
+
+ /* Get the value set in FreeRDP_AutoReconnectMaxRetries (20) */
+ maxattempts = freerdp_settings_get_uint32(settings, FreeRDP_AutoReconnectMaxRetries);
+ REMMINA_PLUGIN_DEBUG("maxattempts from default: %d", maxattempts);
+ /* Get the value from the global preferences if any */
+ if ((cval = remmina_plugin_service->pref_get_value("rdp_reconnect_attempts")) != NULL)
+ maxattempts = atoi(cval);
+ REMMINA_PLUGIN_DEBUG("maxattempts from general preferences: %d", maxattempts);
+ /* Get the value from the profile if any, otherwise uses the value of maxattempts */
+ maxattempts = remmina_plugin_service->file_get_int(remminafile, "rdp_reconnect_attempts", maxattempts);
+ REMMINA_PLUGIN_DEBUG("maxattempts from general plugin: %d", maxattempts);
+ /* If maxattemps is <= 0, we get the value from FreeRDP_AutoReconnectMaxRetries (20) */
+ if (maxattempts <= 0)
+ maxattempts = freerdp_settings_get_uint32(settings, FreeRDP_AutoReconnectMaxRetries);
+ freerdp_settings_set_uint32(settings, FreeRDP_AutoReconnectMaxRetries, maxattempts);
+ REMMINA_PLUGIN_DEBUG("maxattempts set to: %d", maxattempts);
+
+ rfi->reconnect_maxattempts = maxattempts;
+ rfi->reconnect_nattempt = 0;
+
+ /* Only auto reconnect on network disconnects. */
+ switch (freerdp_error_info(rfi->clientContext.context.instance)) {
+ case ERRINFO_GRAPHICS_SUBSYSTEM_FAILED:
+ /* Disconnected by server hitting a bug or resource limit */
+ break;
+ case ERRINFO_SUCCESS:
+ /* A network disconnect was detected */
+ break;
+ default:
+ rfi->is_reconnecting = FALSE;
+ return FALSE;
+ }
+
+ if (!freerdp_settings_get_bool(settings, FreeRDP_AutoReconnectionEnabled)) {
+ /* No auto-reconnect - just quit */
+ rfi->is_reconnecting = FALSE;
+ return FALSE;
+ }
+
+ /* A network disconnect was detected and we should try to reconnect */
+ REMMINA_PLUGIN_DEBUG("[%s] network disconnection detected, initiating reconnection attempt",
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_RECONNECT_PROGRESS;
+ remmina_rdp_event_queue_ui_async(rfi->protocol_widget, ui);
+
+ /* Sleep half a second to allow:
+ * - processing of the UI event we just pushed on the queue
+ * - better network conditions
+ * Remember: We hare on a thread, so the main gui won’t lock */
+ usleep(500000);
+
+ /* Perform an auto-reconnect. */
+ while (TRUE) {
+ /* Quit retrying if max retries has been exceeded */
+ if (rfi->reconnect_nattempt++ >= rfi->reconnect_maxattempts) {
+ REMMINA_PLUGIN_DEBUG("[%s] maximum number of reconnection attempts exceeded.",
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ }
+
+ if (rfi->stop_reconnecting_requested) {
+ REMMINA_PLUGIN_DEBUG("[%s] reconnect request loop interrupted by user.",
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ }
+
+ /* Attempt the next reconnect */
+ REMMINA_PLUGIN_DEBUG("[%s] reconnection, attempt #%d of %d",
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname), rfi->reconnect_nattempt, rfi->reconnect_maxattempts);
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_RECONNECT_PROGRESS;
+ remmina_rdp_event_queue_ui_async(rfi->protocol_widget, ui);
+
+ treconn = time(NULL);
+
+ /* Reconnect the SSH tunnel, if needed */
+ if (!remmina_rdp_tunnel_init(rfi->protocol_widget)) {
+ REMMINA_PLUGIN_DEBUG("[%s] unable to recreate tunnel with remmina_rdp_tunnel_init.",
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ } else {
+ if (freerdp_reconnect(rfi->clientContext.context.instance)) {
+ /* Reconnection is successful */
+ REMMINA_PLUGIN_DEBUG("[%s] reconnected.", freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ rfi->is_reconnecting = FALSE;
+ return TRUE;
+ }
+ }
+
+ /* Wait until 5 secs have elapsed from last reconnect attempt, while checking for rfi->stop_reconnecting_requested */
+ while (time(NULL) - treconn < 5) {
+ if (rfi->stop_reconnecting_requested)
+ break;
+ usleep(200000); // 200ms sleep
+ }
+ }
+
+ rfi->is_reconnecting = FALSE;
+ return FALSE;
+}
+
+static BOOL rf_begin_paint(rdpContext *context)
+{
+ TRACE_CALL(__func__);
+ rdpGdi *gdi;
+
+ if (!context)
+ return FALSE;
+
+ gdi = context->gdi;
+ if (!gdi || !gdi->primary || !gdi->primary->hdc || !gdi->primary->hdc->hwnd)
+ return FALSE;
+
+ return TRUE;
+}
+
+static BOOL rf_end_paint(rdpContext *context)
+{
+ TRACE_CALL(__func__);
+ rdpGdi *gdi;
+ rfContext *rfi;
+ RemminaPluginRdpUiObject *ui;
+ int i, ninvalid;
+ region *reg;
+ HGDI_RGN cinvalid;
+
+ gdi = context->gdi;
+ rfi = (rfContext *)context;
+
+ if (gdi == NULL || gdi->primary == NULL || gdi->primary->hdc == NULL || gdi->primary->hdc->hwnd == NULL)
+ return TRUE;
+
+ if (gdi->primary->hdc->hwnd->invalid->null)
+ return TRUE;
+
+ if (gdi->primary->hdc->hwnd->ninvalid < 1)
+ return TRUE;
+
+ ninvalid = gdi->primary->hdc->hwnd->ninvalid;
+ cinvalid = gdi->primary->hdc->hwnd->cinvalid;
+ reg = (region *)g_malloc(sizeof(region) * ninvalid);
+ for (i = 0; i < ninvalid; i++) {
+ reg[i].x = cinvalid[i].x;
+ reg[i].y = cinvalid[i].y;
+ reg[i].w = cinvalid[i].w;
+ reg[i].h = cinvalid[i].h;
+ }
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_UPDATE_REGIONS;
+ ui->reg.ninvalid = ninvalid;
+ ui->reg.ureg = reg;
+
+ remmina_rdp_event_queue_ui_async(rfi->protocol_widget, ui);
+
+
+ gdi->primary->hdc->hwnd->invalid->null = TRUE;
+ gdi->primary->hdc->hwnd->ninvalid = 0;
+
+
+ return TRUE;
+}
+
+static BOOL rf_desktop_resize(rdpContext *context)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+ RemminaPluginRdpUiObject *ui;
+ UINT32 w, h;
+
+ rfi = (rfContext *)context;
+ gp = rfi->protocol_widget;
+
+ w = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth);
+ h = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight);
+ remmina_plugin_service->protocol_plugin_set_width(gp, w);
+ remmina_plugin_service->protocol_plugin_set_height(gp, h);
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_EVENT;
+ ui->event.type = REMMINA_RDP_UI_EVENT_DESTROY_CAIRO_SURFACE;
+ remmina_rdp_event_queue_ui_sync_retint(gp, ui);
+
+ /* Tell libfreerdp to change its internal GDI bitmap width and heigt,
+ * this will also destroy gdi->primary_buffer, making our rfi->surface invalid */
+ gdi_resize(((rdpContext *)rfi)->gdi, w, h);
+
+ /* Call to remmina_rdp_event_update_scale(gp) on the main UI thread,
+ * this will recreate rfi->surface from gdi->primary_buffer */
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_EVENT;
+ ui->event.type = REMMINA_RDP_UI_EVENT_UPDATE_SCALE;
+ remmina_rdp_event_queue_ui_sync_retint(gp, ui);
+
+ remmina_plugin_service->protocol_plugin_desktop_resize(gp);
+
+ return TRUE;
+}
+
+static BOOL rf_play_sound(rdpContext *context, const PLAY_SOUND_UPDATE *play_sound)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+ GdkDisplay *disp;
+
+ rfi = (rfContext *)context;
+ gp = rfi->protocol_widget;
+
+ disp = gtk_widget_get_display(GTK_WIDGET(gp));
+ gdk_display_beep(disp);
+
+ return TRUE;
+}
+
+static BOOL rf_keyboard_set_indicators(rdpContext *context, UINT16 led_flags)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+ GdkDisplay *disp;
+
+ rfi = (rfContext *)context;
+ gp = rfi->protocol_widget;
+ disp = gtk_widget_get_display(GTK_WIDGET(gp));
+
+#ifdef GDK_WINDOWING_X11
+ if (GDK_IS_X11_DISPLAY(disp)) {
+ /* TODO: We are not on the main thread. Will X.Org complain? */
+ Display *x11_display;
+ x11_display = gdk_x11_display_get_xdisplay(disp);
+ XkbLockModifiers(x11_display, XkbUseCoreKbd,
+ LockMask | Mod2Mask,
+ ((led_flags & KBD_SYNC_CAPS_LOCK) ? LockMask : 0) |
+ ((led_flags & KBD_SYNC_NUM_LOCK) ? Mod2Mask : 0)
+ );
+
+ /* TODO: Add support to KANA_LOCK and SCROLL_LOCK */
+ }
+#endif
+
+ return TRUE;
+}
+
+static BOOL rf_keyboard_set_ime_status(rdpContext *context, UINT16 imeId, UINT32 imeState,
+ UINT32 imeConvMode)
+{
+ TRACE_CALL(__func__);
+ if (!context)
+ return FALSE;
+
+ /* Unimplemented, we ignore it */
+
+ return TRUE;
+}
+
+static BOOL remmina_rdp_pre_connect(freerdp *instance)
+{
+ TRACE_CALL(__func__);
+ rdpChannels *channels;
+ rdpSettings *settings;
+ rdpContext *context = instance->context;
+
+ settings = context->settings;
+ channels = context->channels;
+ freerdp_settings_set_uint32(settings, FreeRDP_OsMajorType, OSMAJORTYPE_UNIX);
+ freerdp_settings_set_uint32(settings, FreeRDP_OsMinorType, OSMINORTYPE_UNSPECIFIED);
+ freerdp_settings_set_bool(settings, FreeRDP_BitmapCacheEnabled, TRUE);
+ freerdp_settings_set_uint32(settings, FreeRDP_OffscreenSupportLevel, 1);
+
+ PubSub_SubscribeChannelConnected(instance->context->pubSub,
+ remmina_rdp_OnChannelConnectedEventHandler);
+ PubSub_SubscribeChannelDisconnected(instance->context->pubSub,
+ remmina_rdp_OnChannelDisconnectedEventHandler);
+
+ if (!freerdp_client_load_addins(channels, settings))
+ return FALSE;
+
+ return true;
+}
+
+static BOOL remmina_rdp_post_connect(freerdp *instance)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+ RemminaPluginRdpUiObject *ui;
+ UINT32 freerdp_local_color_format;
+
+ rfi = (rfContext *)instance->context;
+ gp = rfi->protocol_widget;
+ rfi->postconnect_error = REMMINA_POSTCONNECT_ERROR_OK;
+
+ rfi->attempt_interactive_authentication = FALSE; // We authenticated!
+
+ rfi->srcBpp = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth);
+
+ if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_RemoteFxCodec) == FALSE)
+ rfi->sw_gdi = TRUE;
+
+ rf_register_graphics(instance->context->graphics);
+
+ REMMINA_PLUGIN_DEBUG("bpp: %d", rfi->bpp);
+ switch (rfi->bpp) {
+ case 24:
+ REMMINA_PLUGIN_DEBUG("CAIRO_FORMAT_RGB24");
+ freerdp_local_color_format = PIXEL_FORMAT_BGRX32;
+ rfi->cairo_format = CAIRO_FORMAT_RGB24;
+ break;
+ case 32:
+ /** Do not use alpha as it's not used with the desktop
+ * CAIRO_FORMAT_ARGB32
+ * See https://gitlab.com/Remmina/Remmina/-/issues/2456
+ */
+ REMMINA_PLUGIN_DEBUG("CAIRO_FORMAT_RGB24");
+ freerdp_local_color_format = PIXEL_FORMAT_BGRA32;
+ rfi->cairo_format = CAIRO_FORMAT_RGB24;
+ break;
+ default:
+ REMMINA_PLUGIN_DEBUG("CAIRO_FORMAT_RGB16_565");
+ freerdp_local_color_format = PIXEL_FORMAT_RGB16;
+ rfi->cairo_format = CAIRO_FORMAT_RGB16_565;
+ break;
+ }
+
+ if (!gdi_init(instance, freerdp_local_color_format)) {
+ rfi->postconnect_error = REMMINA_POSTCONNECT_ERROR_GDI_INIT;
+ return FALSE;
+ }
+
+ if (instance->context->codecs->h264 == NULL && freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264)) {
+ gdi_free(instance);
+ rfi->postconnect_error = REMMINA_POSTCONNECT_ERROR_NO_H264;
+ return FALSE;
+ }
+
+ // pointer_cache_register_callbacks(instance->update);
+ rdpUpdate *update = instance->context->update;
+ update->BeginPaint = rf_begin_paint;
+ update->EndPaint = rf_end_paint;
+ update->DesktopResize = rf_desktop_resize;
+
+ update->PlaySound = rf_play_sound;
+ update->SetKeyboardIndicators = rf_keyboard_set_indicators;
+ update->SetKeyboardImeStatus = rf_keyboard_set_ime_status;
+
+ remmina_rdp_clipboard_init(rfi);
+ rfi->connected = true;
+
+ ui = g_new0(RemminaPluginRdpUiObject, 1);
+ ui->type = REMMINA_RDP_UI_CONNECTED;
+ remmina_rdp_event_queue_ui_async(gp, ui);
+
+ return TRUE;
+}
+
+#if !defined(FREERDP_VERSION_MAJOR) || (FREERDP_VERSION_MAJOR < 3)
+static BOOL remmina_rdp_authenticate(freerdp *instance, char **username, char **password, char **domain)
+{
+ TRACE_CALL(__func__);
+ gchar *s_username, *s_password, *s_domain;
+ gint ret;
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+ gboolean save;
+ gboolean disablepasswordstoring;
+ RemminaFile *remminafile;
+
+ rfi = (rfContext *)instance->context;
+ gp = rfi->protocol_widget;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE);
+
+ ret = remmina_plugin_service->protocol_plugin_init_auth(gp,
+ (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME | REMMINA_MESSAGE_PANEL_FLAG_DOMAIN,
+ _("Enter RDP authentication credentials"),
+ remmina_plugin_service->file_get_string(remminafile, "username"),
+ remmina_plugin_service->file_get_string(remminafile, "password"),
+ remmina_plugin_service->file_get_string(remminafile, "domain"),
+ NULL);
+ if (ret == GTK_RESPONSE_OK) {
+ s_username = remmina_plugin_service->protocol_plugin_init_get_username(gp);
+ if (s_username) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Username, s_username);
+
+ s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp);
+ if (s_password) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Password, s_password);
+
+ s_domain = remmina_plugin_service->protocol_plugin_init_get_domain(gp);
+ if (s_domain) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Domain, s_domain);
+
+ remmina_plugin_service->file_set_string(remminafile, "username", s_username);
+ remmina_plugin_service->file_set_string(remminafile, "domain", s_domain);
+
+ save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp);
+ if (save) {
+ // User has requested to save credentials. We put the password
+ // into remminafile->settings. It will be saved later, on successful connection, by
+ // rcw.c
+ remmina_plugin_service->file_set_string(remminafile, "password", s_password);
+ } else {
+ remmina_plugin_service->file_set_string(remminafile, "password", NULL);
+ }
+
+
+ if (s_username) g_free(s_username);
+ if (s_password) g_free(s_password);
+ if (s_domain) g_free(s_domain);
+
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static BOOL remmina_rdp_gw_authenticate(freerdp *instance, char **username, char **password, char **domain)
+{
+ TRACE_CALL(__func__);
+ gchar *s_username, *s_password, *s_domain;
+ gint ret;
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+ gboolean save;
+ gboolean disablepasswordstoring;
+ gboolean basecredforgw;
+ RemminaFile *remminafile;
+
+ rfi = (rfContext *)instance->context;
+ gp = rfi->protocol_widget;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ if (!remmina_plugin_service->file_get_string(remminafile, "gateway_server"))
+ return false;
+ disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE);
+ basecredforgw = remmina_plugin_service->file_get_int(remminafile, "base-cred-for-gw", FALSE);
+
+ if (basecredforgw) {
+ ret = remmina_plugin_service->protocol_plugin_init_auth(gp,
+ (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME | REMMINA_MESSAGE_PANEL_FLAG_DOMAIN,
+ _("Enter RDP authentication credentials"),
+ remmina_plugin_service->file_get_string(remminafile, "username"),
+ remmina_plugin_service->file_get_string(remminafile, "password"),
+ remmina_plugin_service->file_get_string(remminafile, "domain"),
+ NULL);
+ } else {
+ ret = remmina_plugin_service->protocol_plugin_init_auth(gp,
+ (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME | REMMINA_MESSAGE_PANEL_FLAG_DOMAIN,
+ _("Enter RDP gateway authentication credentials"),
+ remmina_plugin_service->file_get_string(remminafile, "gateway_username"),
+ remmina_plugin_service->file_get_string(remminafile, "gateway_password"),
+ remmina_plugin_service->file_get_string(remminafile, "gateway_domain"),
+ NULL);
+ }
+
+
+ if (ret == GTK_RESPONSE_OK) {
+ s_username = remmina_plugin_service->protocol_plugin_init_get_username(gp);
+ if (s_username) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayUsername, s_username);
+
+ s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp);
+ if (s_password) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayPassword, s_password);
+
+ s_domain = remmina_plugin_service->protocol_plugin_init_get_domain(gp);
+ if (s_domain) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayDomain, s_domain);
+
+ save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp);
+
+ if (basecredforgw) {
+ remmina_plugin_service->file_set_string(remminafile, "username", s_username);
+ remmina_plugin_service->file_set_string(remminafile, "domain", s_domain);
+ if (save)
+ remmina_plugin_service->file_set_string(remminafile, "password", s_password);
+ else
+ remmina_plugin_service->file_set_string(remminafile, "password", NULL);
+ } else {
+ remmina_plugin_service->file_set_string(remminafile, "gateway_username", s_username);
+ remmina_plugin_service->file_set_string(remminafile, "gateway_domain", s_domain);
+ if (save)
+ remmina_plugin_service->file_set_string(remminafile, "gateway_password", s_password);
+ else
+ remmina_plugin_service->file_set_string(remminafile, "gateway_password", NULL);
+ }
+
+ if (s_username) g_free(s_username);
+ if (s_password) g_free(s_password);
+ if (s_domain) g_free(s_domain);
+
+ return true;
+ } else {
+ return false;
+ }
+
+ return true;
+}
+#else
+static BOOL remmina_rdp_authenticate_ex(freerdp* instance, char** username, char** password,
+ char** domain, rdp_auth_reason reason)
+{
+ TRACE_CALL(__func__);
+ gchar *s_username = NULL, *s_password = NULL, *s_domain = NULL;
+ const gchar* key_user = NULL;
+ const gchar* key_domain = NULL;
+ const gchar* key_password = NULL;
+ const gchar* key_title = NULL;
+ gint ret;
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+ gboolean save;
+ gboolean disablepasswordstoring;
+ RemminaFile *remminafile;
+ RemminaMessagePanelFlags flags = REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD | REMMINA_MESSAGE_PANEL_FLAG_USERNAME | REMMINA_MESSAGE_PANEL_FLAG_DOMAIN;
+
+ rfi = (rfContext *)instance->context;
+ gp = rfi->protocol_widget;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE);
+
+ FreeRDP_Settings_Keys_String cfg_key_user = FreeRDP_STRING_UNUSED;
+ FreeRDP_Settings_Keys_String cfg_key_domain = FreeRDP_STRING_UNUSED;
+ FreeRDP_Settings_Keys_String cfg_key_password = FreeRDP_STRING_UNUSED;
+ switch(reason) {
+ case AUTH_NLA:
+ case AUTH_TLS:
+ case AUTH_RDP:
+ key_title = _("Enter RDP authentication credentials");
+ key_user = "username";
+ key_domain = "domain";
+ key_password = "password";
+ cfg_key_user = FreeRDP_Username;
+ cfg_key_domain = FreeRDP_Domain;
+ cfg_key_password = FreeRDP_Password;
+ break;
+ case GW_AUTH_HTTP:
+ case GW_AUTH_RDG:
+ case GW_AUTH_RPC:
+ key_title = _("Enter RDP gateway authentication credentials");
+ key_user = "gateway_username";
+ key_domain = "gateway_domain";
+ key_password = "gateway_password";
+ cfg_key_user = FreeRDP_GatewayUsername;
+ cfg_key_domain = FreeRDP_GatewayDomain;
+ cfg_key_password = FreeRDP_GatewayPassword;
+ break;
+ case AUTH_SMARTCARD_PIN:
+ key_title = _("Enter RDP SmartCard PIN");
+ key_password = "smartcard_pin";
+ flags = 0;
+ break;
+ default:
+ // TODO: Display an error dialog informing the user that the remote requires some mechanism FreeRDP or Remmina currently do not support
+ g_fprintf(stderr, "[authentication] unsupported type %d, access denied", reason);
+ return FALSE;
+ }
+
+ if (!disablepasswordstoring)
+ flags |= REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD;
+
+ ret = remmina_plugin_service->protocol_plugin_init_auth(gp, flags,
+ key_title,
+ remmina_plugin_service->file_get_string(remminafile, key_user),
+ remmina_plugin_service->file_get_string(remminafile, key_password),
+ remmina_plugin_service->file_get_string(remminafile, disablepasswordstoring ? NULL : key_domain),
+ NULL);
+ if (ret == GTK_RESPONSE_OK) {
+ if (cfg_key_user != FreeRDP_STRING_UNUSED)
+ {
+ s_username = remmina_plugin_service->protocol_plugin_init_get_username(gp);
+ if (s_username)
+ freerdp_settings_set_string(rfi->clientContext.context.settings, cfg_key_user, s_username);
+ remmina_plugin_service->file_set_string(remminafile, key_user, s_username);
+ }
+
+ if (cfg_key_password != FreeRDP_STRING_UNUSED)
+ {
+ s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp);
+ if (s_password)
+ freerdp_settings_set_string(rfi->clientContext.context.settings, cfg_key_password, s_password);
+ }
+
+ if (cfg_key_domain != FreeRDP_STRING_UNUSED) {
+ s_domain = remmina_plugin_service->protocol_plugin_init_get_domain(gp);
+ if (s_domain)
+ freerdp_settings_set_string(rfi->clientContext.context.settings, cfg_key_domain, s_domain);
+ remmina_plugin_service->file_set_string(remminafile, key_domain, s_domain);
+ }
+
+ save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp);
+ if (save) {
+ // User has requested to save credentials. We put the password
+ // into remminafile->settings. It will be saved later, on successful connection, by
+ // rcw.c
+ remmina_plugin_service->file_set_string(remminafile, key_password, s_password);
+ } else {
+ remmina_plugin_service->file_set_string(remminafile, key_password, NULL);
+ }
+
+
+ if (s_username) g_free(s_username);
+ if (s_password) g_free(s_password);
+ if (s_domain) g_free(s_domain);
+
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static BOOL remmina_rdp_choose_smartcard(freerdp* instance, SmartcardCertInfo** cert_list, DWORD count,
+ DWORD* choice, BOOL gateway)
+{
+ // TODO: Display a simple list of smartcards/certs to choose from.
+ // See client_cli_choose_smartcard for a sample
+ return client_cli_choose_smartcard(instance, cert_list, count, choice, gateway);
+}
+
+static BOOL remmina_rdp_get_access_token(freerdp* instance, AccessTokenType tokenType, char** token,
+ size_t count, ...)
+{
+ // TODO: Open a (currently hard coded) URL, authenticate in a webview/browser, return the access token.
+ // See client_cli_get_access_token or sdl_webview_get_access_token for implementations
+ return client_cli_get_access_token(instance, tokenType, token, count);
+}
+
+static BOOL remmina_rdp_present_gateway_message(freerdp* instance, UINT32 type, BOOL isDisplayMandatory,
+ BOOL isConsentMandatory, size_t length,
+ const WCHAR* message)
+{
+ // TODO: Present a message to the user, usually terms of service or similar
+ // See client_cli_present_gateway_message or sdl_present_gateway_message
+ return client_cli_present_gateway_message(instance, type, isDisplayMandatory, isConsentMandatory, length, message);
+}
+
+static int remmina_rdp_logon_error_info(freerdp* instance, UINT32 data, UINT32 type)
+{
+ // TODO: Display the reason for connection termination
+ // See client_cli_logon_error_info or sdl_logon_error_info
+ return client_cli_logon_error_info(instance, data, type);
+}
+
+static SSIZE_T remmina_rdp_retry_dialog(freerdp* instance, const char* what, size_t current,
+ void* userarg)
+{
+ // TODO:
+ // See client_common_retry_dialog or
+ return client_common_retry_dialog(instance, what, current, userarg);
+}
+
+static void remmina_rdp_post_final_disconnect(freerdp* instance)
+{
+ // Clean up resources allocated in PreConnect
+}
+#endif
+
+static DWORD remmina_rdp_verify_certificate_ex(freerdp *instance, const char *host, UINT16 port,
+ const char *common_name, const char *subject,
+ const char *issuer, const char *fingerprint, DWORD flags)
+{
+ TRACE_CALL(__func__);
+ gint status;
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+
+ rfi = (rfContext *)instance->context;
+ gp = rfi->protocol_widget;
+
+ status = remmina_plugin_service->protocol_plugin_init_certificate(gp, subject, issuer, fingerprint);
+
+ if (status == GTK_RESPONSE_OK)
+ return 1;
+
+ return 0;
+}
+
+static DWORD
+remmina_rdp_verify_certificate(freerdp *instance, const char *common_name, const char *subject, const char *issuer, const char *fingerprint, BOOL host_mismatch) __attribute__ ((unused));
+static DWORD
+remmina_rdp_verify_certificate(freerdp *instance, const char *common_name, const char *subject,
+ const char *issuer, const char *fingerprint, BOOL host_mismatch)
+{
+ TRACE_CALL(__func__);
+ gint status;
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+
+ rfi = (rfContext *)instance->context;
+ gp = rfi->protocol_widget;
+
+ status = remmina_plugin_service->protocol_plugin_init_certificate(gp, subject, issuer, fingerprint);
+
+ if (status == GTK_RESPONSE_OK)
+ return 1;
+
+ return 0;
+}
+
+static DWORD remmina_rdp_verify_changed_certificate_ex(freerdp *instance, const char *host, UINT16 port,
+ const char *common_name, const char *subject,
+ const char *issuer, const char *fingerprint,
+ const char *old_subject, const char *old_issuer,
+ const char *old_fingerprint, DWORD flags)
+{
+ TRACE_CALL(__func__);
+ gint status;
+ rfContext *rfi;
+ RemminaProtocolWidget *gp;
+
+ rfi = (rfContext *)instance->context;
+ gp = rfi->protocol_widget;
+
+ status = remmina_plugin_service->protocol_plugin_changed_certificate(gp, subject, issuer, fingerprint, old_fingerprint);
+
+ if (status == GTK_RESPONSE_OK)
+ return 1;
+
+ return 0;
+}
+
+static void remmina_rdp_post_disconnect(freerdp *instance)
+{
+ TRACE_CALL(__func__);
+
+ if (!instance || !instance->context)
+ return;
+
+ PubSub_UnsubscribeChannelConnected(instance->context->pubSub,
+ remmina_rdp_OnChannelConnectedEventHandler);
+ PubSub_UnsubscribeChannelDisconnected(instance->context->pubSub,
+ remmina_rdp_OnChannelDisconnectedEventHandler);
+
+ /* The remaining cleanup will be continued on main thread by complete_cleanup_on_main_thread() */
+
+ // With FreeRDP3 only resources allocated in PostConnect and later are cleaned up here.
+}
+
+static void remmina_rdp_main_loop(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ DWORD status;
+ gchar buf[100];
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ time_t cur_time, time_diff;
+
+ int jitter_time = remmina_plugin_service->file_get_int(remminafile, "rdp_mouse_jitter", 0);
+ time(&last_time);
+#if FREERDP_VERSION_MAJOR >= 3
+ while (!freerdp_shall_disconnect_context(&rfi->clientContext.context)) {
+#else
+ while (!freerdp_shall_disconnect(rfi->clientContext.context.instance)) {
+#endif
+ // move mouse if we've been idle and option is selected
+ time(&cur_time);
+ time_diff = cur_time - last_time;
+ if (jitter_time > 0 && time_diff > jitter_time){
+ last_time = cur_time;
+ remmina_rdp_mouse_jitter(gp);
+ }
+
+ HANDLE handles[MAXIMUM_WAIT_OBJECTS] = {0};
+ DWORD nCount = freerdp_get_event_handles(&rfi->clientContext.context, &handles[0], ARRAYSIZE(handles));
+ if (rfi->event_handle)
+ handles[nCount++] = rfi->event_handle;
+
+ handles[nCount++] = freerdp_abort_event(&rfi->clientContext.context);
+
+ if (nCount == 0) {
+ fprintf(stderr, "freerdp_get_event_handles failed\n");
+ break;
+ }
+
+ status = WaitForMultipleObjects(nCount, handles, FALSE, 100);
+
+ if (status == WAIT_FAILED) {
+ fprintf(stderr, "WaitForMultipleObjects failed with %lu\n", (unsigned long)status);
+ break;
+ }
+
+ if (rfi->event_handle && WaitForSingleObject(rfi->event_handle, 0) == WAIT_OBJECT_0) {
+ if (!rf_process_event_queue(gp)) {
+ fprintf(stderr, "Could not process local keyboard/mouse event queue\n");
+ break;
+ }
+ if (read(rfi->event_pipe[0], buf, sizeof(buf))) {
+ }
+ }
+
+ /* Check if a processed event called freerdp_abort_connect() and exit if true */
+ if (WaitForSingleObject(freerdp_abort_event(&rfi->clientContext.context), 0) == WAIT_OBJECT_0)
+ /* Session disconnected by local user action */
+ break;
+
+ if (!freerdp_check_event_handles(&rfi->clientContext.context)) {
+ if (rf_auto_reconnect(rfi)) {
+ /* Reset the possible reason/error which made us doing many reconnection reattempts and continue */
+ remmina_plugin_service->protocol_plugin_set_error(gp, NULL);
+ continue;
+ }
+ if (freerdp_get_last_error(&rfi->clientContext.context) == FREERDP_ERROR_SUCCESS)
+ fprintf(stderr, "Could not check FreeRDP file descriptor\n");
+ break;
+ }
+ }
+ const gchar *host = freerdp_settings_get_string (rfi->clientContext.context.settings, FreeRDP_ServerHostname);
+ // TRANSLATORS: the placeholder may be either an IP/FQDN or a server hostname
+ REMMINA_PLUGIN_AUDIT(_("Disconnected from %s via RDP"), host);
+ freerdp_disconnect(rfi->clientContext.context.instance);
+ REMMINA_PLUGIN_DEBUG("RDP client disconnected");
+}
+
+static int remmina_rdp_load_static_channel_addin(rdpChannels *channels, rdpSettings *settings, char *name, void *data)
+{
+ TRACE_CALL(__func__);
+ PVIRTUALCHANNELENTRY entry = NULL;
+ PVIRTUALCHANNELENTRYEX entryEx = NULL;
+
+ entryEx = (PVIRTUALCHANNELENTRYEX)(void *)freerdp_load_channel_addin_entry(
+ name, NULL, NULL, FREERDP_ADDIN_CHANNEL_STATIC | FREERDP_ADDIN_CHANNEL_ENTRYEX);
+
+ if (!entryEx)
+ entry = freerdp_load_channel_addin_entry(name, NULL, NULL, FREERDP_ADDIN_CHANNEL_STATIC);
+
+ if (entryEx) {
+ if (freerdp_channels_client_load_ex(channels, settings, entryEx, data) == 0) {
+ fprintf(stderr, "loading channel %s\n", name);
+ return TRUE;
+ }
+ } else if (entry) {
+ if (freerdp_channels_client_load(channels, settings, entry, data) == 0) {
+ fprintf(stderr, "loading channel %s\n", name);
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static gchar *remmina_rdp_find_prdriver(char *smap, char *prn)
+{
+ char c, *p, *dr;
+ int matching;
+ size_t sz;
+
+ enum { S_WAITPR,
+ S_INPRINTER,
+ S_WAITCOLON,
+ S_WAITDRIVER,
+ S_INDRIVER,
+ S_WAITSEMICOLON } state = S_WAITPR;
+
+ matching = 0;
+ while ((c = *smap++) != 0) {
+ switch (state) {
+ case S_WAITPR:
+ if (c != '\"') return NULL;
+ state = S_INPRINTER;
+ p = prn;
+ matching = 1;
+ break;
+ case S_INPRINTER:
+ if (matching && c == *p && *p != 0) {
+ p++;
+ } else if (c == '\"') {
+ if (*p != 0)
+ matching = 0;
+ state = S_WAITCOLON;
+ } else {
+ matching = 0;
+ }
+ break;
+ case S_WAITCOLON:
+ if (c != ':')
+ return NULL;
+ state = S_WAITDRIVER;
+ break;
+ case S_WAITDRIVER:
+ if (c != '\"')
+ return NULL;
+ state = S_INDRIVER;
+ dr = smap;
+ break;
+ case S_INDRIVER:
+ if (c == '\"') {
+ if (matching)
+ goto found;
+ else
+ state = S_WAITSEMICOLON;
+ }
+ break;
+ case S_WAITSEMICOLON:
+ if (c != ';')
+ return NULL;
+ state = S_WAITPR;
+ break;
+ }
+ }
+ return NULL;
+
+found:
+ sz = smap - dr;
+ p = (char *)malloc(sz);
+ memcpy(p, dr, sz);
+ p[sz - 1] = 0;
+ return p;
+}
+
+#ifdef HAVE_CUPS
+/**
+ * Callback function used by cupsEnumDests
+ * - For each enumerated local printer tries to set the Printer Name and Driver.
+ * @return 1 if there are other printers to scan or 0 when it's done.
+ */
+static int remmina_rdp_set_printers(void *user_data, unsigned flags, cups_dest_t *dest)
+{
+ rfContext *rfi = (rfContext *)user_data;
+ RemminaProtocolWidget *gp = rfi->protocol_widget;
+
+ /** @warning printer-make-and-model is not always the same as on the Windows,
+ * therefore it fails finding to right one and it fails to add
+ * the printer.
+ *
+ * We pass NULL and we do not check for errors. The following code is
+ * how it is supposed to work. @todo Ask CUPS mailing list for help.
+ *
+ * @code
+ * const char *model = cupsGetOption("printer-make-and-model",
+ * dest->num_options,
+ * dest->options);
+ * @endcode
+ */
+
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ const gchar *s = remmina_plugin_service->file_get_string(remminafile, "printer_overrides");
+
+ RDPDR_PRINTER *printer;
+ printer = (RDPDR_PRINTER *)calloc(1, sizeof(RDPDR_PRINTER));
+
+#if FREERDP_VERSION_MAJOR >= 3
+ RDPDR_DEVICE *pdev;
+ pdev = &(printer->device);
+#else
+ RDPDR_PRINTER *pdev;
+ pdev = printer;
+#endif
+
+ pdev->Type = RDPDR_DTYP_PRINT;
+ REMMINA_PLUGIN_DEBUG("Printer Type: %d", pdev->Type);
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RedirectPrinters, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DeviceRedirection, TRUE);
+
+ REMMINA_PLUGIN_DEBUG("Destination: %s", dest->name);
+ if (!(pdev->Name = _strdup(dest->name))) {
+ free(printer);
+ return 1;
+ }
+
+ REMMINA_PLUGIN_DEBUG("Printer Name: %s", pdev->Name);
+
+ if (s) {
+ gchar *d = remmina_rdp_find_prdriver(strdup(s), pdev->Name);
+ if (d) {
+ printer->DriverName = strdup(d);
+ REMMINA_PLUGIN_DEBUG("Printer DriverName set to: %s", printer->DriverName);
+ g_free(d);
+ } else {
+ /**
+ * When remmina_rdp_find_prdriver doesn't return a DriverName
+ * it means that we don't want to share that printer
+ *
+ */
+ free(pdev->Name);
+ free(printer);
+ return 1;
+ }
+ } else {
+ /* We set to a default driver*/
+ printer->DriverName = _strdup("MS Publisher Imagesetter");
+ }
+
+ REMMINA_PLUGIN_DEBUG("Printer Driver: %s", printer->DriverName);
+ if (!freerdp_device_collection_add(rfi->clientContext.context.settings, (RDPDR_DEVICE *)printer)) {
+ free(printer->DriverName);
+ free(pdev->Name);
+ free(printer);
+ return 1;
+ }
+
+ return 1;
+}
+#endif /* HAVE_CUPS */
+
+/* Send Ctrl+Alt+Del keystrokes to the plugin drawing_area widget */
+static void remmina_rdp_send_ctrlaltdel(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ guint keys[] = { GDK_KEY_Control_L, GDK_KEY_Alt_L, GDK_KEY_Delete };
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ remmina_plugin_service->protocol_plugin_send_keys_signals(rfi->drawing_area,
+ keys, G_N_ELEMENTS(keys), GDK_KEY_PRESS | GDK_KEY_RELEASE);
+}
+
+static gboolean remmina_rdp_set_connection_type(rdpSettings *settings, guint32 type)
+{
+ freerdp_settings_set_uint32(settings, FreeRDP_ConnectionType, type);
+
+ if (type == CONNECTION_TYPE_MODEM) {
+ freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, TRUE);
+ } else if (type == CONNECTION_TYPE_BROADBAND_LOW) {
+ freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE);
+ } else if (type == CONNECTION_TYPE_SATELLITE) {
+ freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE);
+ } else if (type == CONNECTION_TYPE_BROADBAND_HIGH) {
+ freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE);
+ } else if (type == CONNECTION_TYPE_WAN) {
+ freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE);
+ } else if (type == CONNECTION_TYPE_LAN) {
+ freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE);
+ } else if (type == CONNECTION_TYPE_AUTODETECT) {
+ freerdp_settings_set_bool(settings, FreeRDP_DisableWallpaper, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowFontSmoothing, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_AllowDesktopComposition, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableFullWindowDrag, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableMenuAnims, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_DisableThemes, FALSE);
+ freerdp_settings_set_bool(settings, FreeRDP_NetworkAutoDetect, TRUE);
+
+ /* Automatically activate GFX and RFX codec support */
+#ifdef WITH_GFX_H264
+ freerdp_settings_set_bool(settings, FreeRDP_GfxAVC444, gfx_h264_available);
+ freerdp_settings_set_bool(settings, FreeRDP_GfxH264, gfx_h264_available);
+#endif
+ freerdp_settings_set_bool(settings, FreeRDP_RemoteFxCodec, TRUE);
+ freerdp_settings_set_bool(settings, FreeRDP_SupportGraphicsPipeline, TRUE);
+ } else if (type == REMMINA_CONNECTION_TYPE_NONE) {
+ return FALSE;
+ } else {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+#ifdef GDK_WINDOWING_X11
+#if FREERDP_CHECK_VERSION(2, 3, 0)
+static gchar *remmina_get_rdp_kbd_remap(const gchar *keymap)
+{
+ TRACE_CALL(__func__);
+ guint *table;
+ gchar keys[20];
+ gchar *rdp_kbd_remap = NULL;
+ gint i;
+ Display *display;
+
+ table = remmina_plugin_service->pref_keymap_get_table(keymap);
+ if (!table)
+ return rdp_kbd_remap;
+ rdp_kbd_remap = g_malloc0(512);
+ display = XOpenDisplay(0);
+ for (i = 0; table[i] > 0; i += 2) {
+ g_snprintf(keys, sizeof(keys), "0x%02x=0x%02x", freerdp_keyboard_get_rdp_scancode_from_x11_keycode(XKeysymToKeycode(display, table[i])),
+ freerdp_keyboard_get_rdp_scancode_from_x11_keycode(XKeysymToKeycode(display, table[i + 1])));
+ if (i > 0)
+ g_strlcat(rdp_kbd_remap, ",", 512);
+ g_strlcat(rdp_kbd_remap, keys, 512);
+ }
+ XCloseDisplay(display);
+
+ return rdp_kbd_remap;
+}
+#endif
+#endif
+
+static gboolean remmina_rdp_main(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ const gchar *s;
+ gchar *sm;
+ gchar *value;
+ const gchar *cs;
+ RemminaFile *remminafile;
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ rdpChannels *channels;
+ gchar *gateway_host;
+ gint gateway_port;
+ gchar *datapath = NULL;
+ gboolean status = TRUE;
+#ifdef GDK_WINDOWING_X11
+ gchar *rdp_kbd_remap;
+#endif
+ gint i;
+
+ gint desktopOrientation, desktopScaleFactor, deviceScaleFactor;
+
+ channels = rfi->clientContext.context.channels;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ datapath = g_build_path("/",
+ remmina_plugin_service->file_get_user_datadir(),
+ "RDP",
+ NULL);
+ REMMINA_PLUGIN_DEBUG("RDP data path is %s", datapath);
+
+ if ((datapath != NULL) && (datapath[0] != '\0'))
+ if (access(datapath, W_OK) == 0)
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ConfigPath, datapath);
+ g_free(datapath);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "assistance_mode", 0)){
+ rdpAssistanceFile* file = freerdp_assistance_file_new();
+ if (!file){
+ REMMINA_PLUGIN_DEBUG("Could not allocate assistance file structure");
+ return FALSE;
+ }
+
+ if (remmina_plugin_service->file_get_string(remminafile, "assistance_file") == NULL ||
+ remmina_plugin_service->file_get_string(remminafile, "assistance_pass") == NULL ){
+
+ REMMINA_PLUGIN_DEBUG("Assistance file and password are not set while assistance mode is on");
+ return FALSE;
+ }
+
+ status = freerdp_assistance_parse_file(file,
+ remmina_plugin_service->file_get_string(remminafile, "assistance_file"),
+ remmina_plugin_service->file_get_string(remminafile, "assistance_pass"));
+
+ if (status < 0){
+ REMMINA_PLUGIN_DEBUG("Could not parse assistance file");
+ return FALSE;
+ }
+
+
+ if (!freerdp_assistance_populate_settings_from_assistance_file(file, rfi->clientContext.context.settings)){
+ REMMINA_PLUGIN_DEBUG("Could not populate settings from assistance file");
+ return FALSE;
+ }
+ }
+
+
+#if defined(PROXY_TYPE_IGNORE)
+ if (!remmina_plugin_service->file_get_int(remminafile, "useproxyenv", FALSE) ? TRUE : FALSE) {
+ REMMINA_PLUGIN_DEBUG("Not using system proxy settings");
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ProxyType, PROXY_TYPE_IGNORE);
+ }
+#endif
+
+ if (!remmina_rdp_tunnel_init(gp))
+ return FALSE;
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AutoReconnectionEnabled, (remmina_plugin_service->file_get_int(remminafile, "disableautoreconnect", FALSE) ? FALSE : TRUE));
+ /* Disable RDP auto reconnection when SSH tunnel is enabled */
+ if (remmina_plugin_service->file_get_int(remminafile, "ssh_tunnel_enabled", FALSE))
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AutoReconnectionEnabled, FALSE);
+
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, remmina_plugin_service->file_get_int(remminafile, "colordepth", 99));
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SoftwareGdi, TRUE);
+ REMMINA_PLUGIN_DEBUG("gfx_h264_available: %d", gfx_h264_available);
+
+ /* Avoid using H.264 modes if they are not available on libfreerdp */
+ if (!gfx_h264_available && (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 65 || freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_ColorDepth == 66)))
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 64); // Fallback to GFX RFX
+
+ if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 0) {
+ /* RFX (Win7)*/
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RemoteFxCodec, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, FALSE);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32);
+ } else if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 63) {
+ /* /gfx (RFX Progressive) (Win8) */
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxAVC444, FALSE);
+ } else if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 64) {
+ /* /gfx:rfx (Win8) */
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RemoteFxCodec, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxAVC444, FALSE);
+ } else if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 65) {
+ /* /gfx:avc420 (Win8.1) */
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264, gfx_h264_available);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxAVC444, FALSE);
+ } else if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 66) {
+ /* /gfx:avc444 (Win10) */
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264, gfx_h264_available);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxAVC444, gfx_h264_available);
+ } else if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth) == 99) {
+ /* Automatic (Let the server choose its best format) */
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RemoteFxCodec, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264, gfx_h264_available);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GfxAVC444, gfx_h264_available);
+ }
+
+ if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_RemoteFxCodec) ||
+ freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_NSCodec) ||
+ freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline)) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FastPathOutput, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FrameMarkerCommandEnabled, TRUE);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ColorDepth, 32);
+ rfi->bpp = 32;
+ }
+
+ gint w = remmina_plugin_service->get_profile_remote_width(gp);
+ gint h = remmina_plugin_service->get_profile_remote_height(gp);
+ /* multiple of 4 */
+ w = (w + 3) & ~0x3;
+ h = (h + 3) & ~0x3;
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth, w);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight, h);
+ REMMINA_PLUGIN_DEBUG("Resolution set by the user: %dx%d", w, h);
+
+ /* Workaround for FreeRDP issue #5417: in GFX AVC modes we can't go under
+ * AVC_MIN_DESKTOP_WIDTH x AVC_MIN_DESKTOP_HEIGHT */
+ if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline) &&
+ freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_GfxH264)) {
+ if (freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth) <
+ AVC_MIN_DESKTOP_WIDTH)
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth,
+ AVC_MIN_DESKTOP_WIDTH);
+ if (freerdp_settings_get_uint32(rfi->clientContext.context.settings,
+ FreeRDP_DesktopHeight) <
+ AVC_MIN_DESKTOP_HEIGHT)
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight,
+ AVC_MIN_DESKTOP_HEIGHT);
+ }
+
+ /* Workaround for FreeRDP issue #5119. This will make our horizontal resolution
+ * an even value, but it will add a vertical black 1 pixel line on the
+ * right of the desktop */
+ if ((freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth) & 1) != 0) {
+ UINT32 tmp = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth, tmp - 1);
+ }
+
+ remmina_plugin_service->protocol_plugin_set_width(gp, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth));
+ remmina_plugin_service->protocol_plugin_set_height(gp, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight));
+
+ w = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth);
+ h = freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight);
+ REMMINA_PLUGIN_DEBUG("Resolution set after workarounds: %dx%d", w, h);
+
+
+ if (remmina_plugin_service->file_get_string(remminafile, "username"))
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Username, remmina_plugin_service->file_get_string(remminafile, "username"));
+
+ if (remmina_plugin_service->file_get_string(remminafile, "domain"))
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Domain, remmina_plugin_service->file_get_string(remminafile, "domain"));
+
+ s = remmina_plugin_service->file_get_string(remminafile, "password");
+ if (s) freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Password, s);
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AutoLogonEnabled, TRUE);
+
+ /**
+ * Proxy support
+ * Proxy settings are hidden at the moment as an advanced feature
+ */
+ gchar *proxy_type = g_strdup(remmina_plugin_service->file_get_string(remminafile, "proxy_type"));
+ gchar *proxy_username = g_strdup(remmina_plugin_service->file_get_string(remminafile, "proxy_username"));
+ gchar *proxy_password = g_strdup(remmina_plugin_service->file_get_string(remminafile, "proxy_password"));
+ gchar *proxy_hostname = g_strdup(remmina_plugin_service->file_get_string(remminafile, "proxy_hostname"));
+ gint proxy_port = remmina_plugin_service->file_get_int(remminafile, "proxy_port", 80);
+ REMMINA_PLUGIN_DEBUG("proxy_type: %s", proxy_type);
+ REMMINA_PLUGIN_DEBUG("proxy_username: %s", proxy_username);
+ REMMINA_PLUGIN_DEBUG("proxy_password: %s", proxy_password);
+ REMMINA_PLUGIN_DEBUG("proxy_hostname: %s", proxy_hostname);
+ REMMINA_PLUGIN_DEBUG("proxy_port: %d", proxy_port);
+ if (proxy_type && proxy_hostname) {
+ if (g_strcmp0(proxy_type, "no_proxy") == 0)
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ProxyType, PROXY_TYPE_IGNORE);
+ else if (g_strcmp0(proxy_type, "http") == 0)
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ProxyType, PROXY_TYPE_HTTP);
+ else if (g_strcmp0(proxy_type, "socks5") == 0)
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ProxyType, PROXY_TYPE_SOCKS);
+ else
+ g_warning("Invalid proxy protocol, at the moment only no_proxy, HTTP and SOCKS5 are supported");
+ REMMINA_PLUGIN_DEBUG("ProxyType set to: %" PRIu32, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_ProxyType));
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ProxyHostname, proxy_hostname);
+ if (proxy_username)
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ProxyUsername, proxy_username);
+ if (proxy_password)
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ProxyPassword, proxy_password);
+ if (proxy_port)
+ freerdp_settings_set_uint16(rfi->clientContext.context.settings, FreeRDP_ProxyPort, proxy_port);
+ }
+ g_free(proxy_hostname);
+ g_free(proxy_username);
+ g_free(proxy_password);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "base-cred-for-gw", FALSE)) {
+ // Reset gateway credentials
+ remmina_plugin_service->file_set_string(remminafile, "gateway_username", NULL);
+ remmina_plugin_service->file_set_string(remminafile, "gateway_domain", NULL);
+ remmina_plugin_service->file_set_string(remminafile, "gateway_password", NULL);
+ }
+
+ /* Remote Desktop Gateway server address */
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayEnabled, FALSE);
+ s = remmina_plugin_service->file_get_string(remminafile, "gateway_server");
+ if (s) {
+ cs = remmina_plugin_service->file_get_string(remminafile, "gwtransp");
+#if FREERDP_CHECK_VERSION(2, 3, 1)
+ if (remmina_plugin_service->file_get_int(remminafile, "websockets", FALSE))
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayHttpUseWebsockets, TRUE);
+ else
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayHttpUseWebsockets, FALSE);
+#endif
+ if (g_strcmp0(cs, "http") == 0) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayRpcTransport, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayHttpTransport, TRUE);
+ } else if (g_strcmp0(cs, "rpc") == 0) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayRpcTransport, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayHttpTransport, FALSE);
+ } else if (g_strcmp0(cs, "auto") == 0) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayRpcTransport, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayHttpTransport, TRUE);
+ }
+ remmina_plugin_service->get_server_port(s, 443, &gateway_host, &gateway_port);
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayHostname, gateway_host);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_GatewayPort, gateway_port);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayEnabled, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials, TRUE);
+ }
+ /* Remote Desktop Gateway domain */
+ if (remmina_plugin_service->file_get_string(remminafile, "gateway_domain")) {
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayDomain, remmina_plugin_service->file_get_string(remminafile, "gateway_domain"));
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials, FALSE);
+ }
+ /* Remote Desktop Gateway username */
+ if (remmina_plugin_service->file_get_string(remminafile, "gateway_username")) {
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayUsername, remmina_plugin_service->file_get_string(remminafile, "gateway_username"));
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials, FALSE);
+ }
+ /* Remote Desktop Gateway password */
+ s = remmina_plugin_service->file_get_string(remminafile, "gateway_password");
+ if (s) {
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayPassword, s);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials, FALSE);
+ }
+ /* If no different credentials were provided for the Remote Desktop Gateway
+ * use the same authentication credentials for the host */
+ if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_GatewayEnabled) && freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials)) {
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayDomain, freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_Domain));
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayUsername, freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_Username));
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayPassword, freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_Password));
+ }
+ /* Remote Desktop Gateway usage */
+ if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_GatewayEnabled))
+ freerdp_set_gateway_usage_method(rfi->clientContext.context.settings,
+ remmina_plugin_service->file_get_int(remminafile, "gateway_usage", FALSE) ? TSC_PROXY_MODE_DETECT : TSC_PROXY_MODE_DIRECT);
+
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayAccessToken,
+ remmina_plugin_service->file_get_string(remminafile, "gatewayaccesstoken"));
+
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_AuthenticationLevel, remmina_plugin_service->file_get_int(
+ remminafile, "authentication level", freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_AuthenticationLevel)));
+
+ /* Certificate ignore */
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_IgnoreCertificate, remmina_plugin_service->file_get_int(remminafile, "cert_ignore", 0));
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_OldLicenseBehaviour, remmina_plugin_service->file_get_int(remminafile, "old-license", 0));
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AllowUnanouncedOrdersFromServer, remmina_plugin_service->file_get_int(remminafile, "relax-order-checks", 0));
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_GlyphSupportLevel, (remmina_plugin_service->file_get_int(remminafile, "glyph-cache", 0) ? GLYPH_SUPPORT_FULL : GLYPH_SUPPORT_NONE));
+
+ if ((cs = remmina_plugin_service->file_get_string(remminafile, "clientname")))
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ClientHostname, cs);
+ else
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ClientHostname, g_get_host_name());
+
+ /* Client Build number is optional, if not specified defaults to 0, allow for comments to appear after number */
+ if ((cs = remmina_plugin_service->file_get_string(remminafile, "clientbuild"))) {
+ if (*cs) {
+ UINT32 val = strtoul(cs, NULL, 0);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_ClientBuild, val);
+ }
+ }
+
+
+ if (remmina_plugin_service->file_get_string(remminafile, "loadbalanceinfo")) {
+ const gchar *tmp = strdup(remmina_plugin_service->file_get_string(remminafile, "loadbalanceinfo"));
+ freerdp_settings_set_pointer_len(rfi->clientContext.context.settings, FreeRDP_LoadBalanceInfo, tmp, strlen(tmp) + 1);
+ }
+
+ if (remmina_plugin_service->file_get_string(remminafile, "exec"))
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_AlternateShell, remmina_plugin_service->file_get_string(remminafile, "exec"));
+
+ if (remmina_plugin_service->file_get_string(remminafile, "execpath"))
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_ShellWorkingDirectory, remmina_plugin_service->file_get_string(remminafile, "execpath"));
+
+ sm = g_strdup_printf("rdp_quality_%i", remmina_plugin_service->file_get_int(remminafile, "quality", DEFAULT_QUALITY_0));
+ value = remmina_plugin_service->pref_get_value(sm);
+ g_free(sm);
+
+ if (value && value[0]) {
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_PerformanceFlags, strtoul(value, NULL, 16));
+ } else {
+ switch (remmina_plugin_service->file_get_int(remminafile, "quality", DEFAULT_QUALITY_0)) {
+ case 9:
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_PerformanceFlags, DEFAULT_QUALITY_9);
+ break;
+
+ case 2:
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_PerformanceFlags, DEFAULT_QUALITY_2);
+ break;
+
+ case 1:
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_PerformanceFlags, DEFAULT_QUALITY_1);
+ break;
+
+ case 0:
+ default:
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_PerformanceFlags, DEFAULT_QUALITY_0);
+ break;
+ }
+ }
+ g_free(value);
+
+ if ((cs = remmina_plugin_service->file_get_string(remminafile, "network"))) {
+ guint32 type = 0;
+
+ if (g_strcmp0(cs, "modem") == 0)
+ type = CONNECTION_TYPE_MODEM;
+ else if (g_strcmp0(cs, "broadband") == 0)
+ type = CONNECTION_TYPE_BROADBAND_HIGH;
+ else if (g_strcmp0(cs, "broadband-low") == 0)
+ type = CONNECTION_TYPE_BROADBAND_LOW;
+ else if (g_strcmp0(cs, "broadband-high") == 0)
+ type = CONNECTION_TYPE_BROADBAND_HIGH;
+ else if (g_strcmp0(cs, "wan") == 0)
+ type = CONNECTION_TYPE_WAN;
+ else if (g_strcmp0(cs, "lan") == 0)
+ type = CONNECTION_TYPE_LAN;
+ else if ((g_strcmp0(cs, "autodetect") == 0))
+ type = CONNECTION_TYPE_AUTODETECT;
+ else if ((g_strcmp0(cs, "none") == 0))
+ type = REMMINA_CONNECTION_TYPE_NONE;
+ else
+ type = REMMINA_CONNECTION_TYPE_NONE;
+
+ if (!remmina_rdp_set_connection_type(rfi->clientContext.context.settings, type))
+ REMMINA_PLUGIN_DEBUG("Network settings not set");
+ }
+
+ /* PerformanceFlags bitmask need also to be splitted into BOOL variables
+ * like freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DisableWallpaper, freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AllowFontSmoothing…
+ * or freerdp_get_param_bool() function will return the wrong value
+ */
+ freerdp_performance_flags_split(rfi->clientContext.context.settings);
+
+#ifdef GDK_WINDOWING_X11
+#if FREERDP_CHECK_VERSION(2, 3, 0)
+ rdp_kbd_remap = remmina_get_rdp_kbd_remap(remmina_plugin_service->file_get_string(remminafile, "keymap"));
+ if (rdp_kbd_remap != NULL) {
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_KeyboardRemappingList, rdp_kbd_remap);
+ REMMINA_PLUGIN_DEBUG(
+ "rdp_keyboard_remapping_list: %s",
+ freerdp_settings_get_string(rfi->clientContext.context.settings,
+ FreeRDP_KeyboardRemappingList));
+ g_free(rdp_kbd_remap);
+ }
+ else {
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_KeyboardRemappingList, remmina_plugin_service->pref_get_value("rdp_kbd_remap"));
+ REMMINA_PLUGIN_DEBUG(
+ "rdp_keyboard_remapping_list: %s",
+ freerdp_settings_get_string(rfi->clientContext.context.settings,
+ FreeRDP_KeyboardRemappingList));
+ }
+#endif
+#endif
+
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_KeyboardLayout, remmina_rdp_settings_get_keyboard_layout());
+
+ if (remmina_plugin_service->file_get_int(remminafile, "console", FALSE))
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ConsoleSession, TRUE);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "restricted-admin", FALSE)) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ConsoleSession, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RestrictedAdminModeRequired, TRUE);
+ }
+
+ if (remmina_plugin_service->file_get_string(remminafile, "pth")) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ConsoleSession, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RestrictedAdminModeRequired, TRUE);
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_PasswordHash, remmina_plugin_service->file_get_string(remminafile, "pth"));
+ remmina_plugin_service->file_set_int(remminafile, "restricted-admin", TRUE);
+ }
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "security");
+ if (g_strcmp0(cs, "rdp") == 0) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RdpSecurity, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_TlsSecurity, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_NlaSecurity, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ExtSecurity, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_UseRdpSecurityLayer, TRUE);
+ } else if (g_strcmp0(cs, "tls") == 0) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RdpSecurity, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_TlsSecurity, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_NlaSecurity, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ExtSecurity, FALSE);
+ } else if (g_strcmp0(cs, "nla") == 0) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RdpSecurity, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_TlsSecurity, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_NlaSecurity, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ExtSecurity, FALSE);
+ } else if (g_strcmp0(cs, "ext") == 0) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RdpSecurity, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_TlsSecurity, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_NlaSecurity, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ExtSecurity, TRUE);
+ } else {
+ /* This is "-nego" switch of xfreerdp */
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_NegotiateSecurityLayer, TRUE);
+ }
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "tls-seclevel");
+ if (cs && g_strcmp0(cs,"")!=0) {
+ i = atoi(cs);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_TlsSecLevel, i);
+ }
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_CompressionEnabled, TRUE);
+ if (remmina_plugin_service->file_get_int(remminafile, "disable_fastpath", FALSE)) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FastPathInput, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FastPathOutput, FALSE);
+ } else {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FastPathInput, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_FastPathOutput, TRUE);
+ }
+
+ /* Orientation and scaling settings */
+ remmina_rdp_settings_get_orientation_scale_prefs(&desktopOrientation, &desktopScaleFactor, &deviceScaleFactor);
+
+ freerdp_settings_set_uint16(rfi->clientContext.context.settings, FreeRDP_DesktopOrientation, desktopOrientation);
+ if (desktopScaleFactor != 0 && deviceScaleFactor != 0) {
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopScaleFactor, desktopScaleFactor);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DeviceScaleFactor, deviceScaleFactor);
+ }
+
+ /* Try to enable "Display Control Virtual Channel Extension", needed to
+ * dynamically resize remote desktop. This will automatically open
+ * the "disp" dynamic channel, if available */
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportDisplayControl, TRUE);
+
+ if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SupportDisplayControl)) {
+ CLPARAM *d[1];
+ int dcount;
+
+ dcount = 1;
+ d[0] = "disp";
+ freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, dcount, d);
+ }
+
+ if (freerdp_settings_get_bool(rfi->clientContext.context.settings, FreeRDP_SupportGraphicsPipeline)) {
+ CLPARAM *d[1];
+
+ int dcount;
+
+ dcount = 1;
+ d[0] = "rdpgfx";
+ freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, dcount, d);
+ }
+
+ /* Sound settings */
+ cs = remmina_plugin_service->file_get_string(remminafile, "sound");
+ if (g_strcmp0(cs, "remote") == 0) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RemoteConsoleAudio, TRUE);
+ } else if ((cs != NULL && cs[0] != '\0') && g_str_has_prefix(cs, "local")) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AudioPlayback, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AudioCapture, TRUE);
+ } else {
+ /* Disable sound */
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AudioPlayback, FALSE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RemoteConsoleAudio, FALSE);
+ }
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "microphone");
+ if (cs != NULL && cs[0] != '\0') {
+ if (g_strcmp0(cs, "0") == 0) {
+ REMMINA_PLUGIN_DEBUG("“microphone” was set to 0, setting to \"\"");
+ remmina_plugin_service->file_set_string(remminafile, "microphone", "");
+ } else {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_AudioCapture, TRUE);
+ REMMINA_PLUGIN_DEBUG("“microphone” set to “%s”", cs);
+ CLPARAM **p;
+ size_t count;
+
+ p = remmina_rdp_CommandLineParseCommaSeparatedValuesEx("audin", g_strdup(cs), &count);
+
+ freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, count, p);
+ g_free(p);
+ }
+ }
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "audio-output");
+ if (cs != NULL && cs[0] != '\0') {
+ REMMINA_PLUGIN_DEBUG("audio output set to %s", cs);
+ CLPARAM **p;
+ size_t count;
+
+ p = remmina_rdp_CommandLineParseCommaSeparatedValuesEx("rdpsnd", g_strdup(cs), &count);
+ status = freerdp_client_add_static_channel(rfi->clientContext.context.settings, count, p);
+ if (status)
+ status = freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, count, p);
+ g_free(p);
+ }
+
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "freerdp_log_level");
+ if (cs != NULL && cs[0] != '\0')
+ REMMINA_PLUGIN_DEBUG("Log level set to to %s", cs);
+ else
+ cs = g_strdup("INFO");
+ wLog *root = WLog_GetRoot();
+ WLog_SetStringLogLevel(root, cs);
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "freerdp_log_filters");
+ if (cs != NULL && cs[0] != '\0') {
+ REMMINA_PLUGIN_DEBUG("Log filters set to to %s", cs);
+ WLog_AddStringLogFilters(cs);
+ } else {
+ WLog_AddStringLogFilters(NULL);
+ }
+
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "usb");
+ if (cs != NULL && cs[0] != '\0') {
+ CLPARAM **p;
+ size_t count;
+ p = remmina_rdp_CommandLineParseCommaSeparatedValuesEx("urbdrc", g_strdup(cs), &count);
+ freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, count, p);
+ g_free(p);
+ }
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "vc");
+ if (cs != NULL && cs[0] != '\0') {
+ CLPARAM **p;
+ size_t count;
+ p = remmina_rdp_CommandLineParseCommaSeparatedValues(g_strdup(cs), &count);
+ freerdp_client_add_static_channel(rfi->clientContext.context.settings, count, p);
+ g_free(p);
+ }
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "dvc");
+ if (cs != NULL && cs[0] != '\0') {
+ CLPARAM **p;
+ size_t count;
+ p = remmina_rdp_CommandLineParseCommaSeparatedValues(g_strdup(cs), &count);
+ freerdp_client_add_dynamic_channel(rfi->clientContext.context.settings, count, p);
+ g_free(p);
+ }
+
+ cs = remmina_plugin_service->file_get_string(remminafile, RDP2TCP_DVC_CHANNEL_NAME);
+ if (cs != NULL && cs[0] != '\0') {
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_RDP2TCPArgs, cs);
+
+#if FREERDP_VERSION_MAJOR >= 3
+ char* args = freerdp_settings_get_string_writable(rfi->clientContext.context.settings, FreeRDP_RDP2TCPArgs);
+#else
+ char* args = freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_RDP2TCPArgs);
+#endif
+ REMMINA_PLUGIN_DEBUG(RDP2TCP_DVC_CHANNEL_NAME " set to %s",args);
+ remmina_rdp_load_static_channel_addin(channels, rfi->clientContext.context.settings, RDP2TCP_DVC_CHANNEL_NAME, args);
+ }
+
+ int vermaj, vermin, verrev;
+ freerdp_get_version(&vermaj, &vermin, &verrev);
+
+#if FREERDP_CHECK_VERSION(2, 1, 0)
+ cs = remmina_plugin_service->file_get_string(remminafile, "timeout");
+ if (cs != NULL && cs[0] != '\0') {
+ const gchar *endptr = NULL;
+ guint64 val = g_ascii_strtoull(cs, (gchar **)&endptr, 10);
+ if (val > 600000 || val <= 0)
+ val = 600000;
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_TcpAckTimeout, (UINT32)val);
+ }
+#endif
+
+ if (remmina_plugin_service->file_get_int(remminafile, "preferipv6", FALSE) ? TRUE : FALSE)
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_PreferIPv6OverIPv4, TRUE);
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RedirectClipboard, remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE) ? FALSE : TRUE);
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "sharefolder");
+ if (cs != NULL && cs[0] != '\0') {
+ gchar *ccs = g_strdup(cs);
+ REMMINA_PLUGIN_DEBUG("[Deprecated->migrating] - Old sharefolder %s to \"drive \"", ccs);
+ if (!remmina_plugin_service->file_get_string(remminafile, "drive")) {
+ remmina_plugin_service->file_set_string(remminafile, "drive", g_strdup(ccs));
+ remmina_plugin_service->file_set_string(remminafile, "sharefolder", NULL);
+ REMMINA_PLUGIN_DEBUG("[Deprecated->migrated] - drive set to %s", g_strdup(ccs));
+ }
+ g_free(ccs);
+ //CLPARAM **p;
+ //size_t count;
+ //p = remmina_rdp_CommandLineParseCommaSeparatedValuesEx("drive", g_strdup(cs), &count);
+ //status = freerdp_client_add_device_channel(rfi->clientContext.context.settings, count, p);
+ //g_free(p);
+ }
+ cs = remmina_plugin_service->file_get_string(remminafile, "drive");
+ if (cs != NULL && cs[0] != '\0') {
+ REMMINA_PLUGIN_DEBUG("Redirect directory set to %s", cs);
+ CLPARAM **p;
+ size_t count;
+
+ gchar **folders = g_strsplit(cs, ";", -1);
+ for (i = 0; folders[i] != NULL; i++) {
+ REMMINA_PLUGIN_DEBUG("Parsing folder %s", folders[i]);
+ p = remmina_rdp_CommandLineParseCommaSeparatedValuesEx("drive", g_strdup(folders[i]), &count);
+ status = freerdp_client_add_device_channel(rfi->clientContext.context.settings, count, p);
+ g_free(p);
+ }
+ g_strfreev(folders);
+ }
+
+ if (remmina_plugin_service->file_get_int(remminafile, "shareprinter", FALSE)) {
+#ifdef HAVE_CUPS
+ REMMINA_PLUGIN_DEBUG("Sharing printers");
+ const gchar *po = remmina_plugin_service->file_get_string(remminafile, "printer_overrides");
+ if (po && po[0] != 0) {
+ /* Fallback to remmina code to override print drivers */
+ if (cupsEnumDests(CUPS_DEST_FLAGS_NONE, 1000, NULL, 0, 0, remmina_rdp_set_printers, rfi))
+ REMMINA_PLUGIN_DEBUG("All printers have been shared");
+ else
+ REMMINA_PLUGIN_DEBUG("Cannot share printers, are there any available?");
+ } else {
+ /* Use libfreerdp code to map all printers */
+ CLPARAM *d[1];
+ int dcount;
+ dcount = 1;
+ d[0] = "printer";
+ freerdp_client_add_device_channel(rfi->clientContext.context.settings, dcount, d);
+ }
+#endif /* HAVE_CUPS */
+ }
+
+ if (remmina_plugin_service->file_get_int(remminafile, "span", FALSE)) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SpanMonitors, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_UseMultimon, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_Fullscreen, TRUE);
+ remmina_plugin_service->file_set_int(remminafile, "multimon", 1);
+ }
+
+ if (remmina_plugin_service->file_get_int(remminafile, "multimon", FALSE)) {
+ guint32 maxwidth = 0;
+ guint32 maxheight = 0;
+ gchar *monitorids;
+ guint32 i;
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_UseMultimon, TRUE);
+ /* TODO Add an option for this */
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ForceMultimon, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_Fullscreen, TRUE);
+
+ gchar *monitorids_string = g_strdup(remmina_plugin_service->file_get_string(remminafile, "monitorids"));
+ /* Otherwise we get all the attached monitors
+ * monitorids may contains desktop orientation values.
+ * But before we check if there are orientation attributes
+ */
+ if (monitorids_string != NULL && monitorids_string[0] != '\0') {
+ if (g_strstr_len(monitorids_string, -1, ",") != NULL) {
+ if (g_strstr_len(monitorids_string, -1, ":") != NULL) {
+ rdpMonitor *base = (rdpMonitor *)freerdp_settings_get_pointer(rfi->clientContext.context.settings, FreeRDP_MonitorDefArray);
+ /* We have an ID and an orientation degree */
+ gchar **temp_items;
+ gchar **rot_items;
+ temp_items = g_strsplit(monitorids_string, ",", -1);
+ for (i = 0; i < g_strv_length(temp_items); i++) {
+ rot_items = g_strsplit(temp_items[i], ":", -1);
+ rdpMonitor *current = &base[atoi(rot_items[0])];
+ if (i == 0)
+ monitorids = g_strdup(rot_items[0]);
+ else
+ monitorids = g_strdup_printf("%s,%s", monitorids, rot_items[0]);
+ current->attributes.orientation = atoi(rot_items[1]);
+ REMMINA_PLUGIN_DEBUG("Monitor n %d orientation: %d", i, current->attributes.orientation);
+ }
+ } else {
+ monitorids = g_strdup(monitorids_string);
+ }
+ } else {
+ monitorids = g_strdup(monitorids_string);
+ }
+ } else {
+ monitorids = g_strdup(monitorids_string);
+ }
+ remmina_rdp_monitor_get(rfi, &monitorids, &maxwidth, &maxheight);
+ if (monitorids != NULL && monitorids[0] != '\0') {
+ UINT32 *base = (UINT32 *)freerdp_settings_get_pointer(rfi->clientContext.context.settings, FreeRDP_MonitorIds);
+ gchar **items;
+ items = g_strsplit(monitorids, ",", -1);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_NumMonitorIds, g_strv_length(items));
+ REMMINA_PLUGIN_DEBUG("NumMonitorIds: %d", freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_NumMonitorIds));
+ for (i = 0; i < g_strv_length(items); i++) {
+ if (base != NULL){
+ UINT32 *current = &base[i];
+ *current = atoi(items[i]);
+ REMMINA_PLUGIN_DEBUG("Added monitor with ID %" PRIu32, *current);
+ }
+ }
+ g_free(monitorids);
+ g_strfreev(items);
+ }
+ if (maxwidth && maxheight) {
+ REMMINA_PLUGIN_DEBUG("Setting DesktopWidth and DesktopHeight to: %dx%d", maxwidth, maxheight);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth, maxwidth);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight, maxheight);
+ REMMINA_PLUGIN_DEBUG("DesktopWidth and DesktopHeight set to: %dx%d", freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth), freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight));
+ } else {
+ REMMINA_PLUGIN_DEBUG("Cannot set Desktop Size, we are using the previously set values: %dx%d", freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth), freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight));
+ }
+ remmina_plugin_service->protocol_plugin_set_width(gp, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopWidth));
+ remmina_plugin_service->protocol_plugin_set_height(gp, freerdp_settings_get_uint32(rfi->clientContext.context.settings, FreeRDP_DesktopHeight));
+ }
+
+ const gchar *sn = remmina_plugin_service->file_get_string(remminafile, "smartcardname");
+ if (remmina_plugin_service->file_get_int(remminafile, "sharesmartcard", FALSE) ||
+ (sn != NULL && sn[0] != '\0')) {
+ RDPDR_SMARTCARD *smartcard;
+ smartcard = (RDPDR_SMARTCARD *)calloc(1, sizeof(RDPDR_SMARTCARD));
+
+#if FREERDP_VERSION_MAJOR >= 3
+ RDPDR_DEVICE *sdev;
+ sdev = &(smartcard->device);
+#else
+ RDPDR_SMARTCARD *sdev;
+ sdev = smartcard;
+#endif
+
+ sdev->Type = RDPDR_DTYP_SMARTCARD;
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DeviceRedirection, TRUE);
+
+ if (sn != NULL && sn[0] != '\0')
+ sdev->Name = _strdup(sn);
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RedirectSmartCards, TRUE);
+
+ freerdp_device_collection_add(rfi->clientContext.context.settings, (RDPDR_DEVICE *)smartcard);
+ }
+
+ if (remmina_plugin_service->file_get_int(remminafile, "passwordispin", FALSE))
+ /* Option works only combined with Username and Domain, because FreeRDP
+ * doesn’t know anything about info on smart card */
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_PasswordIsSmartcardPin, TRUE);
+
+ /* /serial[:<name>[,<path>[,<driver>[,permissive]]]] */
+ if (remmina_plugin_service->file_get_int(remminafile, "shareserial", FALSE)) {
+ RDPDR_SERIAL *serial;
+ serial = (RDPDR_SERIAL *)calloc(1, sizeof(RDPDR_SERIAL));
+
+#if FREERDP_VERSION_MAJOR >= 3
+ RDPDR_DEVICE *sdev;
+ sdev = &(serial->device);
+#else
+ RDPDR_SERIAL *sdev;
+ sdev = serial;
+#endif
+
+ sdev->Type = RDPDR_DTYP_SERIAL;
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DeviceRedirection, TRUE);
+
+ const gchar *sn = remmina_plugin_service->file_get_string(remminafile, "serialname");
+ if (sn != NULL && sn[0] != '\0')
+ sdev->Name = _strdup(sn);
+
+ const gchar *sd = remmina_plugin_service->file_get_string(remminafile, "serialdriver");
+ if (sd != NULL && sd[0] != '\0')
+ serial->Driver = _strdup(sd);
+
+ const gchar *sp = remmina_plugin_service->file_get_string(remminafile, "serialpath");
+ if (sp != NULL && sp[0] != '\0')
+ serial->Path = _strdup(sp);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "serialpermissive", FALSE))
+ serial->Permissive = _strdup("permissive");
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RedirectSerialPorts, TRUE);
+
+ freerdp_device_collection_add(rfi->clientContext.context.settings, (RDPDR_DEVICE *)serial);
+ }
+
+ if (remmina_plugin_service->file_get_int(remminafile, "shareparallel", FALSE)) {
+ RDPDR_PARALLEL *parallel;
+ parallel = (RDPDR_PARALLEL *)calloc(1, sizeof(RDPDR_PARALLEL));
+
+#if FREERDP_VERSION_MAJOR >= 3
+ RDPDR_DEVICE *pdev;
+ pdev = &(parallel->device);
+#else
+ RDPDR_PARALLEL *pdev;
+ pdev = parallel;
+#endif
+
+ pdev->Type = RDPDR_DTYP_PARALLEL;
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DeviceRedirection, TRUE);
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_RedirectParallelPorts, TRUE);
+
+ const gchar *pn = remmina_plugin_service->file_get_string(remminafile, "parallelname");
+ if (pn != NULL && pn[0] != '\0')
+ pdev->Name = _strdup(pn);
+ const gchar *dp = remmina_plugin_service->file_get_string(remminafile, "parallelpath");
+ if (dp != NULL && dp[0] != '\0')
+ parallel->Path = _strdup(dp);
+
+ freerdp_device_collection_add(rfi->clientContext.context.settings, (RDPDR_DEVICE *)parallel);
+ }
+
+ /**
+ * multitransport enables RDP8 UDP support
+ */
+ if (remmina_plugin_service->file_get_int(remminafile, "multitransport", FALSE)) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_DeviceRedirection, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_SupportMultitransport, TRUE);
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_MultitransportFlags,
+ (TRANSPORT_TYPE_UDP_FECR | TRANSPORT_TYPE_UDP_FECL | TRANSPORT_TYPE_UDP_PREFERRED));
+ } else {
+ freerdp_settings_set_uint32(rfi->clientContext.context.settings, FreeRDP_MultitransportFlags, 0);
+ }
+
+ /* If needed, force interactive authentication by deleting all authentication fields,
+ * forcing libfreerdp to call our callbacks for authentication.
+ * This usually happens from a second attempt of connection, never on the 1st one. */
+ if (rfi->attempt_interactive_authentication) {
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Username, NULL);
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Password, NULL);
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_Domain, NULL);
+
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayDomain, NULL);
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayUsername, NULL);
+ freerdp_settings_set_string(rfi->clientContext.context.settings, FreeRDP_GatewayPassword, NULL);
+
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_GatewayUseSameCredentials, FALSE);
+ }
+
+ gboolean orphaned;
+
+ if (!freerdp_connect(rfi->clientContext.context.instance)) {
+ orphaned = (GET_PLUGIN_DATA(rfi->protocol_widget) == NULL);
+ if (!orphaned) {
+ UINT32 e;
+
+ e = freerdp_get_last_error(&rfi->clientContext.context);
+
+ switch (e) {
+ case FREERDP_ERROR_AUTHENTICATION_FAILED:
+ case STATUS_LOGON_FAILURE: // wrong return code from FreeRDP introduced at the end of July 2016? (fixed with b86c0ba)
+#ifdef FREERDP_ERROR_CONNECT_LOGON_FAILURE
+ case FREERDP_ERROR_CONNECT_LOGON_FAILURE:
+#endif
+ /* Logon failure, will retry with interactive authentication */
+ rfi->attempt_interactive_authentication = TRUE;
+ break;
+ case STATUS_ACCOUNT_LOCKED_OUT:
+#ifdef FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT
+ case FREERDP_ERROR_CONNECT_ACCOUNT_LOCKED_OUT:
+#endif
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nAccount locked out."),
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ case STATUS_ACCOUNT_EXPIRED:
+#ifdef FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED
+ case FREERDP_ERROR_CONNECT_ACCOUNT_EXPIRED:
+#endif
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nAccount expired."),
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ case STATUS_PASSWORD_EXPIRED:
+#ifdef FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED
+ case FREERDP_ERROR_CONNECT_PASSWORD_EXPIRED:
+#endif
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nPassword expired."),
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ case STATUS_ACCOUNT_DISABLED:
+#ifdef FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED
+ case FREERDP_ERROR_CONNECT_ACCOUNT_DISABLED:
+#endif
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nAccount disabled."),
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+#ifdef FREERDP_ERROR_SERVER_INSUFFICIENT_PRIVILEGES
+ /* https://msdn.microsoft.com/en-us/library/ee392247.aspx */
+ case FREERDP_ERROR_SERVER_INSUFFICIENT_PRIVILEGES:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nInsufficient user privileges."),
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+#endif
+ case STATUS_ACCOUNT_RESTRICTION:
+#ifdef FREERDP_ERROR_CONNECT_ACCOUNT_RESTRICTION
+ case FREERDP_ERROR_CONNECT_ACCOUNT_RESTRICTION:
+#endif
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nAccount restricted."),
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+
+ case STATUS_PASSWORD_MUST_CHANGE:
+#ifdef FREERDP_ERROR_CONNECT_PASSWORD_MUST_CHANGE
+ case FREERDP_ERROR_CONNECT_PASSWORD_MUST_CHANGE:
+#endif
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not access the RDP server “%s”.\nChange user password before connecting."),
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+
+ case FREERDP_ERROR_CONNECT_FAILED:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Lost connection to the RDP server “%s”."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ case FREERDP_ERROR_DNS_NAME_NOT_FOUND:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not find the address for the RDP server “%s”."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ case FREERDP_ERROR_TLS_CONNECT_FAILED:
+ remmina_plugin_service->protocol_plugin_set_error(gp,
+ _("Could not connect to the RDP server “%s” via TLS. See the DEBUG traces from a terminal for more information."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ case FREERDP_ERROR_SECURITY_NEGO_CONNECT_FAILED:
+ // TRANSLATORS: the placeholder may be either an IP/FQDN or a server hostname
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Unable to establish a connection to the RDP server “%s”. Check “Security protocol negotiation”."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+#ifdef FREERDP_ERROR_POST_CONNECT_FAILED
+ case FREERDP_ERROR_POST_CONNECT_FAILED:
+ /* remmina_rdp_post_connect() returned FALSE to libfreerdp. We saved the error on rfi->postconnect_error */
+ switch (rfi->postconnect_error) {
+ case REMMINA_POSTCONNECT_ERROR_OK:
+ /* We should never come here */
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Cannot connect to the RDP server “%s”."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ case REMMINA_POSTCONNECT_ERROR_GDI_INIT:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Could not start libfreerdp-gdi."));
+ break;
+ case REMMINA_POSTCONNECT_ERROR_NO_H264:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("You requested a H.264 GFX mode for the server “%s”, but your libfreerdp does not support H.264. Please use a non-AVC colour depth setting."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ }
+ break;
+#endif
+#ifdef FREERDP_ERROR_SERVER_DENIED_CONNECTION
+ case FREERDP_ERROR_SERVER_DENIED_CONNECTION:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("The “%s” server refused the connection."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+#endif
+ case 0x800759DB:
+ // E_PROXY_NAP_ACCESSDENIED https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tsgu/84cd92e4-592c-4219-95d8-18021ac654b0
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("The Remote Desktop Gateway “%s” denied the user “%s\\%s” access due to policy."),
+ freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_GatewayHostname), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_GatewayDomain), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_GatewayUsername));
+ break;
+
+ case FREERDP_ERROR_CONNECT_NO_OR_MISSING_CREDENTIALS:
+ rfi->user_cancelled = TRUE;
+ break;
+
+ default:
+ g_printf("libfreerdp returned code is %08X\n", e);
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Cannot connect to the “%s” RDP server."), freerdp_settings_get_string(rfi->clientContext.context.settings, FreeRDP_ServerHostname));
+ break;
+ }
+ }
+
+ return FALSE;
+ }
+
+ if (GET_PLUGIN_DATA(rfi->protocol_widget) == NULL) orphaned = true; else orphaned = false;
+ if (!orphaned && freerdp_get_last_error(&rfi->clientContext.context) == FREERDP_ERROR_SUCCESS && !rfi->user_cancelled)
+ remmina_rdp_main_loop(gp);
+
+ return TRUE;
+}
+
+static void rfi_uninit(rfContext *rfi)
+{
+ freerdp *instance;
+
+ instance = rfi->clientContext.context.instance;
+
+ if (rfi->remmina_plugin_thread) {
+ rfi->thread_cancelled = TRUE; // Avoid all rf_queue function to run
+ pthread_cancel(rfi->remmina_plugin_thread);
+ if (rfi->remmina_plugin_thread)
+ pthread_join(rfi->remmina_plugin_thread, NULL);
+ }
+
+ if (instance) {
+ if (rfi->connected) {
+ freerdp_abort_connect_context(&rfi->clientContext.context);
+ rfi->connected = false;
+ }
+ }
+
+ if (instance) {
+ RDP_CLIENT_ENTRY_POINTS *pEntryPoints = instance->pClientEntryPoints;
+ if (pEntryPoints)
+ IFCALL(pEntryPoints->GlobalUninit);
+ free(instance->pClientEntryPoints);
+ freerdp_context_free(instance); /* context is rfContext* rfi */
+ freerdp_free(instance); /* This implicitly frees instance->context and rfi is no longer valid */
+ }
+}
+
+static gboolean complete_cleanup_on_main_thread(gpointer data)
+{
+ TRACE_CALL(__func__);
+
+ gboolean orphaned;
+ rfContext *rfi = (rfContext *)data;
+ RemminaProtocolWidget *gp;
+
+ remmina_rdp_clipboard_free(rfi);
+
+ gdi_free(rfi->clientContext.context.instance);
+
+ gp = rfi->protocol_widget;
+ if (GET_PLUGIN_DATA(gp) == NULL) orphaned = true; else orphaned = false;
+
+ remmina_rdp_cliprdr_detach_owner(gp);
+ if (!orphaned) remmina_rdp_event_uninit(gp);
+
+ if (!orphaned) g_object_steal_data(G_OBJECT(gp), "plugin-data");
+
+ rfi_uninit(rfi);
+
+ /* Notify the RemminaProtocolWidget that we closed our connection, and the GUI interface
+ * can be removed */
+ if (!orphaned)
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gpointer remmina_rdp_main_thread(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp;
+ rfContext *rfi;
+
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+ CANCEL_ASYNC
+
+ gp = (RemminaProtocolWidget *)data;
+
+ rfi = GET_PLUGIN_DATA(gp);
+
+ rfi->attempt_interactive_authentication = FALSE;
+ do
+ remmina_rdp_main(gp);
+ while (!remmina_plugin_service->protocol_plugin_has_error(gp) && rfi->attempt_interactive_authentication == TRUE && !rfi->user_cancelled);
+
+ rfi->remmina_plugin_thread = 0;
+
+ /* cleanup */
+ g_idle_add(complete_cleanup_on_main_thread, (gpointer)rfi);
+
+
+ return NULL;
+}
+
+static void remmina_rdp_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ freerdp *instance;
+ rfContext *rfi;
+
+ instance = freerdp_new();
+ instance->PreConnect = remmina_rdp_pre_connect;
+ instance->PostConnect = remmina_rdp_post_connect;
+ instance->PostDisconnect = remmina_rdp_post_disconnect;
+ //instance->VerifyCertificate = remmina_rdp_verify_certificate;
+ instance->VerifyCertificateEx = remmina_rdp_verify_certificate_ex;
+ //instance->VerifyChangedCertificate = remmina_rdp_verify_changed_certificate;
+ instance->VerifyChangedCertificateEx = remmina_rdp_verify_changed_certificate_ex;
+#if FREERDP_VERSION_MAJOR >= 3
+ instance->AuthenticateEx = remmina_rdp_authenticate_ex;
+ instance->ChooseSmartcard = remmina_rdp_choose_smartcard;
+ instance->GetAccessToken = remmina_rdp_get_access_token;
+ instance->LoadChannels = freerdp_client_load_channels;
+ instance->PresentGatewayMessage = remmina_rdp_present_gateway_message;
+ instance->LogonErrorInfo = remmina_rdp_logon_error_info;
+ instance->RetryDialog = remmina_rdp_retry_dialog;
+ instance->PostFinalDisconnect = remmina_rdp_post_final_disconnect;
+#else
+ instance->Authenticate = remmina_rdp_authenticate;
+ instance->GatewayAuthenticate = remmina_rdp_gw_authenticate;
+#endif
+
+ instance->ContextSize = sizeof(rfContext);
+ freerdp_context_new(instance);
+ rfi = (rfContext *)instance->context;
+
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", rfi, free);
+
+ rfi->protocol_widget = gp;
+ rfi->clientContext.context.settings = instance->context->settings;
+ rfi->connected = false;
+ rfi->is_reconnecting = false;
+ rfi->stop_reconnecting_requested = false;
+ rfi->user_cancelled = FALSE;
+ rfi->last_x = 0;
+ rfi->last_y = 0;
+
+ freerdp_register_addin_provider(freerdp_channels_load_static_addin_entry, 0);
+
+ remmina_rdp_event_init(gp);
+}
+
+static gboolean remmina_rdp_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+ const gchar *profile_name, *p;
+ gchar thname[16], c;
+ gint nthname = 0;
+
+ rfi->scale = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp);
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+
+ if (pthread_create(&rfi->remmina_plugin_thread, NULL, remmina_rdp_main_thread, gp)) {
+ remmina_plugin_service->protocol_plugin_set_error(gp, "%s",
+ "Could not start pthread.");
+
+ rfi->remmina_plugin_thread = 0;
+
+ return FALSE;
+ }
+
+ /* Generate a thread name to be used with pthread_setname_np() for debugging */
+ profile_name = remmina_plugin_service->file_get_string(remminafile, "name");
+ p = profile_name;
+ strcpy(thname, "RemmRDP:");
+ if (p) {
+ nthname = strlen(thname);
+ while ((c = *p) != 0 && nthname < sizeof(thname) - 1) {
+ if (isalnum(c))
+ thname[nthname++] = c;
+ p++;
+ }
+ } else {
+ strcat(thname, "<NONAM>");
+ nthname = strlen(thname);
+ }
+ thname[nthname] = 0;
+#if defined(__linux__)
+ pthread_setname_np(rfi->remmina_plugin_thread, thname);
+#elif defined(__FreeBSD__)
+ pthread_set_name_np(rfi->remmina_plugin_thread, thname);
+#endif
+
+ return TRUE;
+}
+
+static gboolean remmina_rdp_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ REMMINA_PLUGIN_DEBUG("Requesting to close the connection");
+ RemminaPluginRdpEvent rdp_event = { 0 };
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ if (!remmina_plugin_service->is_main_thread())
+ g_warning("WARNING: %s called on a subthread, which may not work or crash Remmina.", __func__);
+
+ if (rfi && !rfi->connected) {
+ /* libfreerdp is attempting to connect, we cannot interrupt our main thread
+ * in the connect phase.
+ * So we remove "plugin-data" from gp, so our rfi remains "orphan"
+ */
+ remmina_rdp_event_uninit(gp);
+ g_object_steal_data(G_OBJECT(gp), "plugin-data");
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ return FALSE;
+ }
+
+
+ if (rfi && rfi->clipboard.srv_clip_data_wait == SCDW_BUSY_WAIT) {
+ REMMINA_PLUGIN_DEBUG("[RDP] requesting clipboard transfer to abort");
+ /* Allow clipboard transfer from server to terminate */
+ rfi->clipboard.srv_clip_data_wait = SCDW_ABORTING;
+ usleep(100000);
+ }
+
+ if (rfi->is_reconnecting) {
+ /* Special case: window closed when attempting to reconnect */
+ rfi->stop_reconnecting_requested = TRUE;
+ return FALSE;
+ }
+
+ rdp_event.type = REMMINA_RDP_EVENT_DISCONNECT;
+ remmina_rdp_event_event_push(gp, &rdp_event);
+
+ return FALSE;
+}
+
+static gboolean remmina_rdp_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ return TRUE;
+}
+
+static void remmina_rdp_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ switch (feature->id) {
+ case REMMINA_RDP_FEATURE_UNFOCUS:
+ remmina_rdp_event_unfocus(gp);
+ break;
+
+ case REMMINA_RDP_FEATURE_SCALE:
+ if (rfi) {
+ rfi->scale = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp);
+ remmina_rdp_event_update_scale(gp);
+ } else {
+ REMMINA_PLUGIN_DEBUG("Remmina RDP plugin warning: Null value for rfi by REMMINA_RDP_FEATURE_SCALE");
+ }
+ break;
+
+ case REMMINA_RDP_FEATURE_MULTIMON:
+ if (rfi) {
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "multimon", FALSE)) {
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_UseMultimon, TRUE);
+ /* TODO Add an option for this */
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_ForceMultimon, TRUE);
+ freerdp_settings_set_bool(rfi->clientContext.context.settings, FreeRDP_Fullscreen, TRUE);
+ remmina_rdp_event_send_delayed_monitor_layout(gp);
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("Remmina RDP plugin warning: Null value for rfi by REMMINA_RDP_FEATURE_MULTIMON");
+ }
+ break;
+
+ case REMMINA_RDP_FEATURE_DYNRESUPDATE:
+ break;
+
+ case REMMINA_RDP_FEATURE_TOOL_REFRESH:
+ if (rfi)
+ gtk_widget_queue_draw_area(rfi->drawing_area, 0, 0,
+ remmina_plugin_service->protocol_plugin_get_width(gp),
+ remmina_plugin_service->protocol_plugin_get_height(gp));
+ else
+ REMMINA_PLUGIN_DEBUG("Remmina RDP plugin warning: Null value for rfi by REMMINA_RDP_FEATURE_TOOL_REFRESH");
+ break;
+
+ case REMMINA_RDP_FEATURE_TOOL_SENDCTRLALTDEL:
+ remmina_rdp_send_ctrlaltdel(gp);
+ break;
+
+ default:
+ break;
+ }
+}
+
+/* Send a keystroke to the plugin window */
+static void remmina_rdp_keystroke(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen)
+{
+ TRACE_CALL(__func__);
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+
+ remmina_plugin_service->protocol_plugin_send_keys_signals(rfi->drawing_area,
+ keystrokes, keylen, GDK_KEY_PRESS | GDK_KEY_RELEASE);
+ return;
+}
+
+static gboolean remmina_rdp_get_screenshot(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd)
+{
+ rfContext *rfi = GET_PLUGIN_DATA(gp);
+ rdpGdi *gdi;
+ size_t szmem;
+
+ UINT32 bytesPerPixel;
+ UINT32 bitsPerPixel;
+
+ if (!rfi)
+ return FALSE;
+
+ gdi = ((rdpContext *)rfi)->gdi;
+
+#if FREERDP_VERSION_MAJOR >= 3
+ bytesPerPixel = FreeRDPGetBytesPerPixel(gdi->hdc->format);
+ bitsPerPixel = FreeRDPGetBitsPerPixel(gdi->hdc->format);
+#else
+ bytesPerPixel = GetBytesPerPixel(gdi->hdc->format);
+ bitsPerPixel = GetBitsPerPixel(gdi->hdc->format);
+#endif
+
+ /** @todo we should lock FreeRDP subthread to update rfi->primary_buffer, rfi->gdi and w/h,
+ * from here to memcpy, but… how ? */
+
+ szmem = gdi->width * gdi->height * bytesPerPixel;
+
+ REMMINA_PLUGIN_DEBUG("allocating %zu bytes for a full screenshot", szmem);
+ rpsd->buffer = malloc(szmem);
+ if (!rpsd->buffer) {
+ REMMINA_PLUGIN_DEBUG("could not set aside %zu bytes for a full screenshot", szmem);
+ return FALSE;
+ }
+ rpsd->width = gdi->width;
+ rpsd->height = gdi->height;
+ rpsd->bitsPerPixel = bitsPerPixel;
+ rpsd->bytesPerPixel = bytesPerPixel;
+
+ memcpy(rpsd->buffer, gdi->primary_buffer, szmem);
+
+ /* Returning TRUE instruct also the caller to deallocate rpsd->buffer */
+ return TRUE;
+}
+
+/* Array of key/value pairs for colour depths */
+static gpointer colordepth_list[] =
+{
+ /* 1st one is the default in a new install */
+ "99", N_("Automatic (32 bpp) (Server chooses its best format)"),
+ "66", N_("GFX AVC444 (32 bpp)"),
+ "65", N_("GFX AVC420 (32 bpp)"),
+ "64", N_("GFX RFX (32 bpp)"),
+ "63", N_("GFX RFX Progressive (32 bpp)"),
+ "0", N_("RemoteFX (32 bpp)"),
+ "32", N_("True colour (32 bpp)"),
+ "24", N_("True colour (24 bpp)"),
+ "16", N_("High colour (16 bpp)"),
+ "15", N_("High colour (15 bpp)"),
+ "8", N_("256 colours (8 bpp)"),
+ NULL
+};
+
+/* Array of key/value pairs for the FreeRDP logging level */
+static gpointer log_level[] =
+{
+ "INFO", "INFO",
+ "FATAL", "FATAL",
+ "ERROR", "ERROR",
+ "WARN", "WARN",
+ "DEBUG", "DEBUG",
+ "TRACE", "TRACE",
+ "OFF", "OFF",
+ NULL
+};
+
+
+/* Array of key/value pairs for quality selection */
+static gpointer quality_list[] =
+{
+ "0", N_("Poor (fastest)"),
+ "1", N_("Medium"),
+ "2", N_("Good"),
+ "9", N_("Best (slowest)"),
+ NULL
+};
+
+/* Array of key/value pairs for quality selection */
+static gpointer network_list[] =
+{
+ "none", N_("None"),
+ "autodetect", N_("Auto-detect"),
+ "modem", N_("Modem"),
+ "broadband-low", N_("Low performance broadband"),
+ "satellite", N_("Satellite"),
+ "broadband-high", N_("High performance broadband"),
+ "wan", N_("WAN"),
+ "lan", N_("LAN"),
+ NULL
+};
+
+/* Array of key/value pairs for sound options */
+static gpointer sound_list[] =
+{
+ "off", N_("Off"),
+ "local", N_("Local"),
+ "remote", N_("Remote"),
+ NULL
+};
+
+/* Array of key/value pairs for security */
+static gpointer security_list[] =
+{
+ "", N_("Automatic negotiation"),
+ "nla", N_("NLA protocol security"),
+ "tls", N_("TLS protocol security"),
+ "rdp", N_("RDP protocol security"),
+ "ext", N_("NLA extended protocol security"),
+ NULL
+};
+
+/* Array of key/value pairs for mouse movement */
+static gpointer mouse_jitter_list[] =
+{
+ "No", N_("No"),
+ "60", N_("Every 1 min"),
+ "180", N_("Every 3 min"),
+ "300", N_("Every 5 min"),
+ "600", N_("Every 10 min"),
+ NULL
+};
+
+static gpointer gwtransp_list[] =
+{
+ "http", "HTTP",
+ "rpc", "RPC",
+ "auto", "Auto",
+ NULL
+};
+
+static gpointer tls_seclevel[] =
+{
+ "", N_("Default"),
+ "0", N_("0 — Windows 7 compatible"),
+ "1", N_("1"),
+ "2", N_("2"),
+ "3", N_("3"),
+ "4", N_("4"),
+ "5", N_("5"),
+ NULL
+};
+
+static gchar clientbuild_list[] =
+ N_("2600 (Windows XP), 7601 (Windows Vista/7), 9600 (Windows 8 and newer)");
+
+static gchar clientbuild_tooltip[] =
+ N_("Used i.a. by terminal services in a smart card channel to distinguish client capabilities:\n"
+ " • < 4034: Windows XP base smart card functions\n"
+ " • 4034-7064: Windows Vista/7: SCardReadCache(),\n"
+ " SCardWriteCache(), SCardGetTransmitCount()\n"
+ " • >= 7065: Windows 8 and newer: SCardGetReaderIcon(),\n"
+ " SCardGetDeviceTypeId()");
+
+static gchar microphone_tooltip[] =
+ N_("Options for redirection of audio input:\n"
+ " • [sys:<sys>,][dev:<dev>,][format:<format>,][rate:<rate>,]\n"
+ " [channel:<channel>] Audio input (microphone)\n"
+ " • sys:pulse\n"
+ " • format:1\n"
+ " • sys:oss,dev:1,format:1\n"
+ " • sys:alsa");
+
+static gchar audio_tooltip[] =
+ N_("Options for redirection of audio output:\n"
+ " • [sys:<sys>,][dev:<dev>,][format:<format>,][rate:<rate>,]\n"
+ " [channel:<channel>] Audio output\n"
+ " • sys:pulse\n"
+ " • format:1\n"
+ " • sys:oss,dev:1,format:1\n"
+ " • sys:alsa");
+
+
+static gchar usb_tooltip[] =
+ N_("Options for redirection of USB device:\n"
+ " • [dbg,][id:<vid>:<pid>#…,][addr:<bus>:<addr>#…,][auto]\n"
+ " • auto\n"
+ " • id:054c:0268#4669:6e6b,addr:04:0c");
+
+static gchar timeout_tooltip[] =
+ N_("Advanced setting for high latency links:\n"
+ "Adjusts the connection timeout. Use if your connection times out.\n"
+ "The highest possible value is 600000 ms (10 minutes).\n");
+
+static gchar network_tooltip[] =
+ N_("Performance optimisations based on the network connection type:\n"
+ "Using auto-detection is advised.\n"
+ "If “Auto-detect” fails, choose the most appropriate option in the list.\n");
+
+static gchar monitorids_tooltip[] =
+ N_("Comma-separated list of monitor IDs and desktop orientations:\n"
+ " • [<id>:<orientation-in-degrees>,]\n"
+ " • 0,1,2,3\n"
+ " • 0:270,1:90\n"
+ "Orientations are specified in degrees, valid values are:\n"
+ " • 0 (landscape)\n"
+ " • 90 (portrait)\n"
+ " • 180 (landscape flipped)\n"
+ " • 270 (portrait flipped)\n"
+ "\n");
+
+static gchar drive_tooltip[] =
+ N_("Redirect directory <path> as named share <name>.\n"
+ " • <name>,<fullpath>[;<name>,<fullpath>[;…]]\n"
+ " • MyHome,/home/remminer\n"
+ " • /home/remminer\n"
+ " • MyHome,/home/remminer;SomePath,/path/to/somepath\n"
+ "Hotplug support is enabled with:\n"
+ " • hotplug,*\n"
+ "\n");
+
+/* Array of RemminaProtocolSetting for basic settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting tooltip
+ * g) Validation data pointer, will be passed to the validation callback method.
+ * h) Validation callback method (Can be NULL. Every entry will be valid then.)
+ * use following prototype:
+ * gboolean mysetting_validator_method(gpointer key, gpointer value,
+ * gpointer validator_data);
+ * gpointer key is a gchar* containing the setting's name,
+ * gpointer value contains the value which should be validated,
+ * gpointer validator_data contains your passed data.
+ */
+static const RemminaProtocolSetting remmina_rdp_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("Password"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "domain", N_("Domain"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "drive", N_("Share folder"), FALSE, NULL, drive_tooltip, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "restricted-admin", N_("Restricted admin mode"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "pth", N_("Password hash"), FALSE, NULL, N_("Restricted admin mode password hash"), NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "left-handed", N_("Left-handed mouse support"), TRUE, NULL, N_("Swap left and right mouse buttons for left-handed mouse support"), NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disable-smooth-scrolling", N_("Disable smooth scrolling"), TRUE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "multimon", N_("Enable multi monitor"), TRUE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "span", N_("Span screen over multiple monitors"), TRUE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "monitorids", N_("List monitor IDs"), FALSE, NULL, monitorids_tooltip, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION, "resolution", NULL, FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "colordepth", N_("Colour depth"), FALSE, colordepth_list, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "network", N_("Network connection type"), FALSE, network_list, network_tooltip, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP, "keymap", NULL, FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL }
+};
+
+/* Array of RemminaProtocolSetting for advanced settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting Tooltip
+ */
+static const RemminaProtocolSetting remmina_rdp_advanced_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "quality", N_("Quality"), FALSE, quality_list, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "security", N_("Security protocol negotiation"), FALSE, security_list, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "gwtransp", N_("Gateway transport type"), FALSE, gwtransp_list, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "tls-seclevel", N_("TLS Security Level"), FALSE, tls_seclevel, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "freerdp_log_level", N_("FreeRDP log level"), FALSE, log_level, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "freerdp_log_filters", N_("FreeRDP log filters"), FALSE, NULL, N_("tag:level[,tag:level[,…]]") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "sound", N_("Audio output mode"), FALSE, sound_list, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "audio-output", N_("Redirect local audio output"), TRUE, NULL, audio_tooltip },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "microphone", N_("Redirect local microphone"), TRUE, NULL, microphone_tooltip },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "timeout", N_("Connection timeout in ms"), TRUE, NULL, timeout_tooltip },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "gateway_server", N_("Remote Desktop Gateway server"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "gateway_username", N_("Remote Desktop Gateway username"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "gateway_password", N_("Remote Desktop Gateway password"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "gateway_domain", N_("Remote Desktop Gateway domain"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "clientname", N_("Client name"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "clientbuild", N_("Client build"), FALSE, clientbuild_list, clientbuild_tooltip },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "exec", N_("Start-up program"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "execpath", N_("Start-up path"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "loadbalanceinfo", N_("Load balance info"), FALSE, NULL, NULL },
+ // TRANSLATORS: Do not use typographic quotation marks, these must stay as "double quote", also know as “Typewriter ("programmer's") quote, ambidextrous.”
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "printer_overrides", N_("Override printer drivers"), FALSE, NULL, N_("\"Samsung_CLX-3300_Series\":\"Samsung CLX-3300 Series PS\";\"Canon MF410\":\"Canon MF410 Series UFR II\"") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "usb", N_("USB device redirection"), TRUE, NULL, usb_tooltip },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "serialname", N_("Local serial name"), FALSE, NULL, N_("COM1, COM2, etc.") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "serialdriver", N_("Local serial driver"), FALSE, NULL, N_("Serial") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "serialpath", N_("Local serial path"), FALSE, NULL, N_("/dev/ttyS0, /dev/ttyS1, etc.") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "parallelname", N_("Local parallel name"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "parallelpath", N_("Local parallel device"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "smartcardname", N_("Name of smart card"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "dvc", N_("Dynamic virtual channel"), FALSE, NULL, N_("<channel>[,<options>]") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "vc", N_("Static virtual channel"), FALSE, NULL, N_("<channel>[,<options>]") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "rdp2tcp", N_("TCP redirection"), FALSE, NULL, N_("/PATH/TO/rdp2tcp") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "rdp_reconnect_attempts", N_("Reconnect attempts number"), FALSE, NULL, N_("The maximum number of reconnect attempts upon an RDP disconnect (default: 20)") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "rdp_mouse_jitter", N_("Move mouse when connection is idle"), FALSE, mouse_jitter_list, NULL },
+
+ { REMMINA_PROTOCOL_SETTING_TYPE_ASSISTANCE, "assistance_mode", N_("Attempt to connect in assistance mode"), TRUE, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "preferipv6", N_("Prefer IPv6 AAAA record over IPv4 A record"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "shareprinter", N_("Share printers"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "shareserial", N_("Share serial ports"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "serialpermissive", N_("(SELinux) permissive mode for serial ports"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "shareparallel", N_("Share parallel ports"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "sharesmartcard", N_("Share a smart card"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableclipboard", N_("Turn off clipboard sync"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "cert_ignore", N_("Ignore certificate"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "old-license", N_("Use the old license workflow"), TRUE, NULL, N_("It disables CAL and hwId is set to 0") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "console", N_("Attach to console (2003/2003 R2)"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disable_fastpath", N_("Turn off fast-path"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "gateway_usage", N_("Server detection using Remote Desktop Gateway"), TRUE, NULL, NULL },
+#if defined(PROXY_TYPE_IGNORE)
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "useproxyenv", N_("Use system proxy settings"), TRUE, NULL, NULL },
+#endif
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableautoreconnect", N_("Turn off automatic reconnection"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "relax-order-checks", N_("Relax order checks"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "glyph-cache", N_("Glyph cache"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "multitransport", N_("Enable multitransport protocol (UDP)"), TRUE, NULL, N_("Using the UDP protocol may improve performance") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "base-cred-for-gw", N_("Use base credentials for gateway too"), TRUE, NULL, NULL },
+#if FREERDP_CHECK_VERSION(2, 3, 1)
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "websockets", N_("Enable Gateway websockets support"), TRUE, NULL, NULL },
+#endif
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "no-suppress", N_("Update framebuffer even when not visible"), TRUE, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL }
+};
+
+/* Array for available features.
+ * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */
+static const RemminaProtocolFeature remmina_rdp_features[] =
+{
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_RDP_FEATURE_VIEWONLY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "viewonly",
+ N_("View only") },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_RDP_FEATURE_TOOL_REFRESH, N_("Refresh"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, REMMINA_RDP_FEATURE_SCALE, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE, REMMINA_RDP_FEATURE_DYNRESUPDATE, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON, REMMINA_RDP_FEATURE_MULTIMON, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_RDP_FEATURE_TOOL_SENDCTRLALTDEL, N_("Send Ctrl+Alt+Delete"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_UNFOCUS, REMMINA_RDP_FEATURE_UNFOCUS, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL }
+};
+
+/* This will be filled with version info string */
+static char remmina_plugin_rdp_version[256];
+
+/* Protocol plugin definition and features */
+static RemminaProtocolPlugin remmina_rdp =
+{
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ "RDP", // Name
+ N_("RDP - Remote Desktop Protocol"), // Description
+ GETTEXT_PACKAGE, // Translation domain
+ remmina_plugin_rdp_version, // Version number
+ "org.remmina.Remmina-rdp-symbolic", // Icon for normal connection
+ "org.remmina.Remmina-rdp-ssh-symbolic", // Icon for SSH connection
+ remmina_rdp_basic_settings, // Array for basic settings
+ remmina_rdp_advanced_settings, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type
+ remmina_rdp_features, // Array for available features
+ remmina_rdp_init, // Plugin initialization
+ remmina_rdp_open_connection, // Plugin open connection
+ remmina_rdp_close_connection, // Plugin close connection
+ remmina_rdp_query_feature, // Query for available features
+ remmina_rdp_call_feature, // Call a feature
+ remmina_rdp_keystroke, // Send a keystroke
+ remmina_rdp_get_screenshot, // Screenshot
+ remmina_rdp_event_on_map, // RCW map event
+ remmina_rdp_event_on_unmap // RCW unmap event
+};
+
+/* File plugin definition and features */
+static RemminaFilePlugin remmina_rdpf =
+{
+ REMMINA_PLUGIN_TYPE_FILE, // Type
+ "RDPF", // Name
+ N_("RDP - RDP File Handler"), // Description
+ GETTEXT_PACKAGE, // Translation domain
+ remmina_plugin_rdp_version, // Version number
+ remmina_rdp_file_import_test, // Test import function
+ remmina_rdp_file_import, // Import function
+ remmina_rdp_file_export_test, // Test export function
+ remmina_rdp_file_export, // Export function
+ NULL
+};
+
+/* Preferences plugin definition and features */
+static RemminaPrefPlugin remmina_rdps =
+{
+ REMMINA_PLUGIN_TYPE_PREF, // Type
+ "RDPS", // Name
+ N_("RDP - Preferences"), // Description
+ GETTEXT_PACKAGE, // Translation domain
+ remmina_plugin_rdp_version, // Version number
+ "RDP", // Label
+ remmina_rdp_settings_new // Preferences body function
+};
+
+static char *buildconfig_strstr(const char *bc, const char *option)
+{
+ TRACE_CALL(__func__);
+
+ char *p, *n;
+
+ p = strcasestr(bc, option);
+ if (p == NULL)
+ return NULL;
+
+ if (p > bc && *(p - 1) > ' ')
+ return NULL;
+
+ n = p + strlen(option);
+ if (*n > ' ')
+ return NULL;
+
+ return p;
+}
+
+G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service)
+{
+ int vermaj, vermin, verrev;
+
+ TRACE_CALL(__func__);
+ remmina_plugin_service = service;
+
+ /* Check that we are linked to the correct version of libfreerdp */
+
+ freerdp_get_version(&vermaj, &vermin, &verrev);
+ if ((vermaj < FREERDP_REQUIRED_MAJOR) ||
+ ((vermaj == FREERDP_REQUIRED_MAJOR) && ((vermin < FREERDP_REQUIRED_MINOR) ||
+ ((vermin == FREERDP_REQUIRED_MINOR) && (verrev < FREERDP_REQUIRED_REVISION))))) {
+ g_printf("Upgrade your FreeRDP library version from %d.%d.%d to at least libfreerdp %d.%d.%d "
+ "to run the RDP plugin.\n",
+ vermaj, vermin, verrev,
+ FREERDP_REQUIRED_MAJOR, FREERDP_REQUIRED_MINOR, FREERDP_REQUIRED_REVISION);
+ return FALSE;
+ }
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+ if (!service->register_plugin((RemminaPlugin *)&remmina_rdp))
+ return FALSE;
+
+ remmina_rdpf.export_hints = _("Export connection in Windows .rdp file format");
+
+ if (!service->register_plugin((RemminaPlugin *)&remmina_rdpf))
+ return FALSE;
+
+ if (!service->register_plugin((RemminaPlugin *)&remmina_rdps))
+ return FALSE;
+
+ if (buildconfig_strstr(freerdp_get_build_config(), "WITH_GFX_H264=ON")) {
+ gfx_h264_available = TRUE;
+ REMMINA_PLUGIN_DEBUG("gfx_h264_available: %d", gfx_h264_available);
+ } else {
+ gfx_h264_available = FALSE;
+ REMMINA_PLUGIN_DEBUG("gfx_h264_available: %d", gfx_h264_available);
+ /* Remove values 65 and 66 from colordepth_list array by shifting it */
+ gpointer *src, *dst;
+ dst = src = colordepth_list;
+ while (*src) {
+ if (strcmp(*src, "66") != 0 && strcmp(*src, "65") != 0) {
+ if (dst != src) {
+ *dst = *src;
+ *(dst + 1) = *(src + 1);
+ }
+ dst += 2;
+ }
+ src += 2;
+ }
+ *dst = NULL;
+ }
+
+ snprintf(remmina_plugin_rdp_version, sizeof(remmina_plugin_rdp_version),
+ "RDP plugin: %s (Git %s), Compiled with libfreerdp %s (%s), Running with libfreerdp %s (rev %s), H.264 %s",
+ VERSION, REMMINA_GIT_REVISION,
+#if FREERDP_VERSION_MAJOR >= 3
+ FREERDP_VERSION_FULL, FREERDP_GIT_REVISION,
+#else
+ FREERDP_VERSION_FULL, GIT_REVISION,
+#endif
+ freerdp_get_version_string(),
+ freerdp_get_build_revision(),
+ gfx_h264_available ? "Yes" : "No"
+ );
+
+ remmina_rdp_settings_init();
+
+ return TRUE;
+}
diff --git a/plugins/rdp/rdp_plugin.h b/plugins/rdp/rdp_plugin.h
new file mode 100644
index 0000000..a0f0cc7
--- /dev/null
+++ b/plugins/rdp/rdp_plugin.h
@@ -0,0 +1,400 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#include "common/remmina_plugin.h"
+#include <freerdp/freerdp.h>
+#include <freerdp/version.h>
+#include <freerdp/channels/channels.h>
+#include <freerdp/codec/color.h>
+#include <freerdp/codec/rfx.h>
+#include <freerdp/gdi/gdi.h>
+#include <freerdp/gdi/dc.h>
+#include <freerdp/gdi/region.h>
+#include <freerdp/client/cliprdr.h>
+#include <freerdp/client/disp.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#elif defined(GDK_WINDOWING_WAYLAND)
+#include <gdk/gdkwayland.h>
+#endif
+
+#include <winpr/clipboard.h>
+
+/**
+ * FREERDP_CHECK_VERSION:
+ * @major: major version (e.g. 2 for version 2.1.0)
+ * @minor: minor version (e.g. 0 for version 2.1.0)
+ * @release: release version (e.g. 0 for version 2.1.0)
+ *
+ * Returns %TRUE if the version of the FRERDDP header files
+ * is the same as or newer than the passed-in version.
+ *
+ * Returns: %TRUE if FREERDP headers are new enough
+ */
+#define FREERDP_CHECK_VERSION(major,minor,revision) \
+ (FREERDP_VERSION_MAJOR > (major) || \
+ (FREERDP_VERSION_MAJOR == (major) && FREERDP_VERSION_MINOR > (minor)) || \
+ (FREERDP_VERSION_MAJOR == (major) && FREERDP_VERSION_MINOR == (minor) && \
+ FREERDP_VERSION_REVISION >= (revision)))
+
+/**
+ * Constants to workaround FreeRDP issue #5417 (min resolution in AVC mode)
+ * Must be 4 aligned (multiple of 4).
+ * We should instead check destRect and be sure the condition is met
+ * See https://gitlab.com/Remmina/Remmina/-/issues/2507 for a deep discussion
+ */
+#define AVC_MIN_DESKTOP_WIDTH 644
+#define AVC_MIN_DESKTOP_HEIGHT 480
+
+typedef struct rf_context rfContext;
+
+#define GET_PLUGIN_DATA(gp) (rfContext *)g_object_get_data(G_OBJECT(gp), "plugin-data")
+
+/**
+ * Performance Flags, from FreeRDP source
+ *
+ * PERF_FLAG_NONE 0x00000000
+ * PERF_DISABLE_WALLPAPER 0x00000001
+ * PERF_DISABLE_FULLWINDOWDRAG 0x00000002
+ * PERF_DISABLE_MENUANIMATIONS 0x00000004
+ * PERF_DISABLE_THEMING 0x00000008
+ * PERF_DISABLE_CURSOR_SHADOW 0x00000020
+ * PERF_DISABLE_CURSORSETTINGS 0x00000040
+ * PERF_ENABLE_FONT_SMOOTHING 0x00000080
+ * PERF_ENABLE_DESKTOP_COMPOSITION 0x00000100
+ */
+/* Poor (default) - all disabled */
+#define DEFAULT_QUALITY_0 0x6f
+/* Medium - only THEMING, CURSOR_SHADOW and CURSORSETTINGS enabled, all other disabled */
+#define DEFAULT_QUALITY_1 0x07
+/* Good - WALLPAPER, FONT_SMOOTHING, DESKTOP_COMPOSITION disabled, all other enabled */
+#define DEFAULT_QUALITY_2 0x01
+/* Best - DESKTOP_COMPOSITION disabled, all other enabled */
+#define DEFAULT_QUALITY_9 0x80
+
+extern RemminaPluginService *remmina_plugin_service;
+
+#define REMMINA_PLUGIN_INFO(fmt, ...) \
+ remmina_plugin_service->_remmina_info(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_MESSAGE(fmt, ...) \
+ remmina_plugin_service->_remmina_message(__func, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) \
+ remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_WARNING(fmt, ...) \
+ remmina_plugin_service->_remmina_warning(__func__, fmt, ##__VA_ARGS__)
+
+/* This will intentionally crash Remmina */
+#define REMMINA_PLUGIN_ERROR(fmt, ...) \
+ remmina_plugin_service->_remmina_error(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_CRITICAL(fmt, ...) \
+ remmina_plugin_service->_remmina_critical(__func__, fmt, ##__VA_ARGS__)
+#define REMMINA_PLUGIN_AUDIT(fmt, ...) \
+ remmina_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__)
+
+struct rf_clipboard {
+ rfContext * rfi;
+ CliprdrClientContext * context;
+ wClipboard * system;
+ int requestedFormatId;
+
+ UINT32 format;
+ gulong clipboard_handler;
+
+ pthread_mutex_t transfer_clip_mutex;
+ pthread_cond_t transfer_clip_cond;
+ enum { SCDW_NONE, SCDW_BUSY_WAIT, SCDW_ABORTING } srv_clip_data_wait;
+ gpointer srv_data;
+ pthread_mutex_t srv_data_mutex;
+
+ UINT32 server_html_format_id;
+
+ /* Stats for clipboard download */
+ struct timeval clientformatdatarequest_tv;
+};
+typedef struct rf_clipboard rfClipboard;
+
+
+struct rf_pointer {
+ rdpPointer pointer;
+ GdkCursor * cursor;
+};
+typedef struct rf_pointer rfPointer;
+
+#ifdef RF_BITMAP
+struct rf_bitmap {
+ rdpBitmap bitmap;
+ Pixmap pixmap;
+ cairo_surface_t * surface;
+};
+typedef struct rf_bitmap rfBitmap;
+#endif
+
+#ifdef RF_GLYPH
+struct rf_glyph {
+ rdpGlyph glyph;
+ Pixmap pixmap;
+};
+typedef struct rf_glyph rfGlyph;
+#endif
+
+typedef enum {
+ REMMINA_RDP_EVENT_TYPE_SCANCODE,
+ REMMINA_RDP_EVENT_TYPE_SCANCODE_UNICODE,
+ REMMINA_RDP_EVENT_TYPE_MOUSE,
+ REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_LIST,
+ REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_RESPONSE,
+ REMMINA_RDP_EVENT_TYPE_CLIPBOARD_SEND_CLIENT_FORMAT_DATA_REQUEST,
+ REMMINA_RDP_EVENT_TYPE_SEND_MONITOR_LAYOUT,
+ REMMINA_RDP_EVENT_DISCONNECT
+} RemminaPluginRdpEventType;
+
+struct remmina_plugin_rdp_event {
+ RemminaPluginRdpEventType type;
+ union {
+ struct {
+ BOOL up;
+ BOOL extended;
+ UINT8 key_code;
+ UINT32 unicode_code;
+ BOOL extended1;
+ } key_event;
+ struct {
+ UINT16 flags;
+ UINT16 x;
+ UINT16 y;
+ BOOL extended;
+ } mouse_event;
+ struct {
+ CLIPRDR_FORMAT_LIST *pFormatList;
+ } clipboard_formatlist;
+ struct {
+ BYTE * data;
+ int size;
+ } clipboard_formatdataresponse;
+ struct {
+ CLIPRDR_FORMAT_DATA_REQUEST *pFormatDataRequest;
+ } clipboard_formatdatarequest;
+ struct {
+ gint Flags;
+ gint Left;
+ gint Top;
+ gint width;
+ gint height;
+ gint desktopOrientation;
+ gint desktopScaleFactor;
+ gint deviceScaleFactor;
+ gint physicalWidth;
+ gint physicalHeight;
+ } monitor_layout;
+ };
+};
+typedef struct remmina_plugin_rdp_event RemminaPluginRdpEvent;
+
+typedef enum {
+ REMMINA_RDP_UI_UPDATE_REGIONS = 0,
+ REMMINA_RDP_UI_CONNECTED,
+ REMMINA_RDP_UI_RECONNECT_PROGRESS,
+ REMMINA_RDP_UI_CURSOR,
+ REMMINA_RDP_UI_NOCODEC,
+ REMMINA_RDP_UI_CLIPBOARD,
+ REMMINA_RDP_UI_EVENT
+} RemminaPluginRdpUiType;
+
+typedef enum {
+ REMMINA_RDP_UI_CLIPBOARD_FORMATLIST,
+ REMMINA_RDP_UI_CLIPBOARD_GET_DATA,
+ REMMINA_RDP_UI_CLIPBOARD_SET_DATA
+} RemminaPluginRdpUiClipboardType;
+
+typedef enum {
+ REMMINA_RDP_POINTER_NEW,
+ REMMINA_RDP_POINTER_FREE,
+ REMMINA_RDP_POINTER_SET,
+ REMMINA_RDP_POINTER_NULL,
+ REMMINA_RDP_POINTER_DEFAULT,
+ REMMINA_RDP_POINTER_SETPOS
+} RemminaPluginRdpUiPointerType;
+
+typedef enum {
+ REMMINA_RDP_UI_EVENT_UPDATE_SCALE,
+ REMMINA_RDP_UI_EVENT_DESTROY_CAIRO_SURFACE
+} RemminaPluginRdpUiEeventType;
+
+typedef struct {
+ gint x, y, w, h;
+} region;
+
+struct remmina_plugin_rdp_ui_object {
+ RemminaPluginRdpUiType type;
+ gboolean sync;
+ gboolean complete;
+ pthread_mutex_t sync_wait_mutex;
+ pthread_cond_t sync_wait_cond;
+ union {
+ struct {
+ region *ureg;
+ gint ninvalid;
+ } reg;
+ struct {
+ rdpContext * context;
+ rfPointer * pointer;
+ RemminaPluginRdpUiPointerType type;
+ } cursor;
+ struct {
+ gint left;
+ gint top;
+ RFX_MESSAGE * message;
+ } rfx;
+ struct {
+ gint left;
+ gint top;
+ gint width;
+ gint height;
+ UINT8 * bitmap;
+ } nocodec;
+ struct {
+ RemminaPluginRdpUiClipboardType type;
+ GtkTargetList * targetlist;
+ UINT32 format;
+ rfClipboard * clipboard;
+ gpointer data;
+ } clipboard;
+ struct {
+ RemminaPluginRdpUiEeventType type;
+ } event;
+ struct {
+ gint x;
+ gint y;
+ } pos;
+ };
+ /* We can also return values here, usually integers*/
+ int retval;
+ /* Some functions also may return a pointer. */
+ void * retptr;
+};
+
+typedef struct remmina_plugin_rdp_keymap_entry {
+ unsigned orig_keycode;
+ unsigned translated_keycode;
+} RemminaPluginRdpKeymapEntry;
+
+struct rf_context {
+ rdpClientContext clientContext;
+
+ RemminaProtocolWidget *protocol_widget;
+
+ /* main */
+ pthread_t remmina_plugin_thread;
+ RemminaScaleMode scale;
+ gboolean user_cancelled;
+ gboolean thread_cancelled;
+
+ CliprdrClientContext * cliprdr;
+ DispClientContext * dispcontext;
+
+ RDP_PLUGIN_DATA rdpdr_data[5];
+ RDP_PLUGIN_DATA drdynvc_data[5];
+ gchar rdpsnd_options[20];
+
+ gboolean rdpgfxchan;
+
+ gboolean connected;
+ gboolean is_reconnecting;
+ gboolean stop_reconnecting_requested;
+ /* orphaned: rf_context has still one or more libfreerdp thread active,
+ * but no longer maintained by an open RemminaProtocolWidget/tab.
+ * When the orphaned thread terminates, we must cleanup rf_context.
+ */
+ gboolean orphaned;
+ int reconnect_maxattempts;
+ int reconnect_nattempt;
+
+ gboolean sw_gdi;
+ GtkWidget * drawing_area;
+ gint scale_width;
+ gint scale_height;
+ gdouble scale_x;
+ gdouble scale_y;
+ guint delayed_monitor_layout_handler;
+ gboolean use_client_keymap;
+
+ gint srcBpp;
+ GdkDisplay * display;
+ GdkVisual * visual;
+ cairo_surface_t * surface;
+ cairo_format_t cairo_format;
+ gint bpp;
+ gint scanline_pad;
+ gint * colormap;
+
+ guint object_id_seq;
+ GHashTable * object_table;
+
+ GAsyncQueue * ui_queue;
+ pthread_mutex_t ui_queue_mutex;
+ guint ui_handler;
+
+ GArray * pressed_keys;
+ GAsyncQueue * event_queue;
+ gint event_pipe[2];
+ HANDLE event_handle;
+ UINT16 last_x;
+ UINT16 last_y;
+
+ rfClipboard clipboard;
+
+ GArray * keymap; /* Array of RemminaPluginRdpKeymapEntry */
+
+ gboolean attempt_interactive_authentication;
+
+ enum { REMMINA_POSTCONNECT_ERROR_OK = 0, REMMINA_POSTCONNECT_ERROR_GDI_INIT = 1, REMMINA_POSTCONNECT_ERROR_NO_H264 } postconnect_error;
+};
+
+typedef struct remmina_plugin_rdp_ui_object RemminaPluginRdpUiObject;
+
+void rf_init(RemminaProtocolWidget *gp);
+void rf_uninit(RemminaProtocolWidget *gp);
+void rf_get_fds(RemminaProtocolWidget *gp, void **rfds, int *rcount);
+BOOL rf_check_fds(RemminaProtocolWidget *gp);
+void rf_object_free(RemminaProtocolWidget *gp, RemminaPluginRdpUiObject *obj);
+
+void remmina_rdp_event_event_push(RemminaProtocolWidget *gp, const RemminaPluginRdpEvent *e);
diff --git a/plugins/rdp/rdp_settings.c b/plugins/rdp/rdp_settings.c
new file mode 100644
index 0000000..7a3d4aa
--- /dev/null
+++ b/plugins/rdp/rdp_settings.c
@@ -0,0 +1,759 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "rdp_settings.h"
+#include <freerdp/locale/keyboard.h>
+
+static guint keyboard_layout = 0;
+static guint rdp_keyboard_layout = 0;
+static gchar *rdp_keyboard_remapping_list = NULL;
+
+static void remmina_rdp_settings_kbd_init(void)
+{
+ TRACE_CALL(__func__);
+#if FREERDP_CHECK_VERSION(2, 3, 0)
+ rdp_keyboard_remapping_list = g_strdup(
+ remmina_plugin_service->pref_get_value("rdp_kbd_remap"));
+ REMMINA_PLUGIN_DEBUG("rdp_keyboard_remapping_list: %s", rdp_keyboard_remapping_list);
+ keyboard_layout = freerdp_keyboard_init_ex(rdp_keyboard_layout, rdp_keyboard_remapping_list);
+#else
+ keyboard_layout = freerdp_keyboard_init(rdp_keyboard_layout);
+#endif
+}
+
+void remmina_rdp_settings_init(void)
+{
+ TRACE_CALL(__func__);
+ gchar *value;
+
+ value = remmina_plugin_service->pref_get_value("rdp_keyboard_layout");
+
+ if (value && value[0])
+ rdp_keyboard_layout = strtoul(value, NULL, 16);
+
+ g_free(value);
+
+ remmina_rdp_settings_kbd_init();
+}
+
+guint remmina_rdp_settings_get_keyboard_layout(void)
+{
+ TRACE_CALL(__func__);
+ return keyboard_layout;
+}
+
+#define REMMINA_TYPE_PLUGIN_RDPSET_GRID (remmina_rdp_settings_grid_get_type())
+#define REMMINA_RDPSET_GRID(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_PLUGIN_RDPSET_GRID, RemminaPluginRdpsetGrid))
+#define REMMINA_RDPSET_GRID_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_PLUGIN_RDPSET_GRID, RemminaPluginRdpsetGridClass))
+#define REMMINA_IS_PLUGIN_RDPSET_GRID(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_PLUGIN_RDPSET_GRID))
+#define REMMINA_IS_PLUGIN_RDPSET_GRID_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_PLUGIN_RDPSET_GRID))
+#define REMMINA_RDPSET_GRID_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_PLUGIN_RDPSET_GRID, RemminaPluginRdpsetGridClass))
+
+typedef struct _RemminaPluginRdpsetGrid {
+ GtkGrid grid;
+
+ GtkWidget * keyboard_layout_label;
+ GtkWidget * keyboard_layout_combo;
+ GtkListStore * keyboard_layout_store;
+
+ GtkWidget * quality_combo;
+ GtkListStore * quality_store;
+ GtkWidget * wallpaper_check;
+ GtkWidget * windowdrag_check;
+ GtkWidget * menuanimation_check;
+ GtkWidget * theme_check;
+ GtkWidget * cursorshadow_check;
+ GtkWidget * cursorblinking_check;
+ GtkWidget * fontsmoothing_check;
+ GtkWidget * composition_check;
+ GtkWidget * use_client_keymap_check;
+ GtkWidget * disable_smooth_scrolling_check;
+ GtkWidget * reconnect_attempts;
+ GtkWidget * kbd_remap;
+
+ /* FreeRDP /scale-desktop: Scaling of desktop app */
+ GtkWidget * desktop_scale_factor_spin;
+ /* FreeRDP /scale-device: Scaling of appstore app */
+ GtkListStore * device_scale_factor_store;
+ GtkWidget * device_scale_factor_combo;
+ /* FreeRDP /orientation: Orientation of display */
+ GtkListStore * desktop_orientation_store;
+ GtkWidget * desktop_orientation_combo;
+
+ guint quality_values[10];
+} RemminaPluginRdpsetGrid;
+
+typedef struct _RemminaPluginRdpsetGridClass {
+ GtkGridClass parent_class;
+} RemminaPluginRdpsetGridClass;
+
+GType remmina_rdp_settings_grid_get_type(void) G_GNUC_CONST;
+
+G_DEFINE_TYPE(RemminaPluginRdpsetGrid, remmina_rdp_settings_grid, GTK_TYPE_GRID)
+
+static void remmina_rdp_settings_grid_class_init(RemminaPluginRdpsetGridClass *klass)
+{
+ TRACE_CALL(__func__);
+}
+
+static void remmina_rdp_settings_grid_destroy(GtkWidget *widget, gpointer data)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+ guint new_layout;
+ GtkTreeIter iter;
+ RemminaPluginRdpsetGrid *grid;
+ gint val;
+
+ grid = REMMINA_RDPSET_GRID(widget);
+
+ if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->keyboard_layout_combo), &iter)) {
+ gtk_tree_model_get(GTK_TREE_MODEL(grid->keyboard_layout_store), &iter, 0, &new_layout, -1);
+
+ if (new_layout != rdp_keyboard_layout) {
+ rdp_keyboard_layout = new_layout;
+ s = g_strdup_printf("%X", new_layout);
+ remmina_plugin_service->pref_set_value("rdp_keyboard_layout", s);
+ g_free(s);
+
+ remmina_rdp_settings_kbd_init();
+ }
+ }
+
+ remmina_plugin_service->pref_set_value("rdp_use_client_keymap",
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->use_client_keymap_check)) ? "1" : "0");
+
+ remmina_plugin_service->pref_set_value("rdp_disable_smooth_scrolling",
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->disable_smooth_scrolling_check)) ? "1" : "0");
+
+ remmina_plugin_service->pref_set_value("rdp_reconnect_attempts",
+ gtk_entry_get_text(GTK_ENTRY(grid->reconnect_attempts)));
+
+ remmina_plugin_service->pref_set_value("rdp_kbd_remap",
+ gtk_entry_get_text(GTK_ENTRY(grid->kbd_remap)));
+
+ s = g_strdup_printf("%X", grid->quality_values[0]);
+ remmina_plugin_service->pref_set_value("rdp_quality_0", s);
+ g_free(s);
+
+ s = g_strdup_printf("%X", grid->quality_values[1]);
+ remmina_plugin_service->pref_set_value("rdp_quality_1", s);
+ g_free(s);
+
+ s = g_strdup_printf("%X", grid->quality_values[2]);
+ remmina_plugin_service->pref_set_value("rdp_quality_2", s);
+ g_free(s);
+
+ s = g_strdup_printf("%X", grid->quality_values[9]);
+ remmina_plugin_service->pref_set_value("rdp_quality_9", s);
+ g_free(s);
+
+ if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->device_scale_factor_combo), &iter))
+ gtk_tree_model_get(GTK_TREE_MODEL(grid->device_scale_factor_store), &iter, 0, &val, -1);
+ else
+ val = 0;
+ s = g_strdup_printf("%d", val);
+ remmina_plugin_service->pref_set_value("rdp_deviceScaleFactor", s);
+ g_free(s);
+
+ val = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin));
+ s = g_strdup_printf("%d", val);
+ remmina_plugin_service->pref_set_value("rdp_desktopScaleFactor", s);
+ g_free(s);
+
+ if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->desktop_orientation_combo), &iter))
+ gtk_tree_model_get(GTK_TREE_MODEL(grid->desktop_orientation_store), &iter, 0, &val, -1);
+ else
+ val = 0;
+ s = g_strdup_printf("%d", val);
+ remmina_plugin_service->pref_set_value("rdp_desktopOrientation", s);
+ g_free(s);
+}
+
+static void remmina_rdp_settings_grid_load_layout(RemminaPluginRdpsetGrid *grid)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+ GtkTreeIter iter;
+ RDP_KEYBOARD_LAYOUT *layouts;
+
+ gtk_list_store_append(grid->keyboard_layout_store, &iter);
+ gtk_list_store_set(grid->keyboard_layout_store, &iter, 0, 0, 1, _("<Auto-detect>"), -1);
+
+ if (rdp_keyboard_layout == 0)
+ gtk_combo_box_set_active(GTK_COMBO_BOX(grid->keyboard_layout_combo), 0);
+
+ gtk_label_set_text(GTK_LABEL(grid->keyboard_layout_label), "-");
+
+#if FREERDP_VERSION_MAJOR >= 3
+ size_t layout_count = 0;
+ layouts = freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_STANDARD | RDP_KEYBOARD_LAYOUT_TYPE_VARIANT, &layout_count);
+
+ for (DWORD i = 0; i < layout_count; i++) {
+ s = g_strdup_printf("%08X - %s", layouts[i].code, layouts[i].name);
+ gtk_list_store_append(grid->keyboard_layout_store, &iter);
+ gtk_list_store_set(grid->keyboard_layout_store, &iter, 0, layouts[i].code, 1, s, -1);
+
+ if (rdp_keyboard_layout == layouts[i].code)
+ gtk_combo_box_set_active(GTK_COMBO_BOX(grid->keyboard_layout_combo), i + 1);
+
+ if (keyboard_layout == layouts[i].code)
+ gtk_label_set_text(GTK_LABEL(grid->keyboard_layout_label), s);
+
+ g_free(s);
+ }
+
+ freerdp_keyboard_layouts_free(layouts, layout_count);
+#else
+ layouts = freerdp_keyboard_get_layouts(RDP_KEYBOARD_LAYOUT_TYPE_STANDARD | RDP_KEYBOARD_LAYOUT_TYPE_VARIANT);
+
+ for (DWORD i = 0; layouts[i].code; i++) {
+ s = g_strdup_printf("%08X - %s", layouts[i].code, layouts[i].name);
+ gtk_list_store_append(grid->keyboard_layout_store, &iter);
+ gtk_list_store_set(grid->keyboard_layout_store, &iter, 0, layouts[i].code, 1, s, -1);
+
+ if (rdp_keyboard_layout == layouts[i].code)
+ gtk_combo_box_set_active(GTK_COMBO_BOX(grid->keyboard_layout_combo), i + 1);
+
+ if (keyboard_layout == layouts[i].code)
+ gtk_label_set_text(GTK_LABEL(grid->keyboard_layout_label), s);
+
+ g_free(s);
+ }
+
+ freerdp_keyboard_layouts_free(layouts);
+#endif
+}
+
+static void remmina_rdp_settings_grid_load_devicescalefactor_combo(RemminaPluginRdpsetGrid *grid)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+
+ gtk_list_store_append(grid->device_scale_factor_store, &iter);
+ gtk_list_store_set(grid->device_scale_factor_store, &iter, 0, 0, 1, _("<Not set>"), -1);
+ gtk_list_store_append(grid->device_scale_factor_store, &iter);
+ gtk_list_store_set(grid->device_scale_factor_store, &iter, 0, 100, 1, "100%", -1);
+ gtk_list_store_append(grid->device_scale_factor_store, &iter);
+ gtk_list_store_set(grid->device_scale_factor_store, &iter, 0, 140, 1, "140%", -1);
+ gtk_list_store_append(grid->device_scale_factor_store, &iter);
+ gtk_list_store_set(grid->device_scale_factor_store, &iter, 0, 180, 1, "180%", -1);
+}
+
+static void remmina_rdp_settings_grid_load_desktoporientation_combo(RemminaPluginRdpsetGrid *grid)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+
+ gtk_list_store_append(grid->desktop_orientation_store, &iter);
+ gtk_list_store_set(grid->desktop_orientation_store, &iter, 0, 0, 1, "0°", -1);
+ gtk_list_store_append(grid->desktop_orientation_store, &iter);
+ gtk_list_store_set(grid->desktop_orientation_store, &iter, 0, 90, 1, "90°", -1);
+ gtk_list_store_append(grid->desktop_orientation_store, &iter);
+ gtk_list_store_set(grid->desktop_orientation_store, &iter, 0, 180, 1, "180°", -1);
+ gtk_list_store_append(grid->desktop_orientation_store, &iter);
+ gtk_list_store_set(grid->desktop_orientation_store, &iter, 0, 270, 1, "270°", -1);
+}
+
+static void remmina_rdp_settings_grid_load_quality(RemminaPluginRdpsetGrid *grid)
+{
+ TRACE_CALL(__func__);
+ gchar *value;
+ GtkTreeIter iter;
+
+ gtk_list_store_append(grid->quality_store, &iter);
+ gtk_list_store_set(grid->quality_store, &iter, 0, -1, 1, _("<Choose a quality level to edit…>"), -1);
+ gtk_list_store_append(grid->quality_store, &iter);
+ gtk_list_store_set(grid->quality_store, &iter, 0, 0, 1, _("Poor (fastest)"), -1);
+ gtk_list_store_append(grid->quality_store, &iter);
+ gtk_list_store_set(grid->quality_store, &iter, 0, 1, 1, _("Medium"), -1);
+ gtk_list_store_append(grid->quality_store, &iter);
+ gtk_list_store_set(grid->quality_store, &iter, 0, 2, 1, _("Good"), -1);
+ gtk_list_store_append(grid->quality_store, &iter);
+ gtk_list_store_set(grid->quality_store, &iter, 0, 9, 1, _("Best (slowest)"), -1);
+
+ memset(grid->quality_values, 0, sizeof(grid->quality_values));
+
+ value = remmina_plugin_service->pref_get_value("rdp_quality_0");
+ grid->quality_values[0] = (value && value[0] ? strtoul(value, NULL, 16) : DEFAULT_QUALITY_0);
+ g_free(value);
+
+ value = remmina_plugin_service->pref_get_value("rdp_quality_1");
+ grid->quality_values[1] = (value && value[0] ? strtoul(value, NULL, 16) : DEFAULT_QUALITY_1);
+ g_free(value);
+
+ value = remmina_plugin_service->pref_get_value("rdp_quality_2");
+ grid->quality_values[2] = (value && value[0] ? strtoul(value, NULL, 16) : DEFAULT_QUALITY_2);
+ g_free(value);
+
+ value = remmina_plugin_service->pref_get_value("rdp_quality_9");
+ grid->quality_values[9] = (value && value[0] ? strtoul(value, NULL, 16) : DEFAULT_QUALITY_9);
+ g_free(value);
+}
+
+static void remmina_rdp_settings_appscale_on_changed(GtkComboBox *widget, RemminaPluginRdpsetGrid *grid)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+ guint i = 0;
+
+ if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->device_scale_factor_combo), &iter))
+ gtk_tree_model_get(GTK_TREE_MODEL(grid->device_scale_factor_store), &iter, 0, &i, -1);
+ if (i == 0) {
+ gtk_widget_set_sensitive(GTK_WIDGET(grid->desktop_scale_factor_spin), FALSE);
+ gtk_spin_button_set_range(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin), 0, 0);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin), 0);
+ } else {
+ gtk_widget_set_sensitive(GTK_WIDGET(grid->desktop_scale_factor_spin), TRUE);
+ gtk_spin_button_set_range(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin), 100, 500);
+ // gtk_spin_button_set_value(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin), i);
+ }
+}
+
+static void remmina_rdp_settings_quality_on_changed(GtkComboBox *widget, RemminaPluginRdpsetGrid *grid)
+{
+ TRACE_CALL(__func__);
+ guint v;
+ guint i = 0;
+ GtkTreeIter iter;
+ gboolean sensitive;
+
+ if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->quality_combo), &iter)) {
+ gtk_tree_model_get(GTK_TREE_MODEL(grid->quality_store), &iter, 0, &i, -1);
+ sensitive = (i != -1);
+
+ if (sensitive)
+ v = grid->quality_values[i];
+ else
+ v = 0x3f; /* All checkboxes disabled */
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->wallpaper_check), (v & 1) == 0);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->windowdrag_check), (v & 2) == 0);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->menuanimation_check), (v & 4) == 0);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->theme_check), (v & 8) == 0);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->cursorshadow_check), (v & 0x20) == 0);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->cursorblinking_check), (v & 0x40) == 0);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->fontsmoothing_check), (v & 0x80) != 0);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grid->composition_check), (v & 0x100) != 0);
+
+
+ gtk_widget_set_sensitive(GTK_WIDGET(grid->wallpaper_check), sensitive);
+ gtk_widget_set_sensitive(GTK_WIDGET(grid->windowdrag_check), sensitive);
+ gtk_widget_set_sensitive(GTK_WIDGET(grid->menuanimation_check), sensitive);
+ gtk_widget_set_sensitive(GTK_WIDGET(grid->theme_check), sensitive);
+ gtk_widget_set_sensitive(GTK_WIDGET(grid->cursorshadow_check), sensitive);
+ gtk_widget_set_sensitive(GTK_WIDGET(grid->cursorblinking_check), sensitive);
+ gtk_widget_set_sensitive(GTK_WIDGET(grid->fontsmoothing_check), sensitive);
+ gtk_widget_set_sensitive(GTK_WIDGET(grid->composition_check), sensitive);
+ }
+}
+
+static void remmina_rdp_settings_quality_option_on_toggled(GtkToggleButton *togglebutton, RemminaPluginRdpsetGrid *grid)
+{
+ TRACE_CALL(__func__);
+ guint v;
+ guint i = 0;
+ GtkTreeIter iter;
+
+ if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(grid->quality_combo), &iter)) {
+ gtk_tree_model_get(GTK_TREE_MODEL(grid->quality_store), &iter, 0, &i, -1);
+ if (i != -1) {
+ v = 0;
+ v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->wallpaper_check)) ? 0 : 1);
+ v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->windowdrag_check)) ? 0 : 2);
+ v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->menuanimation_check)) ? 0 : 4);
+ v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->theme_check)) ? 0 : 8);
+ v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->cursorshadow_check)) ? 0 : 0x20);
+ v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->cursorblinking_check)) ? 0 : 0x40);
+ v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->fontsmoothing_check)) ? 0x80 : 0);
+ v |= (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(grid->composition_check)) ? 0x100 : 0);
+ grid->quality_values[i] = v;
+ }
+ }
+}
+
+static void remmina_rdp_settings_set_combo_active_item(GtkComboBox *combo, int itemval)
+{
+ GtkTreeIter iter;
+ int i;
+ GtkTreeModel *m;
+ gboolean valid;
+
+ m = gtk_combo_box_get_model(combo);
+ if (!m)
+ return;
+
+ valid = gtk_tree_model_get_iter_first(m, &iter);
+ while (valid) {
+ gtk_tree_model_get(m, &iter, 0, &i, -1);
+ if (i == itemval)
+ gtk_combo_box_set_active_iter(combo, &iter);
+ valid = gtk_tree_model_iter_next(m, &iter);
+ }
+}
+
+static void remmina_rdp_settings_grid_init(RemminaPluginRdpsetGrid *grid)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+ GtkWidget *widget;
+ GtkCellRenderer *renderer;
+ int desktopOrientation, desktopScaleFactor, deviceScaleFactor;
+
+ /* Create the grid */
+ g_signal_connect(G_OBJECT(grid), "destroy", G_CALLBACK(remmina_rdp_settings_grid_destroy), NULL);
+ gtk_grid_set_row_homogeneous(GTK_GRID(grid), FALSE);
+ gtk_grid_set_column_homogeneous(GTK_GRID(grid), FALSE);
+ gtk_container_set_border_width(GTK_CONTAINER(grid), 8);
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 4);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 4);
+
+ /* Create the content */
+ widget = gtk_label_new(_("Keyboard layout"));
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_end(GTK_WIDGET(widget), 6);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_top(GTK_WIDGET(widget), 18);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, 0, 1, 1);
+
+ grid->keyboard_layout_store = gtk_list_store_new(2, G_TYPE_UINT, G_TYPE_STRING);
+ widget = gtk_combo_box_new_with_model(GTK_TREE_MODEL(grid->keyboard_layout_store));
+ gtk_widget_show(widget);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 0, 2, 1);
+
+ renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(widget), renderer, TRUE);
+ gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(widget), renderer, "text", 1);
+ grid->keyboard_layout_combo = widget;
+
+ widget = gtk_label_new("-");
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 1, 2, 1);
+ grid->keyboard_layout_label = widget;
+
+ remmina_rdp_settings_grid_load_layout(grid);
+
+ widget = gtk_check_button_new_with_label(_("Use client keyboard mapping"));
+ gtk_widget_show(widget);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 2, 1, 1);
+ grid->use_client_keymap_check = widget;
+
+ s = remmina_plugin_service->pref_get_value("rdp_use_client_keymap");
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),
+ s && s[0] == '1' ? TRUE : FALSE);
+ g_free(s);
+
+ widget = gtk_label_new(_("Keyboard scancode remapping"));
+ gtk_widget_show(widget);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 3, 1, 1);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ widget = gtk_entry_new();
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_END);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_grid_attach(GTK_GRID(grid), widget, 2, 3, 1, 1);
+ gtk_entry_set_width_chars(GTK_ENTRY(widget), 32);
+#if FREERDP_CHECK_VERSION(2, 3, 0)
+ /* This is the default, but we set it to make things crystal clear */
+ gtk_widget_set_sensitive (widget, TRUE);
+ gtk_widget_set_tooltip_text(widget, _("List of key=value,… pairs to remap scancodes. E.g. 0x56=0x29,0x29=0x56"));
+#else
+ gtk_widget_set_sensitive (widget, FALSE);
+ gtk_widget_set_tooltip_text(widget, _("FreeRDP > 2.3.0 is required to map scancodes"));
+#endif
+ s = remmina_plugin_service->pref_get_value("rdp_kbd_remap");
+ if (s && s[0])
+ gtk_entry_set_text(GTK_ENTRY(widget), s);
+ g_free(s);
+ grid->kbd_remap = widget;
+
+ widget = gtk_label_new(_("Quality settings"));
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_end(GTK_WIDGET(widget), 6);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 18);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, 4, 1, 1);
+
+ grid->quality_store = gtk_list_store_new(2, G_TYPE_UINT, G_TYPE_STRING);
+ widget = gtk_combo_box_new_with_model(GTK_TREE_MODEL(grid->quality_store));
+ gtk_widget_show(widget);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 4, 2, 1);
+
+ renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(widget), renderer, TRUE);
+ gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(widget), renderer, "text", 1);
+ g_signal_connect(G_OBJECT(widget), "changed",
+ G_CALLBACK(remmina_rdp_settings_quality_on_changed), grid);
+ grid->quality_combo = widget;
+
+ remmina_rdp_settings_grid_load_quality(grid);
+
+ widget = gtk_check_button_new_with_label(_("Wallpaper"));
+ gtk_widget_show(widget);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 5, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "toggled",
+ G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid);
+ grid->wallpaper_check = widget;
+
+ widget = gtk_check_button_new_with_label(_("Window drag"));
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 2, 5, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "toggled",
+ G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid);
+ grid->windowdrag_check = widget;
+
+ widget = gtk_check_button_new_with_label(_("Menu animation"));
+ gtk_widget_show(widget);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 6, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "toggled",
+ G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid);
+ grid->menuanimation_check = widget;
+
+ widget = gtk_check_button_new_with_label(_("Theme"));
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 2, 6, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "toggled",
+ G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid);
+ grid->theme_check = widget;
+
+ widget = gtk_check_button_new_with_label(_("Cursor shadow"));
+ gtk_widget_show(widget);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 7, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "toggled",
+ G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid);
+ grid->cursorshadow_check = widget;
+
+ widget = gtk_check_button_new_with_label(_("Cursor blinking"));
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 2, 7, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "toggled",
+ G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid);
+ grid->cursorblinking_check = widget;
+
+ widget = gtk_check_button_new_with_label(_("Font smoothing"));
+ gtk_widget_show(widget);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 8, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "toggled",
+ G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid);
+ grid->fontsmoothing_check = widget;
+
+ widget = gtk_check_button_new_with_label(_("Composition"));
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 2, 8, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "toggled",
+ G_CALLBACK(remmina_rdp_settings_quality_option_on_toggled), grid);
+ grid->composition_check = widget;
+
+ gtk_combo_box_set_active(GTK_COMBO_BOX(grid->quality_combo), 0);
+
+
+ widget = gtk_label_new(_("Remote scale factor"));
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_end(GTK_WIDGET(widget), 6);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 18);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, 9, 1, 1);
+
+ grid->device_scale_factor_store = gtk_list_store_new(2, G_TYPE_INT, G_TYPE_STRING);
+ grid->desktop_orientation_store = gtk_list_store_new(2, G_TYPE_INT, G_TYPE_STRING);
+
+ remmina_rdp_settings_get_orientation_scale_prefs(&desktopOrientation, &desktopScaleFactor, &deviceScaleFactor);
+ remmina_rdp_settings_grid_load_devicescalefactor_combo(grid);
+ remmina_rdp_settings_grid_load_desktoporientation_combo(grid);
+
+ widget = gtk_label_new(_("Desktop scale factor %"));
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 9, 1, 1);
+
+ widget = gtk_spin_button_new_with_range(0, 10000, 1);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 2, 9, 1, 1);
+ grid->desktop_scale_factor_spin = widget;
+
+ widget = gtk_label_new(_("Device scale factor %"));
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 10, 1, 1);
+
+ widget = gtk_combo_box_new_with_model(GTK_TREE_MODEL(grid->device_scale_factor_store));
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 2, 10, 1, 1);
+
+ renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(widget), renderer, TRUE);
+ gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(widget), renderer, "text", 1);
+ grid->device_scale_factor_combo = widget;
+
+ remmina_rdp_settings_set_combo_active_item(GTK_COMBO_BOX(grid->device_scale_factor_combo), deviceScaleFactor);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(grid->desktop_scale_factor_spin), (gdouble)desktopScaleFactor);
+
+ g_signal_connect(G_OBJECT(widget), "changed",
+ G_CALLBACK(remmina_rdp_settings_appscale_on_changed), grid);
+ remmina_rdp_settings_appscale_on_changed(GTK_COMBO_BOX(grid->device_scale_factor_combo), grid);
+
+ widget = gtk_label_new(_("Desktop orientation"));
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_end(GTK_WIDGET(widget), 6);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 18);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, 11, 1, 1);
+
+ widget = gtk_combo_box_new_with_model(GTK_TREE_MODEL(grid->desktop_orientation_store));
+ gtk_widget_show(widget);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 11, 2, 1);
+
+ renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(widget), renderer, TRUE);
+ gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(widget), renderer, "text", 1);
+ grid->desktop_orientation_combo = widget;
+
+ remmina_rdp_settings_set_combo_active_item(GTK_COMBO_BOX(grid->desktop_orientation_combo), desktopOrientation);
+
+ widget = gtk_label_new(_("Input device settings"));
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_end(GTK_WIDGET(widget), 6);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 18);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, 12, 1, 1);
+
+ widget = gtk_check_button_new_with_label(_("Disable smooth scrolling"));
+ gtk_widget_show(widget);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 12, 2, 1);
+ grid->disable_smooth_scrolling_check = widget;
+
+ s = remmina_plugin_service->pref_get_value("rdp_disable_smooth_scrolling");
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),
+ s && s[0] == '1' ? TRUE : FALSE);
+ g_free(s);
+
+ widget = gtk_label_new(_("General settings"));
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_end(GTK_WIDGET(widget), 6);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 18);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, 13, 1, 1);
+ widget = gtk_label_new(_("Reconnect attempts number"));
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_start(GTK_WIDGET(widget), 6);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 13, 1, 1);
+ widget = gtk_entry_new();
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_END);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_grid_attach(GTK_GRID(grid), widget, 2, 13, 1, 1);
+ gtk_entry_set_input_purpose(GTK_ENTRY(widget), GTK_INPUT_PURPOSE_NUMBER);
+ gtk_entry_set_input_hints(GTK_ENTRY(widget), GTK_INPUT_HINT_NONE);
+ gtk_widget_set_tooltip_text(widget, _("The maximum number of reconnect attempts upon an RDP disconnect (default: 20)"));
+ s = remmina_plugin_service->pref_get_value("rdp_reconnect_attempts");
+ if (s && s[0])
+ gtk_entry_set_text(GTK_ENTRY(widget), s);
+ g_free(s);
+ grid->reconnect_attempts = widget;
+
+}
+
+GtkWidget *remmina_rdp_settings_new(RemminaPrefPlugin* plugin)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+
+ widget = GTK_WIDGET(g_object_new(REMMINA_TYPE_PLUGIN_RDPSET_GRID, NULL));
+ gtk_widget_show(widget);
+
+ return widget;
+}
+
+void remmina_rdp_settings_get_orientation_scale_prefs(int *desktopOrientation, int *desktopScaleFactor, int *deviceScaleFactor)
+{
+ TRACE_CALL(__func__);
+
+ /* See https://msdn.microsoft.com/en-us/library/cc240510.aspx */
+
+ int orientation, dpsf, desf;
+ gchar *s;
+
+ *desktopOrientation = *desktopScaleFactor = *deviceScaleFactor = 0;
+
+ s = remmina_plugin_service->pref_get_value("rdp_desktopOrientation");
+ orientation = s ? atoi(s) : 0;
+ g_free(s);
+ if (orientation != 90 && orientation != 180 && orientation != 270)
+ orientation = 0;
+ *desktopOrientation = orientation;
+
+ s = remmina_plugin_service->pref_get_value("rdp_desktopScaleFactor");
+ dpsf = s ? atoi(s) : 0;
+ g_free(s);
+ if (dpsf < 100 || dpsf > 500)
+ return;
+
+ s = remmina_plugin_service->pref_get_value("rdp_deviceScaleFactor");
+ desf = s ? atoi(s) : 0;
+ g_free(s);
+ if (desf != 100 && desf != 140 && desf != 180)
+ return;
+
+ *desktopScaleFactor = dpsf;
+ *deviceScaleFactor = desf;
+}
diff --git a/plugins/rdp/rdp_settings.h b/plugins/rdp/rdp_settings.h
new file mode 100644
index 0000000..e3b9bc7
--- /dev/null
+++ b/plugins/rdp/rdp_settings.h
@@ -0,0 +1,49 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#include <glib.h>
+#include "rdp_plugin.h"
+
+G_BEGIN_DECLS
+
+void remmina_rdp_settings_init(void);
+guint remmina_rdp_settings_get_keyboard_layout(void);
+GtkWidget *remmina_rdp_settings_new(RemminaPrefPlugin* plugin);
+
+void remmina_rdp_settings_get_orientation_scale_prefs(int *desktopOrientation, int *desktopScaleFactor, int *deviceScaleFactor);
+
+G_END_DECLS
diff --git a/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-ssh-symbolic.svg b/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-ssh-symbolic.svg
new file mode 100644
index 0000000..6a345cf
--- /dev/null
+++ b/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-ssh-symbolic.svg
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100"
+ height="100"
+ viewBox="0 0 26.458334 26.458332"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.2 2405546, 2018-03-11"
+ sodipodi:docname="remmina-rdp-ssh-symbolic.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="4"
+ inkscape:cx="25.558198"
+ inkscape:cy="61.982189"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1600"
+ inkscape:window-height="879"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-179.27136"
+ originy="-128.69822" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-179.27135,-141.84345)">
+ <g
+ id="g817"
+ transform="matrix(0.93878631,0,0,0.93878631,13.549948,10.22929)">
+ <path
+ style="isolation:isolate;fill:#000000;stroke-width:0.93878627"
+ d="M 49.884766 0.27539062 C 34.886727 0.27539062 22.888672 12.506314 22.888672 27.273438 L 22.888672 32.117188 L 9.7363281 32.117188 L 9.7363281 99.724609 L 31.220703 99.724609 L 90.263672 99.724609 L 90.263672 32.117188 L 76.880859 32.117188 L 76.880859 27.273438 C 76.880859 12.275364 64.651854 0.27539062 49.884766 0.27539062 z M 49.654297 14.582031 C 56.806908 14.582031 62.34375 20.350818 62.34375 27.273438 L 62.34375 32.117188 L 36.732422 32.117188 L 36.732422 27.273438 L 36.962891 27.273438 C 36.962891 20.119868 42.732636 14.582031 49.654297 14.582031 z M 50 39.673828 C 51.038296 39.673828 52.075804 40.069448 52.867188 40.861328 L 56.373047 44.367188 L 59.878906 47.871094 C 61.462631 49.453896 61.462631 52.024619 59.878906 53.607422 L 56.373047 57.113281 L 52.867188 60.619141 C 51.28442 62.201943 48.715615 62.201943 47.132812 60.619141 L 43.625 57.113281 L 40.121094 53.607422 C 38.538291 52.024619 38.538291 49.453896 40.121094 47.871094 L 43.625 44.367188 L 47.132812 40.861328 C 47.924214 40.069448 48.961704 39.673828 50 39.673828 z M 32.560547 55.105469 C 33.593916 55.105469 34.626422 55.499469 35.414062 56.287109 L 38.933594 59.806641 L 42.451172 63.324219 C 44.02741 64.900457 44.02741 67.458918 42.451172 69.035156 L 38.933594 72.552734 L 35.414062 76.072266 C 33.838782 77.647546 31.280359 77.647546 29.705078 76.072266 L 26.185547 72.552734 L 22.666016 69.035156 C 21.090735 67.458918 21.090735 64.900457 22.666016 63.324219 L 26.185547 59.806641 L 29.705078 56.287109 C 30.492718 55.499469 31.527178 55.105469 32.560547 55.105469 z M 67.710938 55.105469 C 68.744306 55.105469 69.776813 55.499469 70.564453 56.287109 L 74.083984 59.806641 L 77.603516 63.324219 C 79.179719 64.900457 79.179719 67.458918 77.603516 69.035156 L 74.083984 72.552734 L 70.564453 76.072266 C 68.989173 77.647546 66.430749 77.647546 64.855469 76.072266 L 61.335938 72.552734 L 57.816406 69.035156 C 56.241126 67.458918 56.241126 64.900457 57.816406 63.324219 L 61.335938 59.806641 L 64.855469 56.287109 C 65.643109 55.499469 66.677569 55.105469 67.710938 55.105469 z M 50.261719 70.865234 C 51.295087 70.865234 52.329547 71.259235 53.117188 72.046875 L 56.636719 75.566406 L 60.154297 79.083984 C 61.729577 80.660223 61.729577 83.218683 60.154297 84.794922 L 56.636719 88.3125 L 53.117188 91.832031 C 51.541907 93.407312 48.983484 93.407312 47.408203 91.832031 L 43.888672 88.3125 L 40.369141 84.794922 C 38.79386 83.218683 38.79386 80.660223 40.369141 79.083984 L 43.888672 75.566406 L 47.408203 72.046875 C 48.195843 71.259235 49.22835 70.865234 50.261719 70.865234 z "
+ transform="matrix(0.28183552,0,0,0.28183552,176.52729,140.19608)"
+ id="path79" />
+ </g>
+ </g>
+</svg>
diff --git a/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-symbolic.svg b/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-symbolic.svg
new file mode 100644
index 0000000..af2b970
--- /dev/null
+++ b/plugins/rdp/scalable/emblems/org.remmina.Remmina-rdp-symbolic.svg
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100"
+ height="100"
+ viewBox="0 0 26.458334 26.458333"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.2 2405546, 2018-03-11"
+ sodipodi:docname="remmina-rdp-symbolic.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.0986883"
+ inkscape:cx="-49.823663"
+ inkscape:cy="43.808883"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1600"
+ inkscape:window-height="879"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="0"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-190.61239"
+ originy="-78.157086" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-190.61238,-192.38458)">
+ <g
+ transform="matrix(0.26458333,0,0,0.26458333,153.78079,124.26165)"
+ style="isolation:isolate"
+ id="RDP Icons">
+ <g
+ id="Group" />
+ <g
+ id="g50">
+ <path
+ style="fill:#000000"
+ d="M 50 0.41992188 C 22.636004 0.41992188 0.41992185 22.636004 0.41992188 50 C 0.41992188 77.363996 22.636004 99.580078 50 99.580078 C 77.363996 99.580078 99.580078 77.363996 99.580078 50 C 99.580078 22.636004 77.363996 0.41992185 50 0.41992188 z M 50.339844 15.697266 C 51.446721 15.697266 52.552988 16.117441 53.396484 16.960938 L 58.603516 22.167969 L 63.808594 27.373047 C 65.496606 29.061022 65.496606 31.800306 63.808594 33.488281 L 58.603516 38.695312 L 53.396484 43.900391 C 51.709492 45.587421 48.971216 45.587421 47.283203 43.900391 L 42.078125 38.695312 L 36.871094 33.488281 C 35.183119 31.800306 35.183119 29.061022 36.871094 27.373047 L 42.078125 22.167969 L 47.283203 16.960938 C 48.127209 16.117441 49.232967 15.697266 50.339844 15.697266 z M 27.388672 34.402344 C 28.495544 34.402344 29.603259 34.824473 30.447266 35.667969 L 35.652344 40.873047 L 40.857422 46.080078 C 42.545397 47.76707 42.545397 50.507303 40.857422 52.195312 L 35.652344 57.400391 L 30.447266 62.607422 C 28.759253 64.294414 26.019061 64.294414 24.332031 62.607422 L 19.126953 57.400391 L 13.919922 52.195312 C 12.231909 50.507303 12.231909 47.76707 13.919922 46.080078 L 19.126953 40.873047 L 24.332031 35.667969 C 25.175546 34.824473 26.281799 34.402344 27.388672 34.402344 z M 72.611328 34.402344 C 73.718201 34.402344 74.824473 34.824473 75.667969 35.667969 L 80.875 40.873047 L 86.080078 46.080078 C 87.767108 47.76707 87.767108 50.507301 86.080078 52.195312 L 80.875 57.400391 L 75.667969 62.607422 C 73.980977 64.294414 71.240709 64.294414 69.552734 62.607422 L 64.347656 57.400391 L 59.140625 52.195312 C 57.453633 50.507301 57.453633 47.76707 59.140625 46.080078 L 64.347656 40.873047 L 69.552734 35.667969 C 70.396722 34.824473 71.504456 34.402344 72.611328 34.402344 z M 50.339844 54.833984 C 51.446721 54.833984 52.552988 55.255603 53.396484 56.099609 L 58.603516 61.304688 L 63.808594 66.511719 C 65.496606 68.198711 65.496606 70.938008 63.808594 72.625 L 58.603516 77.832031 L 53.396484 83.037109 C 51.709492 84.725122 48.971216 84.725122 47.283203 83.037109 L 42.078125 77.832031 L 36.871094 72.625 C 35.183119 70.938008 35.183119 68.198711 36.871094 66.511719 L 42.078125 61.304688 L 47.283203 56.099609 C 48.127209 55.255603 49.232967 54.833984 50.339844 54.833984 z "
+ transform="translate(139.20601,257.47249)"
+ id="path48" />
+ </g>
+ <g
+ id="g62">
+ <g
+ id="g52" />
+ <g
+ id="g54" />
+ <g
+ id="g56" />
+ <g
+ id="g60">
+ <g
+ id="g58" />
+ </g>
+ </g>
+ </g>
+ <g
+ id="g831"
+ transform="translate(0.20117925,-0.3392995)" />
+ </g>
+</svg>
diff --git a/plugins/secret/CMakeLists.txt b/plugins/secret/CMakeLists.txt
new file mode 100644
index 0000000..7de80a9
--- /dev/null
+++ b/plugins/secret/CMakeLists.txt
@@ -0,0 +1,50 @@
+# remmina-plugin-secret - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2011 Marc-Andre Moreau
+# Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. If you
+# do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+
+find_suggested_package(Libsecret)
+if(LIBSECRET_FOUND)
+ set(REMMINA_PLUGIN_SECRET_SRCS src/glibsecret_plugin.c)
+
+ add_library(remmina-plugin-secret MODULE ${REMMINA_PLUGIN_SECRET_SRCS})
+ set_target_properties(remmina-plugin-secret PROPERTIES PREFIX "")
+ set_target_properties(remmina-plugin-secret PROPERTIES NO_SONAME 1)
+
+ include_directories(${GTK3_INCLUDE_DIRS})
+ target_link_libraries(remmina-plugin-secret ${GTK_LIBRARIES})
+
+ include_directories(SYSTEM ${LIBSECRET_INCLUDE_DIRS})
+ target_link_libraries(remmina-plugin-secret ${LIBSECRET_LIBRARIES} ${GLIB_LIBRARY})
+
+ install(TARGETS remmina-plugin-secret DESTINATION ${REMMINA_PLUGINDIR})
+endif()
diff --git a/plugins/secret/src/glibsecret_plugin.c b/plugins/secret/src/glibsecret_plugin.c
new file mode 100644
index 0000000..dc6d8cc
--- /dev/null
+++ b/plugins/secret/src/glibsecret_plugin.c
@@ -0,0 +1,217 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2011 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "config.h"
+#include "glibsecret_plugin.h"
+#include <gtk/gtk.h>
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+#include <libsecret/secret.h>
+#include <remmina/plugin.h>
+
+static RemminaPluginService *remmina_plugin_service = NULL;
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__)
+
+
+static SecretSchema remmina_file_secret_schema =
+{ "org.remmina.Password", SECRET_SCHEMA_NONE,
+ {
+ { "filename", SECRET_SCHEMA_ATTRIBUTE_STRING },
+ { "key", SECRET_SCHEMA_ATTRIBUTE_STRING },
+ { NULL, 0 }
+ } };
+
+
+#ifdef LIBSECRET_VERSION_0_18
+static SecretService* secretservice;
+static SecretCollection* defaultcollection;
+#endif
+
+
+gboolean remmina_plugin_glibsecret_is_service_available(RemminaSecretPlugin* plugin)
+{
+#ifdef LIBSECRET_VERSION_0_18
+ if (secretservice && defaultcollection)
+ return TRUE;
+ else
+ return FALSE;
+#else
+ return FALSE;
+#endif
+}
+
+static void remmina_plugin_glibsecret_unlock_secret_service(RemminaSecretPlugin* plugin)
+{
+ TRACE_CALL(__func__);
+
+#ifdef LIBSECRET_VERSION_0_18
+
+ GError *error = NULL;
+ GList *objects, *ul;
+ gchar* lbl;
+
+ if (secretservice && defaultcollection) {
+ if (secret_collection_get_locked(defaultcollection)) {
+ lbl = secret_collection_get_label(defaultcollection);
+ REMMINA_PLUGIN_DEBUG("Requesting unlock of the default '%s' collection", lbl);
+ objects = g_list_append(NULL, defaultcollection);
+ secret_service_unlock_sync(secretservice, objects, NULL, &ul, &error);
+ g_list_free(objects);
+ g_list_free(ul);
+ }
+ }
+#endif
+ return;
+}
+
+void remmina_plugin_glibsecret_store_password(RemminaSecretPlugin* plugin, RemminaFile *remminafile, const gchar *key, const gchar *password)
+{
+ TRACE_CALL(__func__);
+ GError *r = NULL;
+ const gchar *path;
+ gchar *s;
+
+ path = remmina_plugin_service->file_get_path(remminafile);
+ s = g_strdup_printf("Remmina: %s - %s", remmina_plugin_service->file_get_string(remminafile, "name"), key);
+ secret_password_store_sync(&remmina_file_secret_schema, SECRET_COLLECTION_DEFAULT, s, password,
+ NULL, &r, "filename", path, "key", key, NULL);
+ g_free(s);
+ if (r == NULL) {
+ REMMINA_PLUGIN_DEBUG("Password “%s” saved for file %s\n", key, path);
+ }else {
+ REMMINA_PLUGIN_DEBUG("Password “%s” cannot be saved for file %s\n", key, path);
+ g_error_free(r);
+ }
+}
+
+gchar*
+remmina_plugin_glibsecret_get_password(RemminaSecretPlugin* plugin, RemminaFile *remminafile, const gchar *key)
+{
+ TRACE_CALL(__func__);
+ GError *r = NULL;
+ const gchar *path;
+ gchar *password;
+ gchar *p;
+
+ path = remmina_plugin_service->file_get_path(remminafile);
+ password = secret_password_lookup_sync(&remmina_file_secret_schema, NULL, &r, "filename", path, "key", key, NULL);
+ if (r == NULL) {
+ p = g_strdup(password);
+ secret_password_free(password);
+ return p;
+ }else {
+ REMMINA_PLUGIN_DEBUG("Password cannot be found for file %s\n", path);
+ return NULL;
+ }
+}
+
+void remmina_plugin_glibsecret_delete_password(RemminaSecretPlugin* plugin, RemminaFile *remminafile, const gchar *key)
+{
+ TRACE_CALL(__func__);
+ GError *r = NULL;
+ const gchar *path;
+
+ path = remmina_plugin_service->file_get_path(remminafile);
+ secret_password_clear_sync(&remmina_file_secret_schema, NULL, &r, "filename", path, "key", key, NULL);
+ if (r == NULL)
+ REMMINA_PLUGIN_DEBUG("password “%s” deleted for file %s", key, path);
+ else
+ REMMINA_PLUGIN_DEBUG("password “%s” cannot be deleted for file %s", key, path);
+}
+
+gboolean remmina_plugin_glibsecret_init(RemminaSecretPlugin* plugin)
+{
+#ifdef LIBSECRET_VERSION_0_18
+ GError *error;
+ error = NULL;
+ secretservice = secret_service_get_sync(SECRET_SERVICE_LOAD_COLLECTIONS, NULL, &error);
+ if (error) {
+ g_print("[glibsecret] unable to get secret service: %s\n", error->message);
+ return FALSE;
+ }
+ if (secretservice == NULL) {
+ g_print("[glibsecret] unable to get secret service: Unknown error.\n");
+ return FALSE;
+ }
+
+ defaultcollection = secret_collection_for_alias_sync(secretservice, SECRET_COLLECTION_DEFAULT, SECRET_COLLECTION_NONE, NULL, &error);
+ if (error) {
+ g_print("[glibsecret] unable to get secret service default collection: %s\n", error->message);
+ return FALSE;
+ }
+
+ remmina_plugin_glibsecret_unlock_secret_service(plugin);
+ return TRUE;
+
+#else
+ g_print("Libsecret was too old during compilation, disabling secret service.\n");
+ return FALSE;
+#endif
+}
+
+static RemminaSecretPlugin remmina_plugin_glibsecret =
+{ REMMINA_PLUGIN_TYPE_SECRET,
+ "glibsecret",
+ N_("Secured password storage in the GNOME keyring"),
+ NULL,
+ VERSION,
+ 2000,
+ remmina_plugin_glibsecret_init,
+ remmina_plugin_glibsecret_is_service_available,
+ remmina_plugin_glibsecret_store_password,
+ remmina_plugin_glibsecret_get_password,
+ remmina_plugin_glibsecret_delete_password
+};
+
+G_MODULE_EXPORT gboolean
+remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+
+ /* This function should only register the secret plugin. No init action
+ * should be performed here. Initialization will be done later
+ * with remmina_plugin_xxx_init() . */
+
+ remmina_plugin_service = service;
+
+ if (!service->register_plugin((RemminaPlugin*)&remmina_plugin_glibsecret)) {
+ return FALSE;
+ }
+
+ return TRUE;
+
+}
+
diff --git a/plugins/secret/src/glibsecret_plugin.h b/plugins/secret/src/glibsecret_plugin.h
new file mode 100644
index 0000000..37099b2
--- /dev/null
+++ b/plugins/secret/src/glibsecret_plugin.h
@@ -0,0 +1,40 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2015-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#ifndef __BACKEND_LIBSECRET_H__
+#define __BACKEND_LIBSECRET_H__
+
+#include <libsecret/secret.h>
+
+#endif // __BACKEND_LIBSECRET_H__
diff --git a/plugins/spice/CMakeLists.txt b/plugins/spice/CMakeLists.txt
new file mode 100644
index 0000000..875d53f
--- /dev/null
+++ b/plugins/spice/CMakeLists.txt
@@ -0,0 +1,55 @@
+# Remmina - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2016-2018 Denis Ollier
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. * If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. * If you
+# do not wish to do so, delete this exception statement from your
+# version. * If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+set(REMMINA_PLUGIN_SPICE_SRCS
+ spice_plugin.c
+ spice_plugin_file_transfer.c
+ spice_plugin_usb.c
+ )
+
+add_library(remmina-plugin-spice MODULE ${REMMINA_PLUGIN_SPICE_SRCS})
+set_target_properties(remmina-plugin-spice PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-spice PROPERTIES NO_SONAME 1)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${SPICE_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-spice ${REMMINA_COMMON_LIBRARIES} ${SPICE_LIBRARIES})
+
+install(TARGETS remmina-plugin-spice DESTINATION ${REMMINA_PLUGINDIR})
+
+install(FILES
+ scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg
+ scalable/emblems/org.remmina.Remmina-spice-symbolic.svg
+ DESTINATION ${APPICONSCALE_EMBLEMS_DIR})
+
+if(WITH_ICON_CACHE)
+ gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor")
+endif()
diff --git a/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg b/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg
new file mode 100644
index 0000000..db98dab
--- /dev/null
+++ b/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-ssh-symbolic.svg
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100"
+ height="100"
+ viewBox="0 0 26.458334 26.458333"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.2 2405546, 2018-03-11"
+ sodipodi:docname="remmina-spice-ssh-symbolic.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="4.0000001"
+ inkscape:cx="-13.204075"
+ inkscape:cy="42.75632"
+ inkscape:document-units="px"
+ inkscape:current-layer="g837"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1041"
+ inkscape:window-x="1600"
+ inkscape:window-y="18"
+ inkscape:window-maximized="0"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-223.14604"
+ originy="-88.618291" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-223.14604,-181.92338)">
+ <g
+ id="g837"
+ transform="matrix(0.83396843,0,0,0.83396843,39.920727,35.024416)">
+ <g
+ transform="matrix(1.1087066,0,0,1.1087066,45.9421,19.072628)"
+ id="g817">
+ <path
+ style="isolation:isolate;fill:#000000;stroke-width:0.92462623"
+ d="M 49.886719 1.0253906 C 35.114901 1.0253906 23.296875 13.070848 23.296875 27.615234 L 23.296875 32.388672 L 10.34375 32.388672 L 10.34375 98.974609 L 31.503906 98.974609 L 89.65625 98.974609 L 89.65625 32.388672 L 76.474609 32.388672 L 76.474609 27.615234 C 76.474609 12.843381 64.43107 1.0253906 49.886719 1.0253906 z M 49.658203 15.117188 C 56.702929 15.117188 62.158203 20.797031 62.158203 27.615234 L 62.158203 32.388672 L 37.228516 32.388672 L 37.160156 27.615234 C 37.160156 20.569565 42.840944 15.117187 49.658203 15.117188 z M 65.447266 49.119141 L 81.927734 84.054688 L 69.425781 84.054688 L 59.197266 62.371094 L 65.447266 49.119141 z M 50.216797 49.291016 L 66.697266 84.226562 L 53.324219 84.226562 L 43.53125 63.466797 L 50.216797 49.291016 z M 34.550781 49.304688 L 51.03125 84.240234 L 18.072266 84.240234 L 34.550781 49.304688 z "
+ transform="matrix(0.28615164,0,0,0.28615164,156.72391,141.67128)"
+ id="path79" />
+ </g>
+ <g
+ style="fill:#ffffff"
+ transform="matrix(0.65160764,0,0,0.79754876,80.032738,36.675708)"
+ id="g822" />
+ </g>
+ </g>
+</svg>
diff --git a/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-symbolic.svg b/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-symbolic.svg
new file mode 100644
index 0000000..7019006
--- /dev/null
+++ b/plugins/spice/scalable/emblems/org.remmina.Remmina-spice-symbolic.svg
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100"
+ height="100"
+ viewBox="0 0 26.458334 26.458333"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="spice protocol.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2"
+ inkscape:cx="25.172184"
+ inkscape:cy="61.742456"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1366"
+ inkscape:window-height="715"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-223.14604"
+ originy="-88.618291" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-223.14604,-181.92338)">
+ <g
+ id="g822"
+ transform="matrix(0.85100655,0,0,1.0416072,33.247302,-14.623309)">
+ <path
+ inkscape:connector-curvature="0"
+ id="polygon65"
+ d="m 246.21287,194.41092 -3.04322,5.27151 4.98005,8.62583 h 6.08697 z"
+ style="isolation:isolate;vector-effect:non-scaling-stroke;fill:#000000;stroke:none;stroke-width:0.79374999;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:3" />
+ <path
+ inkscape:connector-curvature="0"
+ id="polygon67"
+ d="m 238.79737,194.47917 -3.25561,5.63893 4.76766,8.25841 h 6.51175 z"
+ style="isolation:isolate;vector-effect:non-scaling-stroke;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.64583325;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none" />
+ <polygon
+ transform="matrix(0.26458333,0,0,0.26458333,187.32648,46.374434)"
+ points="165.707,559.786 196.032,612.311 135.381,612.311 "
+ stroke-miterlimit="3"
+ id="polygon820"
+ style="isolation:isolate;vector-effect:non-scaling-stroke;fill:#000000;stroke:none;stroke-width:10;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;paint-order:fill markers stroke" />
+ </g>
+ </g>
+</svg>
diff --git a/plugins/spice/spice_plugin.c b/plugins/spice/spice_plugin.c
new file mode 100644
index 0000000..fea8a3e
--- /dev/null
+++ b/plugins/spice/spice_plugin.c
@@ -0,0 +1,836 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2018 Denis Ollier
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "spice_plugin.h"
+
+#define XSPICE_DEFAULT_PORT 5900
+
+enum {
+ REMMINA_PLUGIN_SPICE_FEATURE_PREF_VIEWONLY = 1,
+ REMMINA_PLUGIN_SPICE_FEATURE_DYNRESUPDATE,
+ REMMINA_PLUGIN_SPICE_FEATURE_PREF_DISABLECLIPBOARD,
+ REMMINA_PLUGIN_SPICE_FEATURE_TOOL_SENDCTRLALTDEL,
+ REMMINA_PLUGIN_SPICE_FEATURE_TOOL_USBREDIR,
+ REMMINA_PLUGIN_SPICE_FEATURE_SCALE
+};
+
+RemminaPluginService *remmina_plugin_service = NULL;
+
+static void remmina_plugin_spice_channel_new_cb(SpiceSession *, SpiceChannel *, RemminaProtocolWidget *);
+static void remmina_plugin_spice_main_channel_event_cb(SpiceChannel *, SpiceChannelEvent, RemminaProtocolWidget *);
+static void remmina_plugin_spice_agent_connected_event_cb(SpiceChannel *, RemminaProtocolWidget *);
+static void remmina_plugin_spice_display_ready_cb(GObject *, GParamSpec *, RemminaProtocolWidget *);
+static void remmina_plugin_spice_update_scale_mode(RemminaProtocolWidget *);
+static void remmina_plugin_spice_session_open_fd(RemminaProtocolWidget *);
+static void remmina_plugin_spice_channel_open_fd_cb(SpiceChannel *channel, gint tls G_GNUC_UNUSED, RemminaProtocolWidget *);
+
+void remmina_plugin_spice_select_usb_devices(RemminaProtocolWidget *);
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+void remmina_plugin_spice_file_transfer_new_cb(SpiceMainChannel *, SpiceFileTransferTask *, RemminaProtocolWidget *);
+# endif /* SPICE_GTK_CHECK_VERSION(0, 31, 0) */
+#endif /* SPICE_GTK_CHECK_VERSION */
+
+gchar* str_replace(const gchar *string, const gchar *search, const gchar *replacement)
+{
+ TRACE_CALL(__func__);
+ gchar *str, **arr;
+
+ g_return_val_if_fail(string != NULL, NULL);
+ g_return_val_if_fail(search != NULL, NULL);
+
+ if (replacement == NULL)
+ replacement = "";
+
+ arr = g_strsplit(string, search, -1);
+ if (arr != NULL && arr[0] != NULL)
+ str = g_strjoinv(replacement, arr);
+ else
+ str = g_strdup(string);
+
+ g_strfreev(arr);
+ return str;
+}
+
+static void
+remmina_plugin_spice_session_open_fd(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ gint fd = remmina_plugin_service->open_unix_sock(gpdata->unixPath);
+ REMMINA_PLUGIN_DEBUG("Opening spice session with FD: %d -> %s", fd, gpdata->unixPath);
+ spice_session_open_fd(gpdata->session, fd);
+}
+
+static void
+remmina_plugin_spice_channel_open_fd_cb(SpiceChannel *channel, gint tls, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ gint id, type;
+ g_object_get(channel, "channel-id", &id, "channel-type", &type, NULL);
+ gint fd = remmina_plugin_service->open_unix_sock(gpdata->unixPath);
+ REMMINA_PLUGIN_DEBUG ("Opening channel %p %s %d with FD: %d -> %s", channel, g_type_name(G_OBJECT_TYPE(channel)), id, fd, gpdata->unixPath);
+ spice_channel_open_fd(channel, fd);
+}
+
+static void remmina_plugin_spice_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginSpiceData *gpdata;
+ RemminaFile *remminafile;
+
+ gpdata = g_new0(RemminaPluginSpiceData, 1);
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ gpdata->session = spice_session_new();
+ g_signal_connect(gpdata->session,
+ "channel-new",
+ G_CALLBACK(remmina_plugin_spice_channel_new_cb),
+ gp);
+
+ g_object_set(gpdata->session,
+ "password", g_strdup(remmina_plugin_service->file_get_string(remminafile, "password")),
+ "read-only", remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE),
+ "enable-audio", remmina_plugin_service->file_get_int(remminafile, "enableaudio", FALSE),
+ "enable-smartcard", remmina_plugin_service->file_get_int(remminafile, "sharesmartcard", FALSE),
+ "shared-dir", remmina_plugin_service->file_get_string(remminafile, "sharefolder"),
+ NULL);
+
+ gpdata->gtk_session = spice_gtk_session_get(gpdata->session);
+ g_object_set(gpdata->gtk_session,
+ "auto-clipboard",
+ !remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE),
+ NULL);
+}
+
+static gboolean remmina_plugin_spice_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gint port;
+ const gchar *cacert;
+ gchar *host, *tunnel;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ /* Setup SSH tunnel if needed */
+ tunnel = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, XSPICE_DEFAULT_PORT, FALSE);
+
+ if (!tunnel) {
+ return FALSE;
+ }
+
+ /**-START- UNIX socket */
+ if(strstr(g_strdup(tunnel), "unix:///") != NULL) {
+ REMMINA_PLUGIN_DEBUG("Tunnel contain unix:// -> %s", tunnel);
+ gchar *val = str_replace(tunnel, "unix://", "");
+ REMMINA_PLUGIN_DEBUG("tunnel after cleaning = %s", val);
+ g_object_set(gpdata->session, "unix-path", val, NULL);
+ gpdata->isUnix = TRUE;
+ gint fd = remmina_plugin_service->open_unix_sock(val);
+ REMMINA_PLUGIN_DEBUG("Unix socket fd: %d", fd);
+ gpdata->unixPath = g_strdup(val);
+ if (fd > 0)
+ remmina_plugin_spice_session_open_fd(gp);
+ g_free(val);
+ } else {
+
+ remmina_plugin_service->get_server_port(tunnel,
+ XSPICE_DEFAULT_PORT,
+ &host,
+ &port);
+
+ g_object_set(gpdata->session, "host", host, NULL);
+ gpdata->isUnix = FALSE;
+ g_free(host);
+ g_free(tunnel);
+
+ /* Unencrypted connection */
+ if (!remmina_plugin_service->file_get_int(remminafile, "usetls", FALSE)) {
+ g_object_set(gpdata->session, "port", g_strdup_printf("%i", port), NULL);
+ }
+ /* TLS encrypted connection */
+ else{
+ g_object_set(gpdata->session, "tls_port", g_strdup_printf("%i", port), NULL);
+
+ /* Server CA certificate */
+ cacert = remmina_plugin_service->file_get_string(remminafile, "cacert");
+ if (cacert) {
+ g_object_set(gpdata->session, "ca-file", cacert, NULL);
+ }
+ }
+
+ spice_session_connect(gpdata->session);
+ }
+ /** -END- UNIX socket */
+
+ /*
+ * FIXME: Add a waiting loop until the g_signal "channel-event" occurs.
+ * If the event is SPICE_CHANNEL_OPENED, TRUE should be returned,
+ * otherwise FALSE should be returned.
+ */
+ return TRUE;
+}
+
+static gboolean remmina_plugin_spice_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (gpdata->main_channel) {
+ g_signal_handlers_disconnect_by_func(gpdata->main_channel,
+ G_CALLBACK(remmina_plugin_spice_main_channel_event_cb),
+ gp);
+ g_signal_handlers_disconnect_by_func(gpdata->main_channel,
+ G_CALLBACK(remmina_plugin_spice_agent_connected_event_cb),
+ gp);
+ }
+
+ if (gpdata->session) {
+ spice_session_disconnect(gpdata->session);
+ g_object_unref(gpdata->session);
+ gpdata->session = NULL;
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ }
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ if (gpdata->file_transfers) {
+ g_hash_table_unref(gpdata->file_transfers);
+ }
+# endif /* SPICE_GTK_CHECK_VERSION(0, 31, 0) */
+#endif /* SPICE_GTK_CHECK_VERSION */
+
+ return FALSE;
+}
+
+static gboolean remmina_plugin_spice_disable_gst_overlay(SpiceChannel *channel, void* pipeline_ptr, RemminaProtocolWidget *gp)
+{
+ g_signal_stop_emission_by_name(channel, "gst-video-overlay");
+ return FALSE;
+}
+
+static void remmina_plugin_spice_channel_new_cb(SpiceSession *session, SpiceChannel *channel, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gint id, type;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ g_return_if_fail(gpdata != NULL);
+
+ if(gpdata->isUnix) {
+ g_signal_connect(channel, "open-fd", G_CALLBACK(remmina_plugin_spice_channel_open_fd_cb), gp);
+ }
+
+ g_object_get(channel, "channel-id", &id, "channel-type", &type, NULL);
+ REMMINA_PLUGIN_DEBUG ("New spice channel %p %s %d", channel, g_type_name(G_OBJECT_TYPE(channel)), id);
+
+ if (SPICE_IS_MAIN_CHANNEL(channel)) {
+ gpdata->main_channel = SPICE_MAIN_CHANNEL(channel);
+ g_signal_connect(channel,
+ "channel-event",
+ G_CALLBACK(remmina_plugin_spice_main_channel_event_cb),
+ gp);
+ g_signal_connect(channel,
+ "main-agent-update",
+ G_CALLBACK(remmina_plugin_spice_agent_connected_event_cb),
+ gp);
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ g_signal_connect(channel,
+ "new-file-transfer",
+ G_CALLBACK(remmina_plugin_spice_file_transfer_new_cb),
+ gp);
+# endif /* SPICE_GTK_CHECK_VERSION(0, 31, 0) */
+#endif /* SPICE_GTK_CHECK_VERSION */
+ }
+
+ if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
+ gpdata->display_channel = SPICE_DISPLAY_CHANNEL(channel);
+ gpdata->display = spice_display_new(gpdata->session, id);
+ g_signal_connect(gpdata->display,
+ "notify::ready",
+ G_CALLBACK(remmina_plugin_spice_display_ready_cb),
+ gp);
+ remmina_plugin_spice_display_ready_cb(G_OBJECT(gpdata->display), NULL, gp);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "disablegstvideooverlay", FALSE)) {
+ g_signal_connect(channel,
+ "gst-video-overlay",
+ G_CALLBACK(remmina_plugin_spice_disable_gst_overlay),
+ gp);
+ }
+
+ }
+
+ if (SPICE_IS_INPUTS_CHANNEL(channel)) {
+ REMMINA_PLUGIN_DEBUG("New inputs channel");
+ }
+ if (SPICE_IS_PLAYBACK_CHANNEL(channel)) {
+ REMMINA_PLUGIN_DEBUG("New audio channel");
+ if (remmina_plugin_service->file_get_int(remminafile, "enableaudio", FALSE)) {
+ gpdata->audio = spice_audio_get(gpdata->session, NULL);
+ }
+ }
+
+ if (SPICE_IS_USBREDIR_CHANNEL(channel)) {
+ REMMINA_PLUGIN_DEBUG("New usbredir channel");
+ }
+
+ if (SPICE_IS_WEBDAV_CHANNEL(channel)) {
+ REMMINA_PLUGIN_DEBUG("New webdav channel");
+ if (remmina_plugin_service->file_get_string(remminafile, "sharefolder")) {
+ spice_channel_connect(channel);
+ }
+ }
+}
+
+static gboolean remmina_plugin_spice_ask_auth(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gint ret;
+ gboolean disablepasswordstoring;
+ gchar *s_password;
+ gboolean save;
+
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE);
+
+ ret = remmina_plugin_service->protocol_plugin_init_auth(gp,
+ (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD),
+ _("Enter SPICE password"),
+ NULL,
+ remmina_plugin_service->file_get_string(remminafile, "password"),
+ NULL,
+ NULL);
+ if (ret == GTK_RESPONSE_OK) {
+ s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp);
+ save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp);
+ if (save) {
+ remmina_plugin_service->file_set_string(remminafile, "password", s_password);
+ } else {
+ remmina_plugin_service->file_set_string(remminafile, "password", NULL);
+ }
+ } else {
+ return FALSE;
+ }
+
+ g_object_set(gpdata->session, "password", s_password, NULL);
+ return TRUE;
+}
+
+static void remmina_plugin_spice_main_channel_event_cb(SpiceChannel *channel, SpiceChannelEvent event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ gchar *serverOption = g_strdup(remmina_plugin_service->file_get_string(remminafile, "server"));
+ gchar *message = NULL;
+ gchar *server = NULL;
+
+ if(gpdata->isUnix) {
+ gchar *val = str_replace(serverOption, "unix://", "");
+ message = g_strdup_printf("Unix socket server %s", val);
+ g_free(val), val = NULL;
+ } else {
+ gint port;
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ remmina_plugin_service->get_server_port(remmina_plugin_service->file_get_string(remminafile, "server"),
+ XSPICE_DEFAULT_PORT,
+ &server,
+ &port);
+ message = g_strdup_printf("TCP server %s:%d", server, port);
+ }
+
+ switch (event) {
+ case SPICE_CHANNEL_CLOSED:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Disconnected from the SPICE %s."), message);
+ remmina_plugin_spice_close_connection(gp);
+ REMMINA_PLUGIN_AUDIT(_("Disconnected from %s via SPICE"), message);
+ break;
+ case SPICE_CHANNEL_OPENED:
+ REMMINA_PLUGIN_AUDIT(_("Connected to %s via SPICE"), message);
+ break;
+ case SPICE_CHANNEL_ERROR_AUTH:
+ if (remmina_plugin_spice_ask_auth(gp)) {
+ remmina_plugin_spice_open_connection(gp);
+ }else{
+ /* Connection is cancelled by the user by clicking cancel on auth panel, close it without showing errors */
+ // remmina_plugin_service->protocol_plugin_set_error(gp, _("Invalid password."));
+ remmina_plugin_spice_close_connection(gp);
+ }
+ break;
+ case SPICE_CHANNEL_ERROR_TLS:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("TLS connection error."));
+ remmina_plugin_spice_close_connection(gp);
+ break;
+ case SPICE_CHANNEL_ERROR_IO:
+ case SPICE_CHANNEL_ERROR_LINK:
+ case SPICE_CHANNEL_ERROR_CONNECT:
+ remmina_plugin_service->protocol_plugin_set_error(gp, _("Connection to the SPICE server dropped."));
+ remmina_plugin_spice_close_connection(gp);
+ break;
+ default:
+ break;
+ }
+ g_free(server), server = NULL;
+ g_free(message), message = NULL;
+ g_free(serverOption), message = NULL;
+}
+
+void remmina_plugin_spice_agent_connected_event_cb(SpiceChannel *channel, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gboolean connected;
+
+ g_object_get(channel,
+ "agent-connected", &connected,
+ NULL);
+
+ if (connected) {
+ remmina_plugin_service->protocol_plugin_unlock_dynres(gp);
+ } else {
+ remmina_plugin_service->protocol_plugin_lock_dynres(gp);
+ }
+}
+
+static void remmina_plugin_spice_display_ready_cb(GObject *display, GParamSpec *param_spec, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gboolean ready;
+
+ g_object_get(display, "ready", &ready, NULL);
+
+ if (ready) {
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ g_signal_handlers_disconnect_by_func(display,
+ G_CALLBACK(remmina_plugin_spice_display_ready_cb),
+ gp);
+
+ RemminaScaleMode scaleMode = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp);
+ g_object_set(display,
+ "scaling", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED),
+ "resize-guest", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES),
+ NULL);
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 34, 0)
+ SpiceVideoCodecType videocodec = remmina_plugin_service->file_get_int(remminafile, "videocodec", 0);
+ if (videocodec) {
+# if SPICE_GTK_CHECK_VERSION(0, 38, 0)
+ GError *err = NULL;
+ guint i;
+
+ GArray *preferred_codecs = g_array_sized_new(FALSE, FALSE,
+ sizeof(gint),
+ (SPICE_VIDEO_CODEC_TYPE_ENUM_END - 1));
+
+ g_array_append_val(preferred_codecs, videocodec);
+ for (i = SPICE_VIDEO_CODEC_TYPE_MJPEG; i < SPICE_VIDEO_CODEC_TYPE_ENUM_END; ++i) {
+ if (i != videocodec) {
+ g_array_append_val(preferred_codecs, i);
+ }
+ }
+
+ if (!spice_display_channel_change_preferred_video_codec_types(SPICE_CHANNEL(gpdata->display_channel),
+ (gint *) preferred_codecs->data,
+ preferred_codecs->len,
+ &err)) {
+ REMMINA_PLUGIN_DEBUG("Could not set video-codec preference. %s", err->message);
+ g_error_free(err);
+ }
+
+ g_clear_pointer(&preferred_codecs, g_array_unref);
+
+# elif SPICE_GTK_CHECK_VERSION(0, 35, 0)
+ spice_display_channel_change_preferred_video_codec_type(SPICE_CHANNEL(gpdata->display_channel),
+ videocodec);
+# else
+ spice_display_change_preferred_video_codec_type(SPICE_CHANNEL(gpdata->display_channel),
+ videocodec);
+# endif
+ }
+# endif
+#endif
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ SpiceImageCompression imagecompression = remmina_plugin_service->file_get_int(remminafile, "imagecompression", 0);
+ if (imagecompression) {
+# if SPICE_GTK_CHECK_VERSION(0, 35, 0)
+ spice_display_channel_change_preferred_compression(SPICE_CHANNEL(gpdata->display_channel),
+ imagecompression);
+# else
+ spice_display_change_preferred_compression(SPICE_CHANNEL(gpdata->display_channel),
+ imagecompression);
+# endif
+ }
+# endif
+#endif
+
+ gtk_container_add(GTK_CONTAINER(gp), GTK_WIDGET(display));
+ gtk_widget_show(GTK_WIDGET(display));
+
+ remmina_plugin_service->protocol_plugin_register_hostkey(gp, GTK_WIDGET(display));
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+ }
+}
+
+/* Send a keystroke to the plugin window */
+static void remmina_plugin_spice_keystroke(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (gpdata->display) {
+ spice_display_send_keys(gpdata->display,
+ keystrokes,
+ keylen,
+ SPICE_DISPLAY_KEY_EVENT_CLICK);
+ }
+}
+
+/* Send CTRL+ALT+DEL keys keystrokes to the plugin socket widget */
+static void remmina_plugin_spice_send_ctrlaltdel(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ guint keys[] = { GDK_KEY_Control_L, GDK_KEY_Alt_L, GDK_KEY_Delete };
+
+ remmina_plugin_spice_keystroke(gp, keys, G_N_ELEMENTS(keys));
+}
+
+static void remmina_plugin_spice_update_scale_mode(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gint width, height;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaScaleMode scaleMode = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp);
+
+ g_object_set(gpdata->display,
+ "scaling", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED),
+ "resize-guest", (scaleMode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES),
+ NULL);
+
+ if (scaleMode != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE) {
+ /* In scaled mode, the SpiceDisplay will get its dimensions from its parent */
+ gtk_widget_set_size_request(GTK_WIDGET(gpdata->display), -1, -1 );
+ }else {
+ /* In non scaled mode, the plugins forces dimensions of the SpiceDisplay */
+ g_object_get(gpdata->display_channel,
+ "width", &width,
+ "height", &height,
+ NULL);
+ gtk_widget_set_size_request(GTK_WIDGET(gpdata->display), width, height);
+ }
+}
+
+static gboolean remmina_plugin_spice_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+
+ return TRUE;
+}
+
+static void remmina_plugin_spice_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ switch (feature->id) {
+ case REMMINA_PLUGIN_SPICE_FEATURE_PREF_VIEWONLY:
+ g_object_set(gpdata->session,
+ "read-only",
+ remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE),
+ NULL);
+ break;
+ case REMMINA_PLUGIN_SPICE_FEATURE_PREF_DISABLECLIPBOARD:
+ g_object_set(gpdata->gtk_session,
+ "auto-clipboard",
+ !remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE),
+ NULL);
+ break;
+ case REMMINA_PLUGIN_SPICE_FEATURE_DYNRESUPDATE:
+ case REMMINA_PLUGIN_SPICE_FEATURE_SCALE:
+ remmina_plugin_spice_update_scale_mode(gp);
+ break;
+ case REMMINA_PLUGIN_SPICE_FEATURE_TOOL_SENDCTRLALTDEL:
+ remmina_plugin_spice_send_ctrlaltdel(gp);
+ break;
+ case REMMINA_PLUGIN_SPICE_FEATURE_TOOL_USBREDIR:
+ remmina_plugin_spice_select_usb_devices(gp);
+ break;
+ default:
+ break;
+ }
+}
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 34, 0)
+/* Array of key/value pairs for preferred video codec
+ * Key - SpiceVideoCodecType (spice/enums.h)
+ */
+static gpointer videocodec_list[] =
+{
+ "0", N_("Default"),
+ "1", "mjpeg",
+ "2", "vp8",
+ "3", "h264",
+ "4", "vp9",
+ "5", "h265",
+ NULL
+};
+# endif
+#endif
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+/* Array of key/value pairs for preferred video codec
+ * Key - SpiceImageCompression (spice/enums.h)
+ */
+static gpointer imagecompression_list[] =
+{
+ "0", N_("Default"),
+ "1", N_("Off"),
+ "2", N_("Auto GLZ"),
+ "3", N_("Auto LZ"),
+ "4", "Quic",
+ "5", "GLZ",
+ "6", "LZ",
+ "7", "LZ4",
+ NULL
+};
+# endif
+#endif
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 34, 0)
+static gchar disablegstvideooverlay_tooltip[] =
+ N_("Disable video overlay if videos are not displayed properly.\n");
+# endif
+#endif
+
+/* Array of RemminaProtocolSetting for basic settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting tooltip
+ * g) Validation data pointer, will be passed to the validation callback method.
+ * h) Validation callback method (Can be NULL. Every entry will be valid then.)
+ * use following prototype:
+ * gboolean mysetting_validator_method(gpointer key, gpointer value,
+ * gpointer validator_data);
+ * gpointer key is a gchar* containing the setting's name,
+ * gpointer value contains the value which should be validated,
+ * gpointer validator_data contains your passed data.
+ */
+static const RemminaProtocolSetting remmina_plugin_spice_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("User password"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "usetls", N_("Use TLS encryption"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_FILE, "cacert", N_("Server CA certificate"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "sharefolder", N_("Share folder"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL }
+};
+
+/* Array of RemminaProtocolSetting for advanced settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting Tooltip
+ */
+static const RemminaProtocolSetting remmina_plugin_spice_advanced_settings[] =
+{
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 35, 0)
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "videocodec", N_("Preferred video codec"), FALSE, videocodec_list, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablegstvideooverlay", N_("Turn off GStreamer overlay"), FALSE, NULL, disablegstvideooverlay_tooltip},
+# endif
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "imagecompression", N_("Preferred image compression"), FALSE, imagecompression_list, NULL},
+# endif
+#endif
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableclipboard", N_("No clipboard sync"), TRUE, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), TRUE, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enableaudio", N_("Enable audio channel"), TRUE, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "sharesmartcard", N_("Share smart card"), TRUE, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "viewonly", N_("View only"), TRUE, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, TRUE, NULL, NULL}
+};
+
+/* Array for available features.
+ * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */
+static const RemminaProtocolFeature remmina_plugin_spice_features[] =
+{
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_SPICE_FEATURE_PREF_VIEWONLY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "viewonly", N_("View only")},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_SPICE_FEATURE_PREF_DISABLECLIPBOARD, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "disableclipboard", N_("No clipboard sync")},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_SPICE_FEATURE_TOOL_SENDCTRLALTDEL, N_("Send Ctrl+Alt+Delete"), NULL, NULL},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_SPICE_FEATURE_TOOL_USBREDIR, N_("Select USB devices for redirection"), NULL, NULL},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE, REMMINA_PLUGIN_SPICE_FEATURE_DYNRESUPDATE, NULL, NULL, NULL},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, REMMINA_PLUGIN_SPICE_FEATURE_SCALE, NULL, NULL, NULL},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL}
+};
+
+
+static RemminaProtocolPlugin remmina_plugin_spice =
+{
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ "SPICE", // Name
+ N_("SPICE - Simple Protocol for Independent Computing Environments"), // Description
+ GETTEXT_PACKAGE, // Translation domain
+ VERSION, // Version number
+ "org.remmina.Remmina-spice-symbolic", // Icon for normal connection
+ "org.remmina.Remmina-spice-ssh-symbolic", // Icon for SSH connection
+ remmina_plugin_spice_basic_settings, // Array for basic settings
+ remmina_plugin_spice_advanced_settings, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type
+ remmina_plugin_spice_features, // Array for available features
+ remmina_plugin_spice_init, // Plugin initialization
+ remmina_plugin_spice_open_connection, // Plugin open connection
+ remmina_plugin_spice_close_connection, // Plugin close connection
+ remmina_plugin_spice_query_feature, // Query for available features
+ remmina_plugin_spice_call_feature, // Call a feature
+ remmina_plugin_spice_keystroke, // Send a keystroke
+ NULL, // No screenshot support available
+ NULL, // RCW map event
+ NULL // RCW unmap event
+};
+
+void remmina_plugin_spice_remove_list_option(gpointer *option_list, const gchar *option_to_remove) {
+ gpointer *src, *dst;
+
+ TRACE_CALL(__func__);
+
+ dst = src = option_list;
+ while (*src) {
+ if (strcmp(*src, option_to_remove) != 0) {
+ if (dst != src) {
+ *dst = *src;
+ *(dst + 1) = *(src + 1);
+ }
+ dst += 2;
+ }
+ src += 2;
+ }
+ *dst = NULL;
+}
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+gboolean remmina_plugin_spice_is_lz4_supported() {
+ gboolean result = FALSE;
+ GOptionContext *context;
+ GOptionGroup *spiceGroup;
+ gchar *spiceHelp;
+
+ TRACE_CALL(__func__);
+
+ spiceGroup = spice_get_option_group();
+ context = g_option_context_new("- SPICE client test application");
+ g_option_context_add_group(context, spiceGroup);
+
+ spiceHelp = g_option_context_get_help(context, FALSE, spiceGroup);
+ if (g_strcmp0(spiceHelp, "") != 0) {
+ gchar **spiceHelpLines, **line;
+ spiceHelpLines = g_strsplit(spiceHelp, "\n", -1);
+
+ for (line = spiceHelpLines; *line != NULL; ++line) {
+ if (g_strstr_len(*line, -1, "spice-preferred-compression")) {
+ if (g_strstr_len(*line, -1, ",lz4,")) {
+ result = TRUE;
+ }
+
+ break;
+ }
+ }
+
+ g_strfreev(spiceHelpLines);
+ }
+ g_option_context_free(context);
+ g_free(spiceHelp);
+
+ return result;
+}
+# endif
+#endif
+
+G_MODULE_EXPORT gboolean
+remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_service = service;
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ if (!remmina_plugin_spice_is_lz4_supported()) {
+ char key_str[10];
+ sprintf(key_str, "%d", SPICE_IMAGE_COMPRESSION_LZ4);
+ remmina_plugin_spice_remove_list_option(imagecompression_list, key_str);
+ }
+# endif
+#endif
+
+ if (!service->register_plugin((RemminaPlugin*)&remmina_plugin_spice)) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
diff --git a/plugins/spice/spice_plugin.h b/plugins/spice/spice_plugin.h
new file mode 100644
index 0000000..d651689
--- /dev/null
+++ b/plugins/spice/spice_plugin.h
@@ -0,0 +1,93 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2018 Denis Ollier
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#include "common/remmina_plugin.h"
+#include <spice-client.h>
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+# include <spice-client-gtk.h>
+# else
+# include <spice-widget.h>
+# include <usb-device-widget.h>
+# endif
+#else
+# include <spice-widget.h>
+# include <usb-device-widget.h>
+#endif
+
+#define GET_PLUGIN_DATA(gp) (RemminaPluginSpiceData *)g_object_get_data(G_OBJECT(gp), "plugin-data")
+
+extern RemminaPluginService *remmina_plugin_service;
+
+#define REMMINA_PLUGIN_INFO(fmt, ...) \
+ remmina_plugin_service->_remmina_info(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_MESSAGE(fmt, ...) \
+ remmina_plugin_service->_remmina_message(__func, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) \
+ remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_WARNING(fmt, ...) \
+ remmina_plugin_service->_remmina_warning(__func__, fmt, ##__VA_ARGS__)
+
+/* This will intentionally crash Remmina */
+#define REMMINA_PLUGIN_ERROR(fmt, ...) \
+ remmina_plugin_service->_remmina_error(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_CRITICAL(fmt, ...) \
+ remmina_plugin_service->_remmina_critical(__func__, fmt, ##__VA_ARGS__)
+#define REMMINA_PLUGIN_AUDIT(fmt, ...) \
+ remmina_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__)
+
+typedef struct _RemminaPluginSpiceData {
+ SpiceAudio * audio;
+ SpiceDisplay * display;
+ SpiceDisplayChannel * display_channel;
+ SpiceGtkSession * gtk_session;
+ SpiceMainChannel * main_channel;
+ SpiceSession * session;
+ gchar * unixPath;
+ gboolean isUnix;
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+ /* key: SpiceFileTransferTask, value: RemminaPluginSpiceXferWidgets */
+ GHashTable * file_transfers;
+ GtkWidget * file_transfer_dialog;
+# endif /* SPICE_GTK_CHECK_VERSION(0, 31, 0) */
+#endif /* SPICE_GTK_CHECK_VERSION */
+} RemminaPluginSpiceData;
diff --git a/plugins/spice/spice_plugin_file_transfer.c b/plugins/spice/spice_plugin_file_transfer.c
new file mode 100644
index 0000000..ada7153
--- /dev/null
+++ b/plugins/spice/spice_plugin_file_transfer.c
@@ -0,0 +1,244 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2018 Denis Ollier
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "spice_plugin.h"
+
+#ifdef SPICE_GTK_CHECK_VERSION
+# if SPICE_GTK_CHECK_VERSION(0, 31, 0)
+
+static void remmina_plugin_spice_file_transfer_cancel_cb(GtkButton *, SpiceFileTransferTask *);
+static void remmina_plugin_spice_file_transfer_dialog_response_cb(GtkDialog *, gint, RemminaProtocolWidget *);
+static void remmina_plugin_spice_file_transfer_finished_cb(SpiceFileTransferTask *, GError *, RemminaProtocolWidget *);
+static void remmina_plugin_spice_file_transfer_progress_cb(GObject *, GParamSpec *, RemminaProtocolWidget *);
+
+typedef struct _RemminaPluginSpiceXferWidgets {
+ GtkWidget *vbox;
+ GtkWidget *hbox;
+ GtkWidget *progress;
+ GtkWidget *label;
+ GtkWidget *cancel;
+} RemminaPluginSpiceXferWidgets;
+
+static RemminaPluginSpiceXferWidgets * remmina_plugin_spice_xfer_widgets_new(SpiceFileTransferTask *);
+static void remmina_plugin_spice_xfer_widgets_free(RemminaPluginSpiceXferWidgets *widgets);
+
+void remmina_plugin_spice_file_transfer_new_cb(SpiceMainChannel *main_channel, SpiceFileTransferTask *task, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GtkWidget *dialog_content;
+ RemminaPluginSpiceXferWidgets *widgets;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ g_signal_connect(task,
+ "finished",
+ G_CALLBACK(remmina_plugin_spice_file_transfer_finished_cb),
+ gp);
+
+ if (!gpdata->file_transfers) {
+ gpdata->file_transfers = g_hash_table_new_full(g_direct_hash,
+ g_direct_equal,
+ g_object_unref,
+ (GDestroyNotify)remmina_plugin_spice_xfer_widgets_free);
+ }
+
+ if (!gpdata->file_transfer_dialog) {
+ /*
+ * FIXME: Use the RemminaConnectionWindow as transient parent widget
+ * (and add the GTK_DIALOG_DESTROY_WITH_PARENT flag) if it becomes
+ * accessible from the Remmina plugin API.
+ */
+ gpdata->file_transfer_dialog = gtk_dialog_new_with_buttons(_("File Transfers"),
+ NULL, 0,
+ _("_Cancel"),
+ GTK_RESPONSE_CANCEL,
+ NULL);
+ dialog_content = gtk_dialog_get_content_area(GTK_DIALOG(gpdata->file_transfer_dialog));
+ gtk_widget_set_size_request(dialog_content, 400, -1);
+ gtk_window_set_resizable(GTK_WINDOW(gpdata->file_transfer_dialog), FALSE);
+ g_signal_connect(gpdata->file_transfer_dialog,
+ "response",
+ G_CALLBACK(remmina_plugin_spice_file_transfer_dialog_response_cb),
+ gp);
+ }
+
+ widgets = remmina_plugin_spice_xfer_widgets_new(task);
+ g_hash_table_insert(gpdata->file_transfers, g_object_ref(task), widgets);
+
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(gpdata->file_transfer_dialog))),
+ widgets->vbox,
+ TRUE, TRUE, 6);
+
+ g_signal_connect(task,
+ "notify::progress",
+ G_CALLBACK(remmina_plugin_spice_file_transfer_progress_cb),
+ gp);
+
+ gtk_widget_show(gpdata->file_transfer_dialog);
+}
+
+static RemminaPluginSpiceXferWidgets * remmina_plugin_spice_xfer_widgets_new(SpiceFileTransferTask *task)
+{
+ TRACE_CALL(__func__);
+
+ gchar *filename;
+ RemminaPluginSpiceXferWidgets *widgets = g_new0(RemminaPluginSpiceXferWidgets, 1);
+
+ widgets->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ widgets->hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+
+ filename = spice_file_transfer_task_get_filename(task);
+ widgets->label = gtk_label_new(filename);
+ gtk_widget_set_halign(widgets->label, GTK_ALIGN_START);
+ gtk_widget_set_valign(widgets->label, GTK_ALIGN_BASELINE);
+
+ widgets->progress = gtk_progress_bar_new();
+ gtk_widget_set_hexpand(widgets->progress, TRUE);
+ gtk_widget_set_valign(widgets->progress, GTK_ALIGN_CENTER);
+
+ widgets->cancel = gtk_button_new_from_icon_name("gtk-cancel", GTK_ICON_SIZE_SMALL_TOOLBAR);
+ g_signal_connect(widgets->cancel,
+ "clicked",
+ G_CALLBACK(remmina_plugin_spice_file_transfer_cancel_cb),
+ task);
+ gtk_widget_set_hexpand(widgets->cancel, FALSE);
+ gtk_widget_set_valign(widgets->cancel, GTK_ALIGN_CENTER);
+
+ gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->progress,
+ TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->cancel,
+ FALSE, TRUE, 0);
+
+ gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->label,
+ TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->hbox,
+ TRUE, TRUE, 0);
+
+ gtk_widget_show_all(widgets->vbox);
+
+ g_free(filename);
+
+ return widgets;
+}
+
+static void remmina_plugin_spice_xfer_widgets_free(RemminaPluginSpiceXferWidgets *widgets)
+{
+ TRACE_CALL(__func__);
+
+ /* Child widgets will be destroyed automatically */
+ gtk_widget_destroy(widgets->vbox);
+ g_free(widgets);
+}
+
+static void remmina_plugin_spice_file_transfer_cancel_cb(GtkButton *button, SpiceFileTransferTask *task)
+{
+ TRACE_CALL(__func__);
+
+ spice_file_transfer_task_cancel(task);
+}
+
+
+static void remmina_plugin_spice_file_transfer_dialog_response_cb(GtkDialog *dialog, gint response, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GHashTableIter iter;
+ gpointer key, value;
+ SpiceFileTransferTask *task;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (response == GTK_RESPONSE_CANCEL) {
+ g_hash_table_iter_init(&iter, gpdata->file_transfers);
+ while (g_hash_table_iter_next(&iter, &key, &value)) {
+ task = key;
+ spice_file_transfer_task_cancel(task);
+ }
+ }
+}
+
+static void remmina_plugin_spice_file_transfer_progress_cb(GObject *task, GParamSpec *param_spec, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginSpiceXferWidgets *widgets;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ widgets = g_hash_table_lookup(gpdata->file_transfers, task);
+ if (widgets) {
+ gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(widgets->progress),
+ spice_file_transfer_task_get_progress(SPICE_FILE_TRANSFER_TASK(task)));
+ }
+}
+
+static void remmina_plugin_spice_file_transfer_finished_cb(SpiceFileTransferTask *task, GError *error, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gchar *filename, *notification_message;
+ GNotification *notification;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ /*
+ * Send a desktop notification to inform about the outcome of
+ * the file transfer.
+ */
+ filename = spice_file_transfer_task_get_filename(task);
+
+ if (error) {
+ notification = g_notification_new(_("Transfer error"));
+ notification_message = g_strdup_printf(_("%s: %s"),
+ filename, error->message);
+ }else {
+ notification = g_notification_new(_("Transfer completed"));
+ notification_message = g_strdup_printf(_("The %s file has been transferred"),
+ filename);
+ }
+
+ g_notification_set_body(notification, notification_message);
+ g_application_send_notification(g_application_get_default(),
+ "remmina-plugin-spice-file-transfer-finished",
+ notification);
+
+ g_hash_table_remove(gpdata->file_transfers, task);
+
+ if (!g_hash_table_size(gpdata->file_transfers)) {
+ gtk_widget_hide(gpdata->file_transfer_dialog);
+ }
+
+ g_free(filename);
+ g_free(notification_message);
+ g_object_unref(notification);
+}
+# endif /* SPICE_GTK_CHECK_VERSION(0, 31, 0) */
+#endif /* SPICE_GTK_CHECK_VERSION */
diff --git a/plugins/spice/spice_plugin_usb.c b/plugins/spice/spice_plugin_usb.c
new file mode 100644
index 0000000..b03205b
--- /dev/null
+++ b/plugins/spice/spice_plugin_usb.c
@@ -0,0 +1,100 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2018 Denis Ollier
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "spice_plugin.h"
+
+static void remmina_plugin_spice_usb_connect_failed_cb(GObject *, SpiceUsbDevice *, GError *, RemminaProtocolWidget *);
+
+void remmina_plugin_spice_select_usb_devices(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GtkWidget *dialog, *usb_device_widget;
+ RemminaPluginSpiceData *gpdata = GET_PLUGIN_DATA(gp);
+
+ /*
+ * FIXME: Use the RemminaConnectionWindow as transient parent widget
+ * (and add the GTK_DIALOG_DESTROY_WITH_PARENT flag) if it becomes
+ * accessible from the Remmina plugin API.
+ */
+ dialog = gtk_dialog_new_with_buttons(_("Select USB devices for redirection"),
+ NULL,
+ GTK_DIALOG_MODAL,
+ _("_Close"),
+ GTK_RESPONSE_ACCEPT,
+ NULL);
+ gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
+
+ usb_device_widget = spice_usb_device_widget_new(gpdata->session, NULL);
+ g_signal_connect(usb_device_widget,
+ "connect-failed",
+ G_CALLBACK(remmina_plugin_spice_usb_connect_failed_cb),
+ gp);
+
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
+ usb_device_widget,
+ TRUE,
+ TRUE,
+ 0);
+ gtk_widget_show_all(dialog);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+}
+
+static void remmina_plugin_spice_usb_connect_failed_cb(GObject *object, SpiceUsbDevice *usb_device, GError *error, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ GtkWidget *dialog;
+
+ if (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_CANCELLED) {
+ return;
+ }
+
+ /*
+ * FIXME: Use the RemminaConnectionWindow as transient parent widget
+ * (and add the GTK_DIALOG_DESTROY_WITH_PARENT flag) if it becomes
+ * accessible from the Remmina plugin API.
+ */
+ dialog = gtk_message_dialog_new(NULL,
+ GTK_DIALOG_MODAL,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ _("USB redirection error"));
+ gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
+ "%s",
+ error->message);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+}
diff --git a/plugins/telepathy/CMakeLists.txt b/plugins/telepathy/CMakeLists.txt
new file mode 100644
index 0000000..0314368
--- /dev/null
+++ b/plugins/telepathy/CMakeLists.txt
@@ -0,0 +1,66 @@
+# remmina-plugin-telepathy - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2011 Marc-Andre Moreau
+# Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. If you
+# do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+
+set(REMMINA_PLUGIN_TELEPATHY_SRCS
+ telepathy_plugin.c
+ telepathy_handler.c
+ telepathy_channel_handler.c
+ )
+
+add_library(remmina-plugin-telepathy MODULE ${REMMINA_PLUGIN_TELEPATHY_SRCS})
+set_target_properties(remmina-plugin-telepathy PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-telepathy PROPERTIES NO_SONAME 1)
+
+find_required_package(GTK3)
+pkg_check_modules(PC_DBUS_GLIB dbus-glib-1)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${TELEPATHY_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-telepathy ${REMMINA_COMMON_LIBRARIES}
+ ${TELEPATHY_LIBRARIES} ${PC_DBUS_GLIB_LIBRARIES})
+
+install(TARGETS remmina-plugin-telepathy DESTINATION ${REMMINA_PLUGINDIR})
+
+# Telepathy client file
+install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/Remmina.client
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/telepathy/clients)
+
+# DBus activation file
+if(NOT REMMINA_BINARY_PATH)
+ set(REMMINA_BINARY_PATH ${CMAKE_INSTALL_FULL_BINDIR}/remmina)
+endif()
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/org.freedesktop.Telepathy.Client.Remmina.service.in
+ ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Telepathy.Client.Remmina.service @ONLY)
+
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.freedesktop.Telepathy.Client.Remmina.service
+ DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/dbus-1/services)
diff --git a/plugins/telepathy/Remmina.client b/plugins/telepathy/Remmina.client
new file mode 100644
index 0000000..e5baf37
--- /dev/null
+++ b/plugins/telepathy/Remmina.client
@@ -0,0 +1,8 @@
+[org.freedesktop.Telepathy.Client]
+Interfaces=org.freedesktop.Telepathy.Client.Handler;org.freedesktop.Telepathy.Client.Interface.Requests
+
+[org.freedesktop.Telepathy.Client.Handler.HandlerChannelFilter 0]
+org.freedesktop.Telepathy.Channel.ChannelType s=org.freedesktop.Telepathy.Channel.Type.StreamTube
+org.freedesktop.Telepathy.Channel.TargetHandleType u=1
+org.freedesktop.Telepathy.Channel.Requested b=false
+org.freedesktop.Telepathy.Channel.Type.StreamTube.Service s=rfb
diff --git a/plugins/telepathy/org.freedesktop.Telepathy.Client.Remmina.service.in b/plugins/telepathy/org.freedesktop.Telepathy.Client.Remmina.service.in
new file mode 100644
index 0000000..95c0cf7
--- /dev/null
+++ b/plugins/telepathy/org.freedesktop.Telepathy.Client.Remmina.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=org.freedesktop.Telepathy.Client.Remmina
+Exec=@REMMINA_BINARY_PATH@ -x telepathy
diff --git a/plugins/telepathy/telepathy_channel_handler.c b/plugins/telepathy/telepathy_channel_handler.c
new file mode 100644
index 0000000..b690d97
--- /dev/null
+++ b/plugins/telepathy/telepathy_channel_handler.c
@@ -0,0 +1,373 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "common/remmina_plugin.h"
+#include <telepathy-glib/account.h>
+#include <telepathy-glib/channel.h>
+#include <telepathy-glib/contact.h>
+#include <telepathy-glib/dbus.h>
+#include <telepathy-glib/defs.h>
+#include <telepathy-glib/handle.h>
+#include <telepathy-glib/interfaces.h>
+#include <telepathy-glib/svc-client.h>
+#include <telepathy-glib/util.h>
+#include "telepathy_channel_handler.h"
+
+extern RemminaPluginService *remmina_plugin_telepathy_service;
+
+typedef struct _RemminaTpChannelHandler {
+ gchar *connection_path;
+ gchar *channel_path;
+ GHashTable *channel_properties;
+ DBusGMethodInvocation *context;
+
+ GtkWidget *proto_widget;
+ guint disconnect_handler;
+
+ TpDBusDaemon *bus;
+ TpAccount *account;
+ TpConnection *connection;
+ TpChannel *channel;
+
+ gchar *alias;
+ gchar *host;
+ guint port;
+ gchar *protocol;
+} RemminaTpChannelHandler;
+
+static void remmina_tp_channel_handler_free(RemminaTpChannelHandler *chandler)
+{
+ TRACE_CALL(__func__);
+ if (chandler->disconnect_handler) {
+ g_signal_handler_disconnect(chandler->proto_widget, chandler->disconnect_handler);
+ chandler->disconnect_handler = 0;
+ }
+ g_free(chandler->connection_path);
+ g_free(chandler->channel_path);
+ g_hash_table_destroy(chandler->channel_properties);
+ if (chandler->bus) {
+ g_object_unref(chandler->bus);
+ }
+ if (chandler->account) {
+ g_object_unref(chandler->account);
+ }
+ if (chandler->connection) {
+ g_object_unref(chandler->connection);
+ }
+ if (chandler->channel) {
+ g_object_unref(chandler->channel);
+ }
+ if (chandler->alias) {
+ g_free(chandler->alias);
+ }
+ if (chandler->host) {
+ g_free(chandler->host);
+ }
+ if (chandler->protocol) {
+ g_free(chandler->protocol);
+ }
+ g_free(chandler);
+}
+
+static void remmina_tp_channel_handler_channel_closed(TpChannel *channel, gpointer user_data, GObject *self)
+{
+ TRACE_CALL(__func__);
+ RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data;
+
+ g_print("%s: %s\n", __func__, chandler->channel_path);
+ remmina_tp_channel_handler_free(chandler);
+}
+
+static void remmina_tp_channel_handler_on_disconnect(GtkWidget *widget, RemminaTpChannelHandler *chandler)
+{
+ TRACE_CALL(__func__);
+ g_print("%s: %s\n", __func__, chandler->channel_path);
+ g_signal_handler_disconnect(widget, chandler->disconnect_handler);
+ chandler->disconnect_handler = 0;
+ tp_cli_channel_call_close(chandler->channel, -1, NULL, NULL, NULL, NULL);
+}
+
+static void remmina_tp_channel_handler_connect(RemminaTpChannelHandler *chandler)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+ gchar *s;
+
+ remminafile = remmina_plugin_telepathy_service->file_new();
+ remmina_plugin_telepathy_service->file_set_string(remminafile, "name", chandler->alias);
+ remmina_plugin_telepathy_service->file_set_string(remminafile, "protocol", chandler->protocol);
+ s = g_strdup_printf("[%s]:%i", chandler->host, chandler->port);
+ remmina_plugin_telepathy_service->file_set_string(remminafile, "server", s);
+ g_free(s);
+ remmina_plugin_telepathy_service->file_set_int(remminafile, "colordepth", 8);
+
+ g_free(chandler->alias);
+ chandler->alias = NULL;
+ g_free(chandler->protocol);
+ chandler->protocol = NULL;
+
+ chandler->proto_widget = remmina_plugin_telepathy_service->open_connection(remminafile,
+ G_CALLBACK(remmina_tp_channel_handler_on_disconnect), chandler, &chandler->disconnect_handler);
+}
+
+static void remmina_tp_channel_handler_get_service(TpProxy *channel, const GValue *service, const GError *error,
+ gpointer user_data, GObject *weak_object)
+{
+ TRACE_CALL(__func__);
+ RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data;
+ const gchar *svc;
+
+ if (error != NULL) {
+ g_print("%s: %s", __func__, error->message);
+ remmina_tp_channel_handler_free(chandler);
+ return;
+ }
+ svc = g_value_get_string(service);
+ g_print("%s: %s %s:%u\n", __func__, svc, chandler->host, chandler->port);
+
+ if (g_strcmp0(svc, "rfb") == 0) {
+ chandler->protocol = g_strdup("VNC");
+ }else {
+ chandler->protocol = g_ascii_strup(svc, -1);
+ }
+ remmina_tp_channel_handler_connect(chandler);
+}
+
+static void remmina_tp_channel_handler_accept(TpChannel *channel, const GValue *address, const GError *error,
+ gpointer user_data, GObject *weak_object)
+{
+ TRACE_CALL(__func__);
+ RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data;
+
+ if (error != NULL) {
+ g_print("%s: %s", __func__, error->message);
+ remmina_tp_channel_handler_free(chandler);
+ return;
+ }
+
+ dbus_g_type_struct_get(address, 0, &chandler->host, 1, &chandler->port, G_MAXUINT);
+
+ tp_cli_dbus_properties_call_get(channel, -1, TP_IFACE_CHANNEL_TYPE_STREAM_TUBE, "Service",
+ remmina_tp_channel_handler_get_service, chandler, NULL, NULL);
+}
+
+static void remmina_tp_channel_handler_on_response(GtkDialog *dialog, gint response_id, RemminaTpChannelHandler *chandler)
+{
+ TRACE_CALL(__func__);
+ GValue noop =
+ { 0 };
+ GError *error;
+
+ if (response_id == GTK_RESPONSE_YES) {
+ g_value_init(&noop, G_TYPE_INT);
+ tp_cli_channel_type_stream_tube_call_accept(chandler->channel, -1, TP_SOCKET_ADDRESS_TYPE_IPV4,
+ TP_SOCKET_ACCESS_CONTROL_LOCALHOST, &noop, remmina_tp_channel_handler_accept, chandler, NULL,
+ NULL);
+ g_value_unset(&noop);
+ tp_svc_client_handler_return_from_handle_channels(chandler->context);
+ }else {
+ error = g_error_new(TP_ERROR, TP_ERROR_NOT_AVAILABLE, "Channel rejected by user.");
+ dbus_g_method_return_error(chandler->context, error);
+ g_error_free(error);
+ remmina_tp_channel_handler_free(chandler);
+ }
+}
+
+static void remmina_tp_channel_handler_get_contacts(TpConnection *connection, guint n_contacts, TpContact * const *contacts,
+ guint n_failed, const TpHandle *failed, const GError *error, gpointer user_data, GObject *weak_object)
+{
+ TRACE_CALL(__func__);
+ RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data;
+ TpContact *contact;
+ gchar *token;
+ const gchar *cm;
+ const gchar *protocol;
+ gchar *filename;
+ GdkPixbuf *pixbuf;
+ GtkWidget *image;
+ GtkWidget *dialog;
+
+ if (error != NULL) {
+ g_print("%s: %s", __func__, error->message);
+ remmina_tp_channel_handler_free(chandler);
+ return;
+ }
+ if (n_contacts <= 0) {
+ g_print("%s: no contacts\n", __func__);
+ remmina_tp_channel_handler_free(chandler);
+ return;
+ }
+ contact = contacts[0];
+ chandler->alias = g_strdup(tp_contact_get_alias(contact));
+
+ dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
+ _("%s wants to share their desktop.\nDo you accept?"), chandler->alias);
+ g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(remmina_tp_channel_handler_on_response), chandler);
+ g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_window_set_title(GTK_WINDOW(dialog), _("Desktop sharing invitation"));
+ remmina_plugin_telepathy_service->ui_register(dialog);
+ gtk_widget_show(dialog);
+
+ token = (gchar*)tp_contact_get_avatar_token(contact);
+ if (token == NULL) {
+ return;
+ }
+ protocol = tp_connection_get_protocol_name(chandler->connection);
+ cm = tp_connection_get_cm_name(chandler->connection);
+ if (!protocol || !cm) {
+ return;
+ }
+ token = tp_escape_as_identifier(token);
+ filename = g_build_filename(g_get_user_cache_dir(), "telepathy", "avatars", cm, protocol, token, NULL);
+ g_free(token);
+ if (g_file_test(filename, G_FILE_TEST_EXISTS)) {
+ pixbuf = gdk_pixbuf_new_from_file(filename, NULL);
+ if (pixbuf) {
+ image = gtk_image_new_from_pixbuf(pixbuf);
+ gtk_widget_show(image);
+ g_object_unref(pixbuf);
+ gtk_message_dialog_set_image(GTK_MESSAGE_DIALOG(dialog), image);
+ }
+ }
+ g_free(filename);
+}
+
+static void remmina_tp_channel_handler_channel_ready(TpChannel *channel, const GError *channel_error, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data;
+ TpHandle handle;
+ GError *error = NULL;
+ TpContactFeature features[] =
+ { TP_CONTACT_FEATURE_ALIAS, TP_CONTACT_FEATURE_AVATAR_TOKEN };
+
+ if (channel_error != NULL) {
+ g_print("%s: %s\n", __func__, channel_error->message);
+ remmina_tp_channel_handler_free(chandler);
+ return;
+ }
+
+ if (tp_cli_channel_connect_to_closed(channel, remmina_tp_channel_handler_channel_closed, chandler, NULL, NULL, &error)
+ == NULL) {
+ g_print("tp_cli_channel_connect_to_closed: %s\n", channel_error->message);
+ remmina_tp_channel_handler_free(chandler);
+ return;
+ }
+ g_print("%s: %s\n", __func__, chandler->channel_path);
+
+ handle = tp_channel_get_handle(channel, NULL);
+ tp_connection_get_contacts_by_handle(chandler->connection, 1, &handle, G_N_ELEMENTS(features), features,
+ remmina_tp_channel_handler_get_contacts, chandler, NULL, NULL);
+}
+
+static void remmina_tp_channel_handler_connection_ready(TpConnection *connection, const GError *connection_error,
+ gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data;
+ GError *error = NULL;
+
+ if (connection_error != NULL) {
+ g_print("%s: %s\n", __func__, connection_error->message);
+ remmina_tp_channel_handler_free(chandler);
+ return;
+ }
+
+ chandler->channel = tp_channel_new_from_properties(connection, chandler->channel_path, chandler->channel_properties,
+ &error);
+ if (chandler->channel == NULL) {
+ g_print("tp_channel_new_from_properties: %s\n", error->message);
+ remmina_tp_channel_handler_free(chandler);
+ return;
+ }
+ tp_channel_call_when_ready(chandler->channel, remmina_tp_channel_handler_channel_ready, chandler);
+}
+
+static void remmina_tp_channel_handler_account_ready(GObject *account, GAsyncResult *res, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaTpChannelHandler *chandler = (RemminaTpChannelHandler*)user_data;
+ GError *error = NULL;
+
+ if (!tp_account_prepare_finish(TP_ACCOUNT(account), res, &error)) {
+ g_print("tp_account_prepare_finish: %s\n", error->message);
+ remmina_tp_channel_handler_free(chandler);
+ return;
+ }
+
+ chandler->connection = tp_connection_new(chandler->bus, NULL, chandler->connection_path, &error);
+ if (chandler->connection == NULL) {
+ g_print("tp_connection_new: %s\n", error->message);
+ remmina_tp_channel_handler_free(chandler);
+ return;
+ }
+ tp_connection_call_when_ready(chandler->connection, remmina_tp_channel_handler_connection_ready, chandler);
+}
+
+void remmina_tp_channel_handler_new(const gchar *account_path, const gchar *connection_path, const gchar *channel_path,
+ GHashTable *channel_properties, DBusGMethodInvocation *context)
+{
+ TRACE_CALL(__func__);
+ TpDBusDaemon *bus;
+ TpAccount *account;
+ GError *error = NULL;
+ RemminaTpChannelHandler *chandler;
+
+ bus = tp_dbus_daemon_dup(&error);
+ if (bus == NULL) {
+ g_print("tp_dbus_daemon_dup: %s", error->message);
+ return;
+ }
+ account = tp_account_new(bus, account_path, &error);
+ if (account == NULL) {
+ g_object_unref(bus);
+ g_print("tp_account_new: %s", error->message);
+ return;
+ }
+
+ chandler = g_new0(RemminaTpChannelHandler, 1);
+ chandler->bus = bus;
+ chandler->account = account;
+ chandler->connection_path = g_strdup(connection_path);
+ chandler->channel_path = g_strdup(channel_path);
+ chandler->channel_properties = tp_asv_new(NULL, NULL);
+ tp_g_hash_table_update(chandler->channel_properties, channel_properties, (GBoxedCopyFunc)g_strdup,
+ (GBoxedCopyFunc)tp_g_value_slice_dup);
+ chandler->context = context;
+
+ tp_account_prepare_async(account, NULL, remmina_tp_channel_handler_account_ready, chandler);
+}
+
diff --git a/plugins/telepathy/telepathy_channel_handler.h b/plugins/telepathy/telepathy_channel_handler.h
new file mode 100644
index 0000000..675dcae
--- /dev/null
+++ b/plugins/telepathy/telepathy_channel_handler.h
@@ -0,0 +1,43 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010 Vic Lee
+ * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+G_BEGIN_DECLS
+
+void
+remmina_tp_channel_handler_new(const gchar *account_path, const gchar *connection_path, const gchar *channel_path, GHashTable *channel_properties, DBusGMethodInvocation *context);
+
+G_END_DECLS
diff --git a/plugins/telepathy/telepathy_handler.c b/plugins/telepathy/telepathy_handler.c
new file mode 100644
index 0000000..5b3ef6f
--- /dev/null
+++ b/plugins/telepathy/telepathy_handler.c
@@ -0,0 +1,128 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "common/remmina_plugin.h"
+#include <gmodule.h>
+#include <telepathy-glib/dbus.h>
+#include <telepathy-glib/defs.h>
+#include <telepathy-glib/svc-client.h>
+#include "telepathy_channel_handler.h"
+#include "telepathy_handler.h"
+
+extern RemminaPluginService *remmina_plugin_telepathy_service;
+
+#define REMMINA_TP_BUS_NAME TP_CLIENT_BUS_NAME_BASE "Remmina"
+#define REMMINA_TP_OBJECT_PATH TP_CLIENT_OBJECT_PATH_BASE "Remmina"
+
+static void remmina_tp_handler_iface_init(gpointer g_iface, gpointer iface_data);
+
+G_DEFINE_TYPE_WITH_CODE(RemminaTpHandler, remmina_tp_handler, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE(TP_TYPE_SVC_CLIENT, NULL);
+ G_IMPLEMENT_INTERFACE( TP_TYPE_SVC_CLIENT_HANDLER, remmina_tp_handler_iface_init);
+ );
+
+static void remmina_tp_handler_class_init(RemminaTpHandlerClass *klass)
+{
+ TRACE_CALL(__func__);
+}
+
+static void remmina_tp_handler_init(RemminaTpHandler *handler)
+{
+ TRACE_CALL(__func__);
+}
+
+static void remmina_tp_handler_handle_channels(TpSvcClientHandler *handler, const char *account_path,
+ const char *connection_path, const GPtrArray *channels, const GPtrArray *requests_satisfied,
+ guint64 user_action_time, GHashTable *handler_info, DBusGMethodInvocation *context)
+{
+ TRACE_CALL(__func__);
+ gint i;
+ GArray *array;
+
+ for (i = 0; i < channels->len; i++) {
+ array = g_ptr_array_index(channels, i);
+ remmina_tp_channel_handler_new(account_path, connection_path,
+ (const gchar*)g_value_get_boxed(&g_array_index(array, GValue, 0)),
+ (GHashTable*)g_value_get_boxed(&g_array_index(array, GValue, 1)), context);
+ }
+}
+
+static void remmina_tp_handler_iface_init(gpointer g_iface, gpointer iface_data)
+{
+ TRACE_CALL(__func__);
+ TpSvcClientHandlerClass *klass = (TpSvcClientHandlerClass*)g_iface;
+
+#define IMPLEMENT(x) tp_svc_client_handler_implement_ ## x(klass, remmina_tp_handler_ ## x)
+ IMPLEMENT(handle_channels);
+#undef IMPLEMENT
+}
+
+static gboolean remmina_tp_handler_register(RemminaTpHandler *handler)
+{
+ TRACE_CALL(__func__);
+ TpDBusDaemon *bus;
+ GError *error = NULL;
+
+ bus = tp_dbus_daemon_dup(&error);
+ if (bus == NULL) {
+ g_print("tp_dbus_daemon_dup: %s", error->message);
+ return FALSE;
+ }
+ if (!tp_dbus_daemon_request_name(bus, REMMINA_TP_BUS_NAME, FALSE, &error)) {
+ g_object_unref(bus);
+ g_print("tp_dbus_daemon_request_name: %s", error->message);
+ return FALSE;
+ }
+ dbus_g_connection_register_g_object(
+ tp_proxy_get_dbus_connection(TP_PROXY(bus)),
+ REMMINA_TP_OBJECT_PATH, G_OBJECT(handler));
+ g_object_unref(bus);
+ g_print("%s: bus_name " REMMINA_TP_BUS_NAME
+ " object_path " REMMINA_TP_OBJECT_PATH "\n", __func__);
+ return TRUE;
+}
+
+RemminaTpHandler*
+remmina_tp_handler_new(void)
+{
+ TRACE_CALL(__func__);
+ RemminaTpHandler *handler;
+
+ handler = REMMINA_TP_HANDLER(g_object_new(REMMINA_TYPE_TP_HANDLER, NULL));
+ remmina_tp_handler_register(handler);
+ return handler;
+}
+
diff --git a/plugins/telepathy/telepathy_handler.h b/plugins/telepathy/telepathy_handler.h
new file mode 100644
index 0000000..c4bfb69
--- /dev/null
+++ b/plugins/telepathy/telepathy_handler.h
@@ -0,0 +1,57 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010 Vic Lee
+ * Copyright (C) 2017-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+G_BEGIN_DECLS
+
+#define REMMINA_TYPE_TP_HANDLER (remmina_tp_handler_get_type())
+#define REMMINA_TP_HANDLER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_TP_HANDLER, RemminaTpHandler))
+#define REMMINA_TP_HANDLER_CLASS(obj) (G_TYPE_CHECK_CLASS_CAST((obj), REMMINA_TYPE_TP_HANDLER, RemminaTpHandlerClass))
+#define REMMINA_IS_TP_HANDLER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_TP_HANDLER))
+#define REMMINA_IS_TP_HANDLER_CLASS(obj) (G_TYPE_CHECK_CLASS_TYPE((obj), REMMINA_TYPE_TP_HANDLER))
+#define REMMINA_TP_HANDLER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_TP_HANDLER, RemminaTpHandlerClass))
+
+typedef struct _RemminaTpHandler {
+ GObject parent;
+} RemminaTpHandler;
+
+typedef struct _RemminaTpHandlerClass {
+ GObjectClass parent_class;
+} RemminaTpHandlerClass;
+
+RemminaTpHandler *remmina_tp_handler_new(void);
+
+G_END_DECLS
diff --git a/plugins/telepathy/telepathy_plugin.c b/plugins/telepathy/telepathy_plugin.c
new file mode 100644
index 0000000..263096b
--- /dev/null
+++ b/plugins/telepathy/telepathy_plugin.c
@@ -0,0 +1,77 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "common/remmina_plugin.h"
+#include "telepathy_handler.h"
+
+RemminaPluginService *remmina_plugin_telepathy_service = NULL;
+
+static RemminaTpHandler *remmina_tp_handler = NULL;
+
+void remmina_plugin_telepathy_entry(void)
+{
+ TRACE_CALL(__func__);
+ if (remmina_tp_handler == NULL) {
+ remmina_tp_handler = remmina_tp_handler_new();
+ }
+}
+
+/* Entry plugin definition and features */
+static RemminaEntryPlugin remmina_plugin_telepathy =
+{
+ REMMINA_PLUGIN_TYPE_ENTRY, // Type
+ "telepathy", // Name
+ N_("Telepathy - Desktop Sharing"), // Description
+ GETTEXT_PACKAGE, // Translation domain
+ VERSION, // Version number
+ remmina_plugin_telepathy_entry // Plugin entry function
+};
+
+G_MODULE_EXPORT gboolean
+remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_telepathy_service = service;
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+ if (!service->register_plugin((RemminaPlugin*)&remmina_plugin_telepathy)) {
+ return FALSE;
+ }
+ return TRUE;
+}
+
diff --git a/plugins/tool_hello_world/CMakeLists.txt b/plugins/tool_hello_world/CMakeLists.txt
new file mode 100644
index 0000000..e2c9537
--- /dev/null
+++ b/plugins/tool_hello_world/CMakeLists.txt
@@ -0,0 +1,55 @@
+# remmina-plugin-tool_hello_world - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. If you
+# do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+
+set(REMMINA_PLUGIN_SRCS
+ plugin_config.h
+ plugin.c
+)
+
+add_library(remmina-plugin-tool_hello_world MODULE ${REMMINA_PLUGIN_SRCS})
+set_target_properties(remmina-plugin-tool_hello_world PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-tool_hello_world PROPERTIES NO_SONAME 1)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-tool_hello_world ${REMMINA_COMMON_LIBRARIES})
+
+install(TARGETS remmina-plugin-tool_hello_world DESTINATION ${REMMINA_PLUGINDIR})
+
+install(FILES
+ scalable/emblems/org.remmina.Remmina-tool-symbolic.svg
+ DESTINATION ${APPICONSCALE_EMBLEMS_DIR})
+
+if(WITH_ICON_CACHE)
+ gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor")
+endif()
diff --git a/plugins/tool_hello_world/plugin.c b/plugins/tool_hello_world/plugin.c
new file mode 100644
index 0000000..b852fd0
--- /dev/null
+++ b/plugins/tool_hello_world/plugin.c
@@ -0,0 +1,130 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "plugin_config.h"
+
+#include "common/remmina_plugin.h"
+
+static RemminaPluginService *remmina_plugin_service = NULL;
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__)
+
+static void remmina_plugin_tool_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("[%s] Plugin init", PLUGIN_NAME);
+}
+
+static gboolean remmina_plugin_tool_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("[%s] Plugin open connection", PLUGIN_NAME);
+
+ GtkDialog *dialog;
+ dialog = GTK_DIALOG(gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR,
+ GTK_MESSAGE_INFO, GTK_BUTTONS_OK, PLUGIN_DESCRIPTION));
+ gtk_dialog_run(dialog);
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+ return FALSE;
+}
+
+static gboolean remmina_plugin_tool_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("[%s] Plugin close connection", PLUGIN_NAME);
+ remmina_plugin_service->protocol_plugin_emit_signal(gp, "disconnect");
+ return FALSE;
+}
+
+/* Array of RemminaProtocolSetting for basic settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting tooltip
+ * g) Validation data pointer, will be passed to the validation callback method.
+ * h) Validation callback method (Can be NULL. Every entry will be valid then.)
+ * use following prototype:
+ * gboolean mysetting_validator_method(gpointer key, gpointer value,
+ * gpointer validator_data);
+ * gpointer key is a gchar* containing the setting's name,
+ * gpointer value contains the value which should be validated,
+ * gpointer validator_data contains your passed data.
+ */
+static const RemminaProtocolSetting remmina_plugin_tool_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL }
+};
+
+/* Protocol plugin definition and features */
+static RemminaProtocolPlugin remmina_plugin = {
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ PLUGIN_NAME, // Name
+ PLUGIN_DESCRIPTION, // Description
+ GETTEXT_PACKAGE, // Translation domain
+ PLUGIN_VERSION, // Version number
+ PLUGIN_APPICON, // Icon for normal connection
+ PLUGIN_APPICON, // Icon for SSH connection
+ remmina_plugin_tool_basic_settings, // Array for basic settings
+ NULL, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type
+ NULL, // Array for available features
+ remmina_plugin_tool_init, // Plugin initialization
+ remmina_plugin_tool_open_connection, // Plugin open connection
+ remmina_plugin_tool_close_connection, // Plugin close connection
+ NULL, // Query for available features
+ NULL, // Call a feature
+ NULL, // Send a keystroke
+ NULL, // No screenshot support available
+ NULL, // RCW map event
+ NULL // RCW unmap event
+};
+
+G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_service = service;
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+ if (!service->register_plugin((RemminaPlugin*)&remmina_plugin)) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/plugins/tool_hello_world/plugin_config.h b/plugins/tool_hello_world/plugin_config.h
new file mode 100644
index 0000000..6742460
--- /dev/null
+++ b/plugins/tool_hello_world/plugin_config.h
@@ -0,0 +1,43 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#ifndef __PLUGIN_CONFIG_H
+#define __PLUGIN_CONFIG_H
+
+#define PLUGIN_NAME "HELLO"
+#define PLUGIN_DESCRIPTION N_("Hello, World!")
+#define PLUGIN_VERSION "1.0"
+#define PLUGIN_APPICON "org.remmina.Remmina-tool-symbolic"
+#endif
diff --git a/plugins/tool_hello_world/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg b/plugins/tool_hello_world/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg
new file mode 100644
index 0000000..a1b1987
--- /dev/null
+++ b/plugins/tool_hello_world/scalable/emblems/org.remmina.Remmina-tool-symbolic.svg
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="24"
+ height="24"
+ viewBox="0 0 24 24"
+ version="1.1"
+ id="svg8"
+ inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
+ sodipodi:docname="org.remmina.Remmina-tool-symbolic.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="16.789507"
+ inkscape:cx="23.288356"
+ inkscape:cy="20.876135"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="995"
+ inkscape:window-x="1080"
+ inkscape:window-y="427"
+ inkscape:window-maximized="1"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:deskcolor="#d1d1d1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-171.36869"
+ originy="-105.18186" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-171.36866,-165.16877)">
+ <path
+ d="m 187.92458,165.16877 v 0.20535 l -7.92231,10.6258 c 0.2293,0.0655 0.45497,0.14268 0.67606,0.23147 -0.78468,-0.31266 -1.63838,-0.49451 -2.53674,-0.49451 -1.79575,0 -3.51692,0.70485 -4.78578,1.95455 -0.35794,0.35256 -0.67437,0.74332 -0.94373,1.1652 l 3.15125,3.09956 -2.1018,2.81919 c 0.50277,0.9546 1.29492,1.73163 2.26642,2.22354 l 2.8633,-2.06448 3.15416,3.10242 c 0.42818,-0.2659 0.82449,-0.57809 1.18172,-0.93087 1.27054,-1.24804 1.98546,-2.94122 1.98715,-4.70728 0,-0.86035 -0.18006,-1.67679 -0.482,-2.43174 0.0821,0.1961 0.15471,0.39575 0.21747,0.5985 l 10.80034,-7.99464 z m -12.17009,0.3326 -4.25646,4.1895 5.52629,5.43302 4.2567,-4.18665 z m 14.0397,13.80941 -4.25671,4.1895 5.52653,5.43587 4.2567,-4.18974 z m -17.2998,6.6616 -1.12573,1.62789 1.50491,1.48046 1.5346,-1.1075 c -0.76007,-0.49428 -1.41103,-1.13456 -1.91378,-1.88215 z"
+ id="path32"
+ inkscape:connector-curvature="0"
+ style="isolation:isolate;fill:#171717;stroke-width:0.239377" />
+ </g>
+</svg>
diff --git a/plugins/vnc/CMakeLists.txt b/plugins/vnc/CMakeLists.txt
new file mode 100644
index 0000000..eccc9d8
--- /dev/null
+++ b/plugins/vnc/CMakeLists.txt
@@ -0,0 +1,56 @@
+# remmina-plugin-vnc - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2011 Marc-Andre Moreau
+# Copyright (C) 2014-2023 Antenore Gatta, Giovanni Panozzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. If you
+# do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+
+set(REMMINA_PLUGIN_VNC_SRCS
+ vnc_plugin.c
+ vnc_plugin.h
+)
+
+add_library(remmina-plugin-vnc MODULE ${REMMINA_PLUGIN_VNC_SRCS})
+set_target_properties(remmina-plugin-vnc PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-vnc PROPERTIES NO_SONAME 1)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${LIBVNCSERVER_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-vnc ${REMMINA_COMMON_LIBRARIES} ${LIBVNCSERVER_LIBRARIES})
+
+install(TARGETS remmina-plugin-vnc DESTINATION ${REMMINA_PLUGINDIR})
+
+install(FILES
+ scalable/emblems/org.remmina.Remmina-vnc-ssh-symbolic.svg
+ scalable/emblems/org.remmina.Remmina-vnc-symbolic.svg
+ DESTINATION ${APPICONSCALE_EMBLEMS_DIR})
+
+if(WITH_ICON_CACHE)
+ gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor")
+endif()
diff --git a/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-ssh-symbolic.svg b/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-ssh-symbolic.svg
new file mode 100644
index 0000000..6c1da59
--- /dev/null
+++ b/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-ssh-symbolic.svg
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="99.772003"
+ height="99.772079"
+ viewBox="0 0 26.39801 26.398028"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="vnc over ssh.svg">
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1.0493442"
+ inkscape:cx="-11.459557"
+ inkscape:cy="109.76653"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1366"
+ inkscape:window-height="715"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-162.7958"
+ originy="-83.119086" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-162.79579,-187.48289)">
+ <path
+ d="m 175.99485,195.20186 c -1.73487,0 -3.13161,1.42611 -3.13161,3.18029 v 1.51686 h -0.7829 c -0.43365,0 -0.7829,0.34925 -0.7829,0.7829 v 4.69715 c 0,0.43365 0.34925,0.7829 0.7829,0.7829 h 7.82875 c 0.43366,0 0.78291,-0.34925 0.78291,-0.7829 v -4.69715 c 0,-0.43365 -0.34925,-0.7829 -0.78291,-0.7829 h -0.7829 v -1.51686 c 0,-1.75365 -1.39673,-3.18029 -3.13134,-3.18029 z m 0,1.56581 c 0.8673,0 1.56554,0.69823 1.56554,1.56554 v 1.5658 h -3.13135 v -1.5658 c 0,-0.86731 0.69824,-1.56554 1.56581,-1.56554 z"
+ id="path34"
+ inkscape:connector-curvature="0"
+ style="isolation:isolate;fill:#171717;stroke-width:0.26458332" />
+ <path
+ d="m 187.30817,213.88092 c 0,0 0.14367,-0.008 0.34237,-0.0442 0.38523,-0.0569 0.67627,-0.195 0.89138,-0.38312 0.0415,-0.0317 0.082,-0.0661 0.12144,-0.10319 0.037,-0.0394 0.0712,-0.0796 0.10319,-0.12144 0.18785,-0.21484 0.32623,-0.50615 0.38285,-0.89112 0.0368,-0.1987 0.0444,-0.34263 0.0444,-0.34263 v -22.62691 c 0,-1.88542 -1.88568,-1.88542 -1.88568,-1.88542 h -22.6269 c 0,0 -1.88543,0 -1.88543,1.88542 v 22.6269 c 0,0 0.008,0.14393 0.0442,0.34264 0.0585,0.38497 0.19447,0.67627 0.38285,0.89111 0.032,0.0418 0.0675,0.082 0.10319,0.12145 0.0397,0.037 0.0802,0.0714 0.12144,0.10318 0.21511,0.18812 0.50668,0.32624 0.89138,0.38312 0.19976,0.0363 0.34238,0.0442 0.34238,0.0442 z m -1.88569,-4.5347 h -18.85553 v -18.09221 h 18.85553 z"
+ id="path36"
+ inkscape:connector-curvature="0"
+ style="isolation:isolate;fill:#171717;stroke-width:0.26458332" />
+ </g>
+</svg>
diff --git a/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-symbolic.svg b/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-symbolic.svg
new file mode 100644
index 0000000..060a62a
--- /dev/null
+++ b/plugins/vnc/scalable/emblems/org.remmina.Remmina-vnc-symbolic.svg
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100"
+ height="100"
+ viewBox="0 0 26.458335 26.458332"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="vnc protocol.svg">
+ <defs
+ id="defs2">
+ <mask
+ id="_mask_t2Us8opi94eYjaS69EfyEcHSNVQ03DDZ"
+ x="-200%"
+ y="-200%"
+ width="400%"
+ height="400%">
+ <rect
+ x="0"
+ y="0"
+ width="100%"
+ height="100%"
+ style="fill:#ffffff"
+ id="rect40" />
+ <path
+ d="m 850.286,202.852 c 0,0 0.452,-0.025 1.077,-0.139 1.211,-0.179 2.128,-0.614 2.804,-1.205 0.13,-0.1 0.257,-0.208 0.382,-0.324 0.116,-0.125 0.224,-0.251 0.324,-0.382 0.592,-0.677 1.026,-1.593 1.205,-2.804 0.115,-0.625 0.139,-1.077 0.139,-1.077 V 125.75 c 0,-5.931 -5.931,-5.931 -5.931,-5.931 h -71.171 c 0,0 -5.931,0 -5.931,5.931 v 71.171 c 0,0 0.026,0.452 0.139,1.077 0.184,1.211 0.612,2.127 1.205,2.804 0.101,0.131 0.212,0.257 0.324,0.382 0.125,0.116 0.252,0.224 0.383,0.324 0.676,0.591 1.593,1.026 2.803,1.205 0.628,0.114 1.077,0.139 1.077,0.139 z m -5.931,-14.263 h -59.309 v -56.908 h 59.309 z"
+ id="path42"
+ inkscape:connector-curvature="0"
+ style="fill:#000000;stroke:none" />
+ </mask>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1.4839967"
+ inkscape:cx="-65.390539"
+ inkscape:cy="-17.020138"
+ inkscape:document-units="px"
+ inkscape:current-layer="g820"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="true"
+ inkscape:window-width="1366"
+ inkscape:window-height="715"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-146.65307"
+ originy="-135.56714" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ <cc:license
+ rdf:resource="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-146.65306,-134.97453)">
+ <g
+ id="g820"
+ transform="matrix(1.0697634,0,0,1.0697634,-9.2735492,-11.925523)">
+ <path
+ inkscape:connector-curvature="0"
+ id="path38"
+ d="m 147.83075,137.94029 c 0,0 -1.17769,2e-5 -1.17769,1.17769 v 14.13263 c 0,0 0.005,0.09 0.0274,0.21405 0.0365,0.24056 0.12144,0.42255 0.23928,0.55686 0.02,0.0261 0.0421,0.051 0.0644,0.0757 0.0248,0.023 0.0503,0.0445 0.0761,0.0644 0.13431,0.11739 0.31608,0.20363 0.55643,0.23928 0.12474,0.0227 0.21405,0.0278 0.21405,0.0278 h 4.62807 v -2.83218 H 149.0084 V 140.2961 h 11.77727 v 4.05511 h 2.35536 v -5.23323 c 0,-1.17767 -1.17768,-1.17769 -1.17768,-1.17769 z m 12.95495,10.06883 v 3.58743 h -4.66941 v 2.83218 h 5.84709 c 0,0 0.09,-0.005 0.21405,-0.0278 0.24056,-0.0356 0.4221,-0.1219 0.55643,-0.23928 0.0258,-0.0199 0.0514,-0.0414 0.0761,-0.0644 0.023,-0.0247 0.0445,-0.0497 0.0644,-0.0757 0.11738,-0.13431 0.20386,-0.3163 0.23927,-0.55686 0.023,-0.12408 0.0274,-0.21405 0.0274,-0.21405 v -5.24149 z"
+ style="isolation:isolate;fill:#171717;stroke-width:0.22274615" />
+ <path
+ style="isolation:isolate;fill:#171717;stroke-width:0.22274615"
+ inkscape:connector-curvature="0"
+ id="path38-5"
+ d="m 168.41828,161.43286 c 0,0 0.0898,-0.005 0.21384,-0.0276 0.24056,-0.0356 0.42232,-0.12185 0.55664,-0.23923 0.0258,-0.0199 0.0513,-0.0414 0.0759,-0.0644 0.0229,-0.0248 0.0445,-0.0499 0.0644,-0.0759 0.11739,-0.13432 0.20381,-0.31608 0.23923,-0.55665 0.023,-0.12407 0.0276,-0.21405 0.0276,-0.21405 v -14.13259 c 0,-1.17766 -1.17767,-1.17766 -1.17767,-1.17766 h -14.13279 c 0,0 -1.17765,0 -1.17765,1.17766 v 14.13258 c 0,0 0.005,0.09 0.0276,0.21406 0.0365,0.24057 0.1214,0.42233 0.23924,0.55664 0.02,0.0261 0.0421,0.0512 0.0644,0.0759 0.0248,0.023 0.0501,0.0445 0.0759,0.0644 0.13432,0.11738 0.3163,0.20359 0.55664,0.23923 0.12474,0.0227 0.21384,0.0276 0.21384,0.0276 z m -1.17788,-2.83222 v -11.30036 h -11.77704 11.77704 z"
+ sodipodi:nodetypes="cccccccsccsccccccccccccc" />
+ </g>
+ </g>
+</svg>
diff --git a/plugins/vnc/vnc_plugin.c b/plugins/vnc/vnc_plugin.c
new file mode 100644
index 0000000..633a2a9
--- /dev/null
+++ b/plugins/vnc/vnc_plugin.c
@@ -0,0 +1,2228 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010-2011 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-2022 Antenore Gatta, Giovanni Panozzo
+ * Copyright (C) 2022-2023 Antenore Gatta, Giovanni Panozzo, Hiroyuki Tanaka
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include <gmodule.h>
+#include "vnc_plugin.h"
+#include <rfb/rfbclient.h>
+
+#define REMMINA_PLUGIN_VNC_FEATURE_PREF_QUALITY 1
+#define REMMINA_PLUGIN_VNC_FEATURE_PREF_VIEWONLY 2
+#define REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT 3
+#define REMMINA_PLUGIN_VNC_FEATURE_TOOL_REFRESH 4
+#define REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT 5
+#define REMMINA_PLUGIN_VNC_FEATURE_SCALE 6
+#define REMMINA_PLUGIN_VNC_FEATURE_UNFOCUS 7
+#define REMMINA_PLUGIN_VNC_FEATURE_TOOL_SENDCTRLALTDEL 8
+#define REMMINA_PLUGIN_VNC_FEATURE_PREF_COLOR 9
+#define REMMINA_PLUGIN_VNC_FEATURE_DYNRESUPDATE 10
+
+#define VNC_DEFAULT_PORT 5900
+
+#define GET_PLUGIN_DATA(gp) (RemminaPluginVncData *)g_object_get_data(G_OBJECT(gp), "plugin-data")
+
+static RemminaPluginService *remmina_plugin_service = NULL;
+
+static int dot_cursor_x_hot = 2;
+static int dot_cursor_y_hot = 2;
+static const gchar *dot_cursor_xpm[] =
+{ "5 5 3 1", " c None", ". c #000000", "+ c #FFFFFF", " ... ", ".+++.", ".+ +.", ".+++.", " ... " };
+
+
+#define LOCK_BUFFER(t) if (t) { CANCEL_DEFER } pthread_mutex_lock(&gpdata->buffer_mutex);
+#define UNLOCK_BUFFER(t) pthread_mutex_unlock(&gpdata->buffer_mutex); if (t) { CANCEL_ASYNC }
+
+struct onMainThread_cb_data {
+ enum { FUNC_UPDATE_SCALE } func;
+ GtkWidget * widget;
+ gint x, y, width, height;
+ RemminaProtocolWidget * gp;
+ gboolean scale;
+
+ /* Mutex for thread synchronization */
+ pthread_mutex_t mu;
+ /* Flag to catch cancellations */
+ gboolean cancelled;
+};
+
+static gboolean onMainThread_cb(struct onMainThread_cb_data *d)
+{
+ TRACE_CALL(__func__);
+ if (!d->cancelled) {
+ switch (d->func) {
+ case FUNC_UPDATE_SCALE:
+ remmina_plugin_vnc_update_scale(d->gp, d->scale);
+ break;
+ }
+ pthread_mutex_unlock(&d->mu);
+ } else {
+ /* thread has been cancelled, so we must free d memory here */
+ g_free(d);
+ }
+ return G_SOURCE_REMOVE;
+}
+
+
+static void onMainThread_cleanup_handler(gpointer data)
+{
+ TRACE_CALL(__func__);
+ struct onMainThread_cb_data *d = data;
+
+ d->cancelled = TRUE;
+}
+
+
+static void onMainThread_schedule_callback_and_wait(struct onMainThread_cb_data *d)
+{
+ TRACE_CALL(__func__);
+ d->cancelled = FALSE;
+ pthread_cleanup_push(onMainThread_cleanup_handler, d);
+ pthread_mutex_init(&d->mu, NULL);
+ pthread_mutex_lock(&d->mu);
+ gdk_threads_add_idle((GSourceFunc)onMainThread_cb, (gpointer)d);
+
+ pthread_mutex_lock(&d->mu);
+
+ pthread_cleanup_pop(0);
+ pthread_mutex_unlock(&d->mu);
+ pthread_mutex_destroy(&d->mu);
+}
+
+/**
+ * Function check_for_endianness() returns 1, if architecture
+ * is little endian, 0 in case of big endian.
+ */
+static gboolean check_for_endianness()
+{
+ unsigned int x = 1;
+ char *c = (char *)&x;
+
+ return (int)*c;
+}
+
+static void remmina_plugin_vnc_event_push(RemminaProtocolWidget *gp, gint event_type, gpointer p1, gpointer p2, gpointer p3)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaPluginVncEvent *event;
+
+ event = g_new(RemminaPluginVncEvent, 1);
+ event->event_type = event_type;
+ switch (event_type) {
+ case REMMINA_PLUGIN_VNC_EVENT_KEY:
+ event->event_data.key.keyval = GPOINTER_TO_UINT(p1);
+ event->event_data.key.pressed = GPOINTER_TO_INT(p2);
+ break;
+ case REMMINA_PLUGIN_VNC_EVENT_POINTER:
+ event->event_data.pointer.x = GPOINTER_TO_INT(p1);
+ event->event_data.pointer.y = GPOINTER_TO_INT(p2);
+ event->event_data.pointer.button_mask = GPOINTER_TO_INT(p3);
+ break;
+ case REMMINA_PLUGIN_VNC_EVENT_CUTTEXT:
+ case REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND:
+ event->event_data.text.text = g_strdup((char *)p1);
+ break;
+ default:
+ break;
+ }
+
+ pthread_mutex_lock(&gpdata->vnc_event_queue_mutex);
+ g_queue_push_tail(gpdata->vnc_event_queue, event);
+ pthread_mutex_unlock(&gpdata->vnc_event_queue_mutex);
+
+ if (write(gpdata->vnc_event_pipe[1], "\0", 1)) {
+ /* Ignore */
+ }
+}
+
+static void remmina_plugin_vnc_event_free(RemminaPluginVncEvent *event)
+{
+ TRACE_CALL(__func__);
+ switch (event->event_type) {
+ case REMMINA_PLUGIN_VNC_EVENT_CUTTEXT:
+ case REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND:
+ g_free(event->event_data.text.text);
+ break;
+ default:
+ break;
+ }
+ g_free(event);
+}
+
+static void remmina_plugin_vnc_event_free_all(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaPluginVncEvent *event;
+
+ /* This is called from main thread after plugin thread has
+ * been closed, so no queue locking is necessary here */
+ while ((event = g_queue_pop_head(gpdata->vnc_event_queue)) != NULL)
+ remmina_plugin_vnc_event_free(event);
+}
+
+static void remmina_plugin_vnc_scale_area(RemminaProtocolWidget *gp, gint *x, gint *y, gint *w, gint *h)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ GtkAllocation widget_allocation;
+ gint width, height;
+ gint sx, sy, sw, sh;
+
+ if (gpdata->rgb_buffer == NULL)
+ return;
+
+ width = remmina_plugin_service->protocol_plugin_get_width(gp);
+ height = remmina_plugin_service->protocol_plugin_get_height(gp);
+
+ gtk_widget_get_allocation(GTK_WIDGET(gp), &widget_allocation);
+
+ if (widget_allocation.width == width && widget_allocation.height == height)
+ return; /* Same size, no scaling */
+
+ /* We have to extend the scaled region 2 scaled pixels, to avoid gaps */
+ sx = MIN(MAX(0, (*x) * widget_allocation.width / width - widget_allocation.width / width - 2), widget_allocation.width - 1);
+ sy = MIN(MAX(0, (*y) * widget_allocation.height / height - widget_allocation.height / height - 2), widget_allocation.height - 1);
+ sw = MIN(widget_allocation.width - sx, (*w) * widget_allocation.width / width + widget_allocation.width / width + 4);
+ sh = MIN(widget_allocation.height - sy, (*h) * widget_allocation.height / height + widget_allocation.height / height + 4);
+
+ *x = sx;
+ *y = sy;
+ *w = sw;
+ *h = sh;
+}
+
+static void remmina_plugin_vnc_update_scale(RemminaProtocolWidget *gp, gboolean scale)
+{
+ TRACE_CALL(__func__);
+ /* This function can be called from a non main thread */
+
+ RemminaPluginVncData *gpdata;
+ gint width, height;
+
+ if (!remmina_plugin_service->is_main_thread()) {
+ struct onMainThread_cb_data *d;
+ d = (struct onMainThread_cb_data *)g_malloc(sizeof(struct onMainThread_cb_data));
+ d->func = FUNC_UPDATE_SCALE;
+ d->gp = gp;
+ d->scale = scale;
+ onMainThread_schedule_callback_and_wait(d);
+ g_free(d);
+ return;
+ }
+
+ gpdata = GET_PLUGIN_DATA(gp);
+
+ width = remmina_plugin_service->protocol_plugin_get_width(gp);
+ height = remmina_plugin_service->protocol_plugin_get_height(gp);
+ if (scale)
+ /* In scaled mode, drawing_area will get its dimensions from its parent */
+ gtk_widget_set_size_request(GTK_WIDGET(gpdata->drawing_area), -1, -1);
+ else
+ /* In non scaled mode, the plugins forces dimensions of drawing area */
+ gtk_widget_set_size_request(GTK_WIDGET(gpdata->drawing_area), width, height);
+
+ remmina_plugin_service->protocol_plugin_update_align(gp);
+}
+
+gboolean remmina_plugin_vnc_setcursor(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ GdkCursor *cur;
+
+ LOCK_BUFFER(FALSE);
+ gpdata->queuecursor_handler = 0;
+
+ if (gpdata->queuecursor_surface) {
+ cur = gdk_cursor_new_from_surface(gdk_display_get_default(), gpdata->queuecursor_surface, gpdata->queuecursor_x,
+ gpdata->queuecursor_y);
+ gdk_window_set_cursor(gtk_widget_get_window(gpdata->drawing_area), cur);
+ g_object_unref(cur);
+ cairo_surface_destroy(gpdata->queuecursor_surface);
+ gpdata->queuecursor_surface = NULL;
+ } else {
+ gdk_window_set_cursor(gtk_widget_get_window(gpdata->drawing_area), NULL);
+ }
+ UNLOCK_BUFFER(FALSE);
+
+ return FALSE;
+}
+
+static void remmina_plugin_vnc_queuecursor(RemminaProtocolWidget *gp, cairo_surface_t *surface, gint x, gint y)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (gpdata->queuecursor_surface)
+ cairo_surface_destroy(gpdata->queuecursor_surface);
+ gpdata->queuecursor_surface = surface;
+ gpdata->queuecursor_x = x;
+ gpdata->queuecursor_y = y;
+ if (!gpdata->queuecursor_handler)
+ gpdata->queuecursor_handler = IDLE_ADD((GSourceFunc)remmina_plugin_vnc_setcursor, gp);
+}
+
+typedef struct _RemminaKeyVal {
+ guint keyval;
+ guint16 keycode;
+} RemminaKeyVal;
+
+/***************************** LibVNCClient related codes *********************************/
+
+static const uint32_t remmina_plugin_vnc_no_encrypt_auth_types[] =
+{ rfbNoAuth, rfbVncAuth, rfbMSLogon, 0 };
+
+static RemminaPluginVncEvent *remmina_plugin_vnc_event_queue_pop_head(RemminaPluginVncData *gpdata)
+{
+ RemminaPluginVncEvent *event;
+
+ CANCEL_DEFER;
+ pthread_mutex_lock(&gpdata->vnc_event_queue_mutex);
+
+ event = g_queue_pop_head(gpdata->vnc_event_queue);
+
+ pthread_mutex_unlock(&gpdata->vnc_event_queue_mutex);
+ CANCEL_ASYNC;
+
+ return event;
+}
+
+static void remmina_plugin_vnc_process_vnc_event(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncEvent *event;
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ rfbClient *cl;
+ gchar buf[100];
+
+ cl = (rfbClient *)gpdata->client;
+ while ((event = remmina_plugin_vnc_event_queue_pop_head(gpdata)) != NULL) {
+ if (cl) {
+ switch (event->event_type) {
+ case REMMINA_PLUGIN_VNC_EVENT_KEY:
+ SendKeyEvent(cl, event->event_data.key.keyval, event->event_data.key.pressed);
+ break;
+ case REMMINA_PLUGIN_VNC_EVENT_POINTER:
+ SendPointerEvent(cl, event->event_data.pointer.x, event->event_data.pointer.y,
+ event->event_data.pointer.button_mask);
+ break;
+ case REMMINA_PLUGIN_VNC_EVENT_CUTTEXT:
+ if (event->event_data.text.text) {
+ rfbClientLog("sending clipboard text '%s'\n", event->event_data.text.text);
+ SendClientCutText(cl, event->event_data.text.text, strlen(event->event_data.text.text));
+ }
+ break;
+ case REMMINA_PLUGIN_VNC_EVENT_CHAT_OPEN:
+ TextChatOpen(cl);
+ break;
+ case REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND:
+ TextChatSend(cl, event->event_data.text.text);
+ break;
+ case REMMINA_PLUGIN_VNC_EVENT_CHAT_CLOSE:
+ TextChatClose(cl);
+ TextChatFinish(cl);
+ break;
+ default:
+ rfbClientLog("Ignoring VNC event: 0x%x\n", event->event_type);
+ break;
+ }
+ }
+ remmina_plugin_vnc_event_free(event);
+ }
+ if (read(gpdata->vnc_event_pipe[0], buf, sizeof(buf))) {
+ /* Ignore */
+ }
+}
+
+typedef struct _RemminaPluginVncCuttextParam {
+ RemminaProtocolWidget * gp;
+ gchar * text;
+ gint textlen;
+} RemminaPluginVncCuttextParam;
+
+static void remmina_plugin_vnc_update_quality(rfbClient *cl, gint quality)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL);
+ RemminaFile *remminafile;
+ gchar *enc = NULL;
+
+ /**
+ * "0", "Poor (fastest)"
+ * "1", "Medium"
+ * "2", "Good"
+ * "9", "Best (slowest)"
+ */
+ switch (quality) {
+ case 9:
+ cl->appData.useBGR233 = 0;
+ cl->appData.encodingsString = "copyrect zlib hextile raw";
+ cl->appData.compressLevel = 1;
+ cl->appData.qualityLevel = 9;
+ break;
+ case 2:
+ cl->appData.useBGR233 = 0;
+ cl->appData.encodingsString = "tight zrle ultra copyrect hextile zlib corre rre raw";
+ cl->appData.compressLevel = 2;
+ cl->appData.qualityLevel = 7;
+ break;
+ case 1:
+ cl->appData.useBGR233 = 0;
+ cl->appData.encodingsString = "tight zrle ultra copyrect hextile zlib corre rre raw";
+ cl->appData.compressLevel = 3;
+ cl->appData.qualityLevel = 5;
+ break;
+ case 0:
+ default:
+ // bpp8 and tight encoding is not supported in libvnc
+ cl->appData.useBGR233 = 1;
+ cl->appData.encodingsString = "copyrect zrle ultra zlib hextile corre rre raw";
+ cl->appData.qualityLevel = 1;
+ break;
+ }
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ enc = g_strdup(remmina_plugin_service->file_get_string(remminafile, "encodings"));
+ if (enc) {
+ cl->appData.encodingsString = g_strdup(enc);
+ g_free (enc), enc = NULL;
+ }
+ gboolean tight = remmina_plugin_service->file_get_int (remminafile, "tightencoding", FALSE);
+ if (tight) {
+ if (!g_strrstr ( g_strdup(cl->appData.encodingsString), "tight\0")) {
+ cl->appData.encodingsString = g_strdup_printf("%s %s", "tight", g_strdup(cl->appData.encodingsString));
+ }
+ }
+
+ REMMINA_PLUGIN_DEBUG("Quality: %d", quality);
+ REMMINA_PLUGIN_DEBUG("Encodings: %s", cl->appData.encodingsString);
+}
+
+static void remmina_plugin_vnc_update_colordepth(rfbClient *cl, gint colordepth)
+{
+ TRACE_CALL(__func__);
+
+ cl->format.depth = colordepth;
+ cl->appData.requestedDepth = colordepth;
+
+ cl->format.trueColour = 1;
+ cl->format.bigEndian = check_for_endianness()?FALSE:TRUE;
+
+ switch (colordepth) {
+ case 8:
+ cl->format.depth = 8;
+ cl->format.bitsPerPixel = 8;
+ cl->format.blueMax = 3;
+ cl->format.blueShift = 6;
+ cl->format.greenMax = 7;
+ cl->format.greenShift = 3;
+ cl->format.redMax = 7;
+ cl->format.redShift = 0;
+ break;
+ case 16:
+ cl->format.depth = 15;
+ cl->format.bitsPerPixel = 16;
+ cl->format.redShift = 11;
+ cl->format.greenShift = 6;
+ cl->format.blueShift = 1;
+ cl->format.redMax = 31;
+ cl->format.greenMax = 31;
+ cl->format.blueMax = 31;
+ break;
+ case 32:
+ default:
+ cl->format.depth = 24;
+ cl->format.bitsPerPixel = 32;
+ cl->format.blueShift = 0;
+ cl->format.redShift = 16;
+ cl->format.greenShift = 8;
+ cl->format.blueMax = 0xff;
+ cl->format.redMax = 0xff;
+ cl->format.greenMax = 0xff;
+ break;
+ }
+
+ rfbClientLog("colordepth = %d\n", colordepth);
+ rfbClientLog("format.depth = %d\n", cl->format.depth);
+ rfbClientLog("format.bitsPerPixel = %d\n", cl->format.bitsPerPixel);
+ rfbClientLog("format.blueShift = %d\n", cl->format.blueShift);
+ rfbClientLog("format.redShift = %d\n", cl->format.redShift);
+ rfbClientLog("format.trueColour = %d\n", cl->format.trueColour);
+ rfbClientLog("format.greenShift = %d\n", cl->format.greenShift);
+ rfbClientLog("format.blueMax = %d\n", cl->format.blueMax);
+ rfbClientLog("format.redMax = %d\n", cl->format.redMax);
+ rfbClientLog("format.greenMax = %d\n", cl->format.greenMax);
+ rfbClientLog("format.bigEndian = %d\n", cl->format.bigEndian);
+}
+
+static rfbBool remmina_plugin_vnc_rfb_allocfb(rfbClient *cl)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ gint width, height, depth, size;
+ gboolean scale;
+ cairo_surface_t *new_surface, *old_surface;
+
+ width = cl->width;
+ height = cl->height;
+ depth = cl->format.bitsPerPixel;
+ size = width * height * (depth / 8);
+
+ new_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
+ if (cairo_surface_status(new_surface) != CAIRO_STATUS_SUCCESS)
+ return FALSE;
+ old_surface = gpdata->rgb_buffer;
+
+ LOCK_BUFFER(TRUE);
+
+ remmina_plugin_service->protocol_plugin_set_width(gp, width);
+ remmina_plugin_service->protocol_plugin_set_height(gp, height);
+
+ gpdata->rgb_buffer = new_surface;
+
+ if (gpdata->vnc_buffer)
+ g_free(gpdata->vnc_buffer);
+ gpdata->vnc_buffer = (guchar *)g_malloc(size);
+ cl->frameBuffer = gpdata->vnc_buffer;
+
+ UNLOCK_BUFFER(TRUE);
+
+ if (old_surface)
+ cairo_surface_destroy(old_surface);
+
+ scale = (remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE);
+ remmina_plugin_vnc_update_scale(gp, scale);
+
+ /* Notify window of change so that scroll border can be hidden or shown if needed */
+ remmina_plugin_service->protocol_plugin_desktop_resize(gp);
+
+ /* Refresh the client’s updateRect - bug in xvncclient */
+ cl->updateRect.w = width;
+ cl->updateRect.h = height;
+
+ return TRUE;
+}
+
+static gint remmina_plugin_vnc_bits(gint n)
+{
+ TRACE_CALL(__func__);
+ gint b = 0;
+
+ while (n) {
+ b++;
+ n >>= 1;
+ }
+ return b ? b : 1;
+}
+
+static gboolean remmina_plugin_vnc_queue_draw_area_real(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ gint x, y, w, h;
+
+ if (GTK_IS_WIDGET(gp) && gpdata->connected) {
+ LOCK_BUFFER(FALSE);
+ x = gpdata->queuedraw_x;
+ y = gpdata->queuedraw_y;
+ w = gpdata->queuedraw_w;
+ h = gpdata->queuedraw_h;
+ gpdata->queuedraw_handler = 0;
+ UNLOCK_BUFFER(FALSE);
+
+ gtk_widget_queue_draw_area(GTK_WIDGET(gp), x, y, w, h);
+ }
+ return FALSE;
+}
+
+static void remmina_plugin_vnc_queue_draw_area(RemminaProtocolWidget *gp, gint x, gint y, gint w, gint h)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ gint nx2, ny2, ox2, oy2;
+
+ LOCK_BUFFER(TRUE);
+ if (gpdata->queuedraw_handler) {
+ nx2 = x + w;
+ ny2 = y + h;
+ ox2 = gpdata->queuedraw_x + gpdata->queuedraw_w;
+ oy2 = gpdata->queuedraw_y + gpdata->queuedraw_h;
+ gpdata->queuedraw_x = MIN(gpdata->queuedraw_x, x);
+ gpdata->queuedraw_y = MIN(gpdata->queuedraw_y, y);
+ gpdata->queuedraw_w = MAX(ox2, nx2) - gpdata->queuedraw_x;
+ gpdata->queuedraw_h = MAX(oy2, ny2) - gpdata->queuedraw_y;
+ } else {
+ gpdata->queuedraw_x = x;
+ gpdata->queuedraw_y = y;
+ gpdata->queuedraw_w = w;
+ gpdata->queuedraw_h = h;
+ gpdata->queuedraw_handler = IDLE_ADD((GSourceFunc)remmina_plugin_vnc_queue_draw_area_real, gp);
+ }
+ UNLOCK_BUFFER(TRUE);
+}
+
+static void remmina_plugin_vnc_rfb_fill_buffer(rfbClient *cl, guchar *dest, gint dest_rowstride, guchar *src,
+ gint src_rowstride, guchar *mask, gint w, gint h)
+{
+ TRACE_CALL(__func__);
+ guchar *srcptr;
+ gint bytesPerPixel;
+ guint32 src_pixel;
+ gint ix, iy;
+ gint i;
+ guchar c;
+ gint rs, gs, bs, rm, gm, bm, rl, gl, bl, rr, gr, br;
+ gint r;
+ guint32 *destptr;
+
+ union {
+ struct {
+ guchar a, r, g, b;
+ } colors;
+ guint32 argb;
+ } dst_pixel;
+
+ bytesPerPixel = cl->format.bitsPerPixel / 8;
+ switch (cl->format.bitsPerPixel) {
+ case 32:
+ /* The following codes fill in the Alpha channel swap red/green value */
+ for (iy = 0; iy < h; iy++) {
+ destptr = (guint32 *)(dest + iy * dest_rowstride);
+ srcptr = src + iy * src_rowstride;
+ for (ix = 0; ix < w; ix++) {
+ if (!mask || *mask++) {
+ dst_pixel.colors.a = 0xff;
+ dst_pixel.colors.r = *(srcptr + 2);
+ dst_pixel.colors.g = *(srcptr + 1);
+ dst_pixel.colors.b = *srcptr;
+ *destptr++ = ntohl(dst_pixel.argb);
+ } else {
+ *destptr++ = 0;
+ }
+ srcptr += 4;
+ }
+ }
+ break;
+ default:
+ rm = cl->format.redMax;
+ gm = cl->format.greenMax;
+ bm = cl->format.blueMax;
+ rr = remmina_plugin_vnc_bits(rm);
+ gr = remmina_plugin_vnc_bits(gm);
+ br = remmina_plugin_vnc_bits(bm);
+ rl = 8 - rr;
+ gl = 8 - gr;
+ bl = 8 - br;
+ rs = cl->format.redShift;
+ gs = cl->format.greenShift;
+ bs = cl->format.blueShift;
+ for (iy = 0; iy < h; iy++) {
+ destptr = (guint32 *)(dest + iy * dest_rowstride);
+ srcptr = src + iy * src_rowstride;
+ for (ix = 0; ix < w; ix++) {
+ src_pixel = 0;
+ for (i = 0; i < bytesPerPixel; i++)
+ src_pixel += (*srcptr++) << (8 * i);
+
+ if (!mask || *mask++) {
+ dst_pixel.colors.a = 0xff;
+ c = (guchar)((src_pixel >> rs) & rm) << rl;
+ for (r = rr; r < 8; r *= 2)
+ c |= c >> r;
+ dst_pixel.colors.r = c;
+ c = (guchar)((src_pixel >> gs) & gm) << gl;
+ for (r = gr; r < 8; r *= 2)
+ c |= c >> r;
+ dst_pixel.colors.g = c;
+ c = (guchar)((src_pixel >> bs) & bm) << bl;
+ for (r = br; r < 8; r *= 2)
+ c |= c >> r;
+ dst_pixel.colors.b = c;
+ *destptr++ = ntohl(dst_pixel.argb);
+ } else {
+ *destptr++ = 0;
+ }
+ }
+ }
+ break;
+ }
+}
+
+static void remmina_plugin_vnc_rfb_updatefb(rfbClient *cl, int x, int y, int w, int h)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ gint bytesPerPixel;
+ gint rowstride;
+ gint width;
+
+ LOCK_BUFFER(TRUE);
+
+ if (w >= 1 || h >= 1) {
+ width = remmina_plugin_service->protocol_plugin_get_width(gp);
+ bytesPerPixel = cl->format.bitsPerPixel / 8;
+ rowstride = cairo_image_surface_get_stride(gpdata->rgb_buffer);
+ cairo_surface_flush(gpdata->rgb_buffer);
+ remmina_plugin_vnc_rfb_fill_buffer(cl, cairo_image_surface_get_data(gpdata->rgb_buffer) + y * rowstride + x * 4,
+ rowstride, gpdata->vnc_buffer + ((y * width + x) * bytesPerPixel), width * bytesPerPixel, NULL,
+ w, h);
+ cairo_surface_mark_dirty(gpdata->rgb_buffer);
+ }
+
+ if ((remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE))
+ remmina_plugin_vnc_scale_area(gp, &x, &y, &w, &h);
+
+ UNLOCK_BUFFER(TRUE);
+
+ remmina_plugin_vnc_queue_draw_area(gp, x, y, w, h);
+}
+
+static void remmina_plugin_vnc_rfb_finished(rfbClient *cl) __attribute__ ((unused));
+static void remmina_plugin_vnc_rfb_finished(rfbClient *cl)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("FinishedFrameBufferUpdate");
+}
+
+static void remmina_plugin_vnc_rfb_led_state(rfbClient *cl, int value, int pad)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Led state - value: %d, pad: %d", value, pad);
+}
+
+static gboolean remmina_plugin_vnc_queue_cuttext(RemminaPluginVncCuttextParam *param)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = param->gp;
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ GDateTime *t;
+ glong diff;
+ const char *cur_charset;
+ gchar *text;
+ gsize br, bw;
+
+ if (GTK_IS_WIDGET(gp) && gpdata->connected) {
+ t = g_date_time_new_now_utc();
+ diff = g_date_time_difference(t, gpdata->clipboard_timer) / 100000; // tenth of second
+ if (diff >= 10) {
+ g_date_time_unref(gpdata->clipboard_timer);
+ gpdata->clipboard_timer = t;
+ /* Convert text from VNC latin-1 to current GTK charset (usually UTF-8) */
+ g_get_charset(&cur_charset);
+ text = g_convert_with_fallback(param->text, param->textlen, cur_charset, "ISO-8859-1", "?", &br, &bw, NULL);
+ gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), text, bw);
+ g_free(text);
+ } else {
+ g_date_time_unref(t);
+ }
+ }
+ g_free(param->text);
+ g_free(param);
+ return FALSE;
+}
+
+static void remmina_plugin_vnc_rfb_cuttext(rfbClient *cl, const char *text, int textlen)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncCuttextParam *param;
+
+ param = g_new(RemminaPluginVncCuttextParam, 1);
+ param->gp = (RemminaProtocolWidget *)rfbClientGetClientData(cl, NULL);
+ param->text = g_malloc(textlen);
+ memcpy(param->text, text, textlen);
+ param->textlen = textlen;
+ IDLE_ADD((GSourceFunc)remmina_plugin_vnc_queue_cuttext, param);
+}
+
+static char *
+remmina_plugin_vnc_rfb_password(rfbClient *cl)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+ gchar *pwd = NULL;
+
+ gpdata->auth_called = TRUE;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ if (gpdata->auth_first)
+ pwd = g_strdup(remmina_plugin_service->file_get_string(remminafile, "password"));
+ if (!pwd) {
+ gboolean save;
+ gint ret;
+ gboolean disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE);
+ ret = remmina_plugin_service->protocol_plugin_init_auth(gp,
+ (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD),
+ _("Enter VNC password"),
+ NULL,
+ remmina_plugin_service->file_get_string(remminafile, "password"),
+ NULL,
+ NULL);
+ if (ret != GTK_RESPONSE_OK) {
+ gpdata->connected = FALSE;
+ return NULL;
+ }
+ pwd = remmina_plugin_service->protocol_plugin_init_get_password(gp);
+ save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp);
+ if (save)
+ remmina_plugin_service->file_set_string(remminafile, "password", pwd);
+ else
+ remmina_plugin_service->file_set_string(remminafile, "password", NULL);
+ }
+ return pwd;
+}
+
+static rfbCredential *
+remmina_plugin_vnc_rfb_credential(rfbClient *cl, int credentialType)
+{
+ TRACE_CALL(__func__);
+ rfbCredential *cred;
+ RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+ gint ret;
+ gchar *s1, *s2;
+ gboolean disablepasswordstoring;
+
+ gpdata->auth_called = TRUE;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ cred = g_new0(rfbCredential, 1);
+
+ switch (credentialType) {
+ case rfbCredentialTypeUser:
+
+ s1 = g_strdup(remmina_plugin_service->file_get_string(remminafile, "username"));
+
+ s2 = g_strdup(remmina_plugin_service->file_get_string(remminafile, "password"));
+
+ if (gpdata->auth_first && s1 && s2) {
+ cred->userCredential.username = s1;
+ cred->userCredential.password = s2;
+ } else {
+ g_free(s1);
+ g_free(s2);
+
+ disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE);
+ ret = remmina_plugin_service->protocol_plugin_init_auth(gp,
+ (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME,
+ _("Enter VNC authentication credentials"),
+ remmina_plugin_service->file_get_string(remminafile, "username"),
+ remmina_plugin_service->file_get_string(remminafile, "password"),
+ NULL,
+ NULL);
+ if (ret == GTK_RESPONSE_OK) {
+ gboolean save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp);
+ cred->userCredential.username = remmina_plugin_service->protocol_plugin_init_get_username(gp);
+ cred->userCredential.password = remmina_plugin_service->protocol_plugin_init_get_password(gp);
+ if (save) {
+ remmina_plugin_service->file_set_string(remminafile, "username", cred->userCredential.username);
+ remmina_plugin_service->file_set_string(remminafile, "password", cred->userCredential.password);
+ } else {
+ remmina_plugin_service->file_set_string(remminafile, "username", NULL);
+ remmina_plugin_service->file_set_string(remminafile, "password", NULL);
+ }
+ } else {
+ g_free(cred);
+ cred = NULL;
+ gpdata->connected = FALSE;
+ }
+ }
+ break;
+
+ case rfbCredentialTypeX509:
+ if (gpdata->auth_first &&
+ remmina_plugin_service->file_get_string(remminafile, "cacert")) {
+ cred->x509Credential.x509CACertFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "cacert"));
+ cred->x509Credential.x509CACrlFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "cacrl"));
+ cred->x509Credential.x509ClientCertFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "clientcert"));
+ cred->x509Credential.x509ClientKeyFile = g_strdup(remmina_plugin_service->file_get_string(remminafile, "clientkey"));
+ } else {
+ ret = remmina_plugin_service->protocol_plugin_init_authx509(gp);
+
+ if (ret == GTK_RESPONSE_OK) {
+ cred->x509Credential.x509CACertFile = remmina_plugin_service->protocol_plugin_init_get_cacert(gp);
+ cred->x509Credential.x509CACrlFile = remmina_plugin_service->protocol_plugin_init_get_cacrl(gp);
+ cred->x509Credential.x509ClientCertFile = remmina_plugin_service->protocol_plugin_init_get_clientcert(gp);
+ cred->x509Credential.x509ClientKeyFile = remmina_plugin_service->protocol_plugin_init_get_clientkey(gp);
+ } else {
+ g_free(cred);
+ cred = NULL;
+ gpdata->connected = FALSE;
+ }
+ }
+ break;
+
+ default:
+ g_free(cred);
+ cred = NULL;
+ break;
+ }
+ return cred;
+}
+
+static void remmina_plugin_vnc_rfb_cursor_shape(rfbClient *cl, int xhot, int yhot, int width, int height, int bytesPerPixel)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = rfbClientGetClientData(cl, NULL);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (!gtk_widget_get_window(GTK_WIDGET(gp)))
+ return;
+
+ if (width && height) {
+ gint stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width);
+ guchar *data = g_malloc(stride * height);
+ remmina_plugin_vnc_rfb_fill_buffer(cl, data, stride, cl->rcSource,
+ width * cl->format.bitsPerPixel / 8, cl->rcMask, width, height);
+ cairo_surface_t *surface = cairo_image_surface_create_for_data(data, CAIRO_FORMAT_ARGB32, width, height, stride);
+ if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
+ g_free(data);
+ return;
+ }
+ if (cairo_surface_set_user_data(surface, NULL, NULL, g_free) != CAIRO_STATUS_SUCCESS) {
+ g_free(data);
+ return;
+ }
+
+ LOCK_BUFFER(TRUE);
+ remmina_plugin_vnc_queuecursor(gp, surface, xhot, yhot);
+ UNLOCK_BUFFER(TRUE);
+ }
+}
+
+static void remmina_plugin_vnc_rfb_bell(rfbClient *cl)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Bell message received");
+ RemminaProtocolWidget *gp;
+ RemminaFile *remminafile;
+ GdkWindow *window;
+
+ gp = (RemminaProtocolWidget *)(rfbClientGetClientData(cl, NULL));
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "disableserverbell", FALSE))
+ return;
+
+ window = gtk_widget_get_window(GTK_WIDGET(gp));
+
+ if (window)
+ gdk_window_beep(window);
+ REMMINA_PLUGIN_DEBUG("Beep emitted");
+}
+
+/* Translate known VNC messages. It’s for intltool only, not for gcc */
+#ifdef __DO_NOT_COMPILE_ME__
+N_("Unable to connect to VNC server")
+N_("Couldn’t convert '%s' to host address")
+N_("VNC connection failed: %s")
+N_("Your connection has been rejected.")
+#endif
+/** @todo We only store the last message at this moment. */
+#define MAX_ERROR_LENGTH 1000
+static gchar vnc_error[MAX_ERROR_LENGTH + 1];
+static gboolean vnc_encryption_disable_requested;
+
+static void remmina_plugin_vnc_rfb_output(const char *format, ...)
+{
+ TRACE_CALL(__func__);
+ gchar *f, *p, *ff;
+
+ if (!rfbEnableClientLogging)
+ return;
+
+ va_list args;
+ va_start(args, format);
+ /* eliminate the last \n */
+ f = g_strdup(format);
+ if (f[strlen(f) - 1] == '\n') f[strlen(f) - 1] = '\0';
+
+ if (g_strcmp0(f, "VNC connection failed: %s") == 0) {
+ p = va_arg(args, gchar *);
+ g_snprintf(vnc_error, MAX_ERROR_LENGTH, _(f), _(p));
+ } else if (g_strcmp0(f, "The VNC server requested an unknown authentication method. %s") == 0) {
+ p = va_arg(args, gchar *);
+ if (vnc_encryption_disable_requested) {
+ ff = g_strconcat(_("The VNC server requested an unknown authentication method. %s"),
+ ". ",
+ _("Please retry after turning on encryption for this profile."),
+ NULL);
+ g_snprintf(vnc_error, MAX_ERROR_LENGTH, ff, p);
+ g_free(ff);
+ } else {
+ g_snprintf(vnc_error, MAX_ERROR_LENGTH, _(f), p);
+ }
+ } else {
+ g_vsnprintf(vnc_error, MAX_ERROR_LENGTH, _(f), args);
+ }
+ g_free(f);
+ va_end(args);
+
+ REMMINA_PLUGIN_DEBUG("VNC returned: %s", vnc_error);
+}
+
+static void remmina_plugin_vnc_chat_on_send(RemminaProtocolWidget *gp, const gchar *text)
+{
+ TRACE_CALL(__func__);
+ gchar *ptr;
+
+ /* Need to add a line-feed for UltraVNC */
+ ptr = g_strdup_printf("%s\n", text);
+ remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND, ptr, NULL, NULL);
+ g_free(ptr);
+}
+
+static void remmina_plugin_vnc_chat_on_destroy(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CHAT_CLOSE, NULL, NULL, NULL);
+}
+
+/* Send CTRL+ALT+DEL keys keystrokes to the plugin drawing_area widget */
+static void remmina_plugin_vnc_send_ctrlaltdel(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ guint keys[] = { GDK_KEY_Control_L, GDK_KEY_Alt_L, GDK_KEY_Delete };
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+
+ remmina_plugin_service->protocol_plugin_send_keys_signals(gpdata->drawing_area,
+ keys, G_N_ELEMENTS(keys), GDK_KEY_PRESS | GDK_KEY_RELEASE);
+}
+
+static gboolean remmina_plugin_vnc_close_chat(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_service->protocol_plugin_chat_close(gp);
+ return FALSE;
+}
+
+static gboolean remmina_plugin_vnc_open_chat(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ rfbClient *cl;
+
+ cl = (rfbClient *)gpdata->client;
+
+ remmina_plugin_service->protocol_plugin_chat_open(gp, cl->desktopName, remmina_plugin_vnc_chat_on_send,
+ remmina_plugin_vnc_chat_on_destroy);
+ remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CHAT_OPEN, NULL, NULL, NULL);
+ return FALSE;
+}
+
+static void remmina_plugin_vnc_rfb_chat(rfbClient *cl, int value, char *text)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp;
+
+ gp = (RemminaProtocolWidget *)(rfbClientGetClientData(cl, NULL));
+ switch (value) {
+ case rfbTextChatOpen:
+ IDLE_ADD((GSourceFunc)remmina_plugin_vnc_open_chat, gp);
+ break;
+ case rfbTextChatClose:
+ /* Do nothing… but wait for the next rfbTextChatFinished signal */
+ break;
+ case rfbTextChatFinished:
+ IDLE_ADD((GSourceFunc)remmina_plugin_vnc_close_chat, gp);
+ break;
+ default:
+ /* value is the text length */
+ remmina_plugin_service->protocol_plugin_chat_receive(gp, text);
+ break;
+ }
+}
+
+static gboolean remmina_plugin_vnc_incoming_connection(RemminaProtocolWidget *gp, rfbClient *cl)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ fd_set fds;
+
+ /**
+ * @fixme This may fail or not working as expected with multiple network interfaces,
+ * change with ListenAtTcpPortAndAddress
+ */
+ gpdata->listen_sock = ListenAtTcpPort(cl->listenPort);
+ if (gpdata->listen_sock < 0)
+ return FALSE;
+
+ remmina_plugin_service->protocol_plugin_init_show_listen(gp, cl->listenPort);
+
+ remmina_plugin_service->protocol_plugin_start_reverse_tunnel(gp, cl->listenPort);
+
+ FD_ZERO(&fds);
+ if (gpdata->listen_sock >= 0)
+ FD_SET(gpdata->listen_sock, &fds);
+
+ select(gpdata->listen_sock + 1, &fds, NULL, NULL, NULL);
+
+ if (!FD_ISSET(gpdata->listen_sock, &fds)) {
+ close(gpdata->listen_sock);
+ gpdata->listen_sock = -1;
+ return FALSE;
+ }
+
+ if (FD_ISSET(gpdata->listen_sock, &fds))
+ cl->sock = AcceptTcpConnection(gpdata->listen_sock);
+ if (cl->sock >= 0) {
+ close(gpdata->listen_sock);
+ gpdata->listen_sock = -1;
+ }
+ if (cl->sock < 0 || !SetNonBlocking(cl->sock))
+ return FALSE;
+
+ return TRUE;
+}
+
+
+static gboolean remmina_plugin_vnc_main_loop(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ gint ret;
+ gint i;
+ rfbClient *cl;
+ fd_set fds;
+ struct timeval timeout;
+
+ if (!gpdata->connected) {
+ gpdata->running = FALSE;
+ return FALSE;
+ }
+
+ cl = (rfbClient *)gpdata->client;
+
+ /*
+ * Do not explicitly wait while data is on the buffer, see:
+ * - https://jira.glyptodon.com/browse/GUAC-1056
+ * - https://jira.glyptodon.com/browse/GUAC-1056?focusedCommentId=14348&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-14348
+ * - https://github.com/apache/guacamole-server/blob/67680bd2d51e7949453f0f7ffc7f4234a1136715/src/protocols/vnc/vnc.c#L155
+ */
+ if (cl->buffered)
+ goto handle_buffered;
+
+ timeout.tv_sec = 10;
+ timeout.tv_usec = 0;
+ FD_ZERO(&fds);
+ FD_SET(cl->sock, &fds);
+ FD_SET(gpdata->vnc_event_pipe[0], &fds);
+ ret = select(MAX(cl->sock, gpdata->vnc_event_pipe[0]) + 1, &fds, NULL, NULL, &timeout);
+
+ /* Sometimes it returns <0 when opening a modal dialog in other window. Absolutely weird */
+ /* So we continue looping anyway */
+ if (ret <= 0)
+ return TRUE;
+
+ if (FD_ISSET(gpdata->vnc_event_pipe[0], &fds))
+ remmina_plugin_vnc_process_vnc_event(gp);
+ if (FD_ISSET(cl->sock, &fds)) {
+ i = WaitForMessage(cl, 500);
+ if (i < 0)
+ return TRUE;
+handle_buffered:
+ if (!HandleRFBServerMessage(cl)) {
+ gpdata->running = FALSE;
+ if (gpdata->connected && !remmina_plugin_service->protocol_plugin_is_closed(gp))
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+static gboolean remmina_plugin_vnc_main(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+ rfbClient *cl = NULL;
+ gchar *host;
+ gchar *s = NULL;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ gpdata->running = TRUE;
+
+ rfbClientLog = rfbClientErr = remmina_plugin_vnc_rfb_output;
+
+ gint colordepth = remmina_plugin_service->file_get_int(remminafile, "colordepth", 32);
+ gint quality = remmina_plugin_service->file_get_int(remminafile, "quality", 9);
+
+ while (gpdata->connected) {
+ gpdata->auth_called = FALSE;
+
+ host = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, VNC_DEFAULT_PORT, TRUE);
+
+ if (host == NULL) {
+ REMMINA_PLUGIN_DEBUG("host is null");
+ gpdata->connected = FALSE;
+ break;
+ }
+
+ /* int bitsPerSample,int samplesPerPixel, int bytesPerPixel */
+ switch (colordepth) {
+ case 8:
+ cl = rfbGetClient(2, 3, 1);
+ break;
+ case 15:
+ case 16:
+ cl = rfbGetClient(5, 3, 2);
+ break;
+ case 24:
+ cl = rfbGetClient(6, 3, 3);
+ break;
+ case 32:
+ default:
+ cl = rfbGetClient(8, 3, 4);
+ break;
+ }
+ REMMINA_PLUGIN_DEBUG("Color depth: %d", colordepth);
+ cl->MallocFrameBuffer = remmina_plugin_vnc_rfb_allocfb;
+ cl->canHandleNewFBSize = TRUE;
+ cl->GetPassword = remmina_plugin_vnc_rfb_password;
+ cl->GetCredential = remmina_plugin_vnc_rfb_credential;
+ cl->GotFrameBufferUpdate = remmina_plugin_vnc_rfb_updatefb;
+ /**
+ * @fixme we have to implement FinishedFrameBufferUpdate
+ * This is to know when the server has finished to send a batch of frame buffer
+ * updates.
+ * cl->FinishedFrameBufferUpdate = remmina_plugin_vnc_rfb_finished;
+ */
+ /**
+ * @fixme we have to implement HandleKeyboardLedState
+ * cl->HandleKeyboardLedState = remmina_plugin_vnc_rfb_led_state
+ */
+ cl->HandleKeyboardLedState = remmina_plugin_vnc_rfb_led_state;
+ cl->GotXCutText = (
+ remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE) ?
+ NULL : remmina_plugin_vnc_rfb_cuttext);
+ cl->GotCursorShape = remmina_plugin_vnc_rfb_cursor_shape;
+ cl->Bell = remmina_plugin_vnc_rfb_bell;
+ cl->HandleTextChat = remmina_plugin_vnc_rfb_chat;
+ /**
+ * @fixme we have to implement HandleXvpMsg
+ * cl->HandleXvpMsg = remmina_plugin_vnc_rfb_handle_xvp;
+ */
+
+ rfbClientSetClientData(cl, NULL, gp);
+
+ if (host[0] == '\0') {
+ cl->serverHost = g_strdup(host);
+ cl->listenSpecified = TRUE;
+ if (remmina_plugin_service->file_get_int(remminafile, "ssh_tunnel_enabled", FALSE))
+ /* When we use reverse tunnel, the local port does not really matter.
+ * Hardcode a default port just in case the remote port is customized
+ * to a privilege port then we will have problem listening. */
+ cl->listenPort = 5500;
+ else
+ cl->listenPort = remmina_plugin_service->file_get_int(remminafile, "listenport", 5500);
+
+ remmina_plugin_vnc_incoming_connection(gp, cl);
+ } else {
+ if (strstr(host, "unix://") == host) {
+ cl->serverHost = g_strdup(host + strlen("unix://"));
+ cl->serverPort = 0;
+ } else {
+ remmina_plugin_service->get_server_port(host, VNC_DEFAULT_PORT, &s, &cl->serverPort);
+ cl->serverHost = g_strdup(s);
+ g_free(s);
+ /* Support short-form (:0, :1) */
+ if (cl->serverPort < 100)
+ cl->serverPort += VNC_DEFAULT_PORT;
+ }
+ }
+ g_free(host);
+ host = NULL;
+
+ if (cl->serverHost && strstr(cl->serverHost, "unix://") != cl->serverHost && remmina_plugin_service->file_get_string(remminafile, "proxy")) {
+ remmina_plugin_service->get_server_port(
+ remmina_plugin_service->file_get_string(remminafile, "server"),
+ VNC_DEFAULT_PORT,
+ &cl->destHost,
+ &cl->destPort);
+ remmina_plugin_service->get_server_port(
+ remmina_plugin_service->file_get_string(remminafile, "proxy"),
+ VNC_DEFAULT_PORT,
+ &cl->serverHost,
+ &cl->serverPort);
+ REMMINA_PLUGIN_DEBUG("cl->serverHost: %s", cl->serverHost);
+ REMMINA_PLUGIN_DEBUG("cl->serverPort: %d", cl->serverPort);
+ REMMINA_PLUGIN_DEBUG("cl->destHost: %s", cl->destHost);
+ REMMINA_PLUGIN_DEBUG("cl->destPort: %d", cl->destPort);
+ }
+
+ cl->appData.useRemoteCursor = (
+ remmina_plugin_service->file_get_int(remminafile, "showcursor", FALSE) ? FALSE : TRUE);
+
+ remmina_plugin_vnc_update_quality(cl, quality);
+ remmina_plugin_vnc_update_colordepth(cl, colordepth);
+ if ((cl->format.depth == 8) && (quality == 9))
+ cl->appData.encodingsString = "copyrect zlib hextile raw";
+ else if ((cl->format.depth == 8) && (quality == 2))
+ cl->appData.encodingsString = "zrle ultra copyrect hextile zlib corre rre raw";
+ else if ((cl->format.depth == 8) && (quality == 1))
+ cl->appData.encodingsString = "zrle ultra copyrect hextile zlib corre rre raw";
+ else if ((cl->format.depth == 8) && (quality == 0))
+ cl->appData.encodingsString = "zrle ultra copyrect hextile zlib corre rre raw";
+ SetFormatAndEncodings(cl);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "disableencryption", FALSE)) {
+ vnc_encryption_disable_requested = TRUE;
+ SetClientAuthSchemes(cl, remmina_plugin_vnc_no_encrypt_auth_types, -1);
+ } else {
+ vnc_encryption_disable_requested = FALSE;
+ }
+
+ if (rfbInitClient(cl, NULL, NULL)) {
+ REMMINA_PLUGIN_DEBUG("Client initialization successfull");
+ break;
+ } else {
+ REMMINA_PLUGIN_DEBUG("Client initialization failed");
+ }
+
+ /* If the authentication is not called, it has to be a fatal error and must quit */
+ if (!gpdata->auth_called) {
+ REMMINA_PLUGIN_DEBUG("Client not authenticated");
+ gpdata->connected = FALSE;
+ break;
+ }
+
+ /* vnc4server reports "already in use" after authentication. Workaround here */
+ if (strstr(vnc_error, "The server is already in use")) {
+ gpdata->connected = FALSE;
+ gpdata->auth_called = FALSE;
+ break;
+ }
+ /* Don't assume authentication failed for known network-related errors in
+ libvncclient/sockets.c. */
+ if (strstr(vnc_error, "read (") || strstr(vnc_error, "select\n") ||
+ strstr(vnc_error, "write\n") || strstr(vnc_error, "Connection timed out")) {
+ gpdata->connected = FALSE;
+ gpdata->auth_called = FALSE;
+ break;
+ }
+
+ /* Otherwise, it’s a password error. Try to clear saved password if any */
+ remmina_plugin_service->file_set_string(remminafile, "password", NULL);
+
+ if (!gpdata->connected)
+ break;
+
+ remmina_plugin_service->protocol_plugin_init_show_retry(gp);
+
+ /* It’s safer to sleep a while before reconnect */
+ sleep(2);
+
+ gpdata->auth_first = FALSE;
+ }
+
+ if (!gpdata->connected) {
+ REMMINA_PLUGIN_DEBUG("Client not connected with error: %s", vnc_error);
+ if (cl && !gpdata->auth_called && !(remmina_plugin_service->protocol_plugin_has_error(gp)))
+ remmina_plugin_service->protocol_plugin_set_error(gp, "%s", vnc_error);
+ gpdata->running = FALSE;
+
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+
+ return FALSE;
+ }
+
+ REMMINA_PLUGIN_DEBUG("Client connected");
+ remmina_plugin_service->protocol_plugin_init_save_cred(gp);
+
+ gpdata->client = cl;
+
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "disableserverinput", FALSE))
+ PermitServerInput(cl, 1);
+
+ if (gpdata->thread) {
+ while (remmina_plugin_vnc_main_loop(gp)) {
+ }
+ gpdata->running = FALSE;
+ } else {
+ IDLE_ADD((GSourceFunc)remmina_plugin_vnc_main_loop, gp);
+ }
+
+ return FALSE;
+}
+
+
+static gpointer
+remmina_plugin_vnc_main_thread(gpointer data)
+{
+ TRACE_CALL(__func__);
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+
+ CANCEL_ASYNC
+ remmina_plugin_vnc_main((RemminaProtocolWidget *)data);
+ return NULL;
+}
+
+
+static RemminaPluginVncCoordinates remmina_plugin_vnc_scale_coordinates(GtkWidget *widget, RemminaProtocolWidget *gp, gint x, gint y)
+{
+ GtkAllocation widget_allocation;
+ RemminaPluginVncCoordinates result;
+
+ if ((remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE)) {
+ gtk_widget_get_allocation(widget, &widget_allocation);
+ result.x = x * remmina_plugin_service->protocol_plugin_get_width(gp) / widget_allocation.width;
+ result.y = y * remmina_plugin_service->protocol_plugin_get_height(gp) / widget_allocation.height;
+ } else {
+ result.x = x;
+ result.y = y;
+ }
+
+ return result;
+}
+
+static gboolean remmina_plugin_vnc_on_motion(GtkWidget *widget, GdkEventMotion *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+ RemminaPluginVncCoordinates coordinates;
+
+ if (!gpdata->connected || !gpdata->client)
+ return FALSE;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE))
+ return FALSE;
+
+ coordinates = remmina_plugin_vnc_scale_coordinates(widget, gp, event->x, event->y);
+ remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y),
+ GINT_TO_POINTER(gpdata->button_mask));
+ return TRUE;
+}
+
+static gboolean remmina_plugin_vnc_on_button(GtkWidget *widget, GdkEventButton *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+ RemminaPluginVncCoordinates coordinates;
+ gint mask;
+
+ if (!gpdata->connected || !gpdata->client)
+ return FALSE;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE))
+ return FALSE;
+
+ /* We only accept 3 buttons */
+ if (event->button < 1 || event->button > 3)
+ return FALSE;
+ /* We bypass 2button-press and 3button-press events */
+ if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE)
+ return TRUE;
+
+ mask = (1 << (event->button - 1));
+ gpdata->button_mask = (event->type == GDK_BUTTON_PRESS ? (gpdata->button_mask | mask) :
+ (gpdata->button_mask & (0xff - mask)));
+
+ coordinates = remmina_plugin_vnc_scale_coordinates(widget, gp, event->x, event->y);
+ remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y),
+ GINT_TO_POINTER(gpdata->button_mask));
+ return TRUE;
+}
+
+static gint delta_to_mask(float delta, float *accum, gint mask_plus, gint mask_minus)
+{
+ *accum += delta;
+ if (*accum >= 1.0) {
+ *accum = 0.0;
+ return mask_plus;
+ } else if (*accum <= -1.0) {
+ *accum = 0.0;
+ return mask_minus;
+ }
+ return 0;
+}
+
+static gboolean remmina_plugin_vnc_on_scroll(GtkWidget *widget, GdkEventScroll *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+ RemminaPluginVncCoordinates coordinates;
+ gint mask;
+
+ if (!gpdata->connected || !gpdata->client)
+ return FALSE;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE))
+ return FALSE;
+
+ switch (event->direction) {
+ case GDK_SCROLL_UP:
+ mask = (1 << 3);
+ gpdata->scroll_y_accumulator = 0;
+ break;
+ case GDK_SCROLL_DOWN:
+ mask = (1 << 4);
+ gpdata->scroll_y_accumulator = 0;
+ break;
+ case GDK_SCROLL_LEFT:
+ mask = (1 << 5);
+ gpdata->scroll_x_accumulator = 0;
+ break;
+ case GDK_SCROLL_RIGHT:
+ mask = (1 << 6);
+ gpdata->scroll_x_accumulator = 0;
+ break;
+#if GTK_CHECK_VERSION(3, 4, 0)
+ case GDK_SCROLL_SMOOTH:
+ /* RFB does not seems to support SMOOTH scroll, so we accumulate GTK delta requested
+ * up to 1.0 and then send a normal RFB wheel scroll when the accumulator reaches 1.0 */
+ mask = delta_to_mask(event->delta_y, &(gpdata->scroll_y_accumulator), (1 << 4), (1 << 3));
+ mask |= delta_to_mask(event->delta_x, &(gpdata->scroll_x_accumulator), (1 << 6), (1 << 5));
+ if (!mask)
+ return FALSE;
+ break;
+#endif
+ default:
+ return FALSE;
+ }
+
+ coordinates = remmina_plugin_vnc_scale_coordinates(widget, gp, event->x, event->y);
+ remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y),
+ GINT_TO_POINTER(mask | gpdata->button_mask));
+ remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_POINTER, GINT_TO_POINTER(coordinates.x), GINT_TO_POINTER(coordinates.y),
+ GINT_TO_POINTER(gpdata->button_mask));
+
+ return TRUE;
+}
+
+static void remmina_plugin_vnc_release_key(RemminaProtocolWidget *gp, guint16 keycode)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaKeyVal *k;
+ gint i;
+
+ if (!gpdata)
+ return;
+
+ if (keycode == 0) {
+ /* Send all release key events for previously pressed keys */
+ for (i = 0; i < gpdata->pressed_keys->len; i++) {
+ k = g_ptr_array_index(gpdata->pressed_keys, i);
+ remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_KEY, GUINT_TO_POINTER(k->keyval),
+ GINT_TO_POINTER(FALSE), NULL);
+ g_free(k);
+ }
+ g_ptr_array_set_size(gpdata->pressed_keys, 0);
+ } else {
+ /* Unregister the keycode only */
+ for (i = 0; i < gpdata->pressed_keys->len; i++) {
+ k = g_ptr_array_index(gpdata->pressed_keys, i);
+ if (k->keycode == keycode) {
+ g_free(k);
+ g_ptr_array_remove_index_fast(gpdata->pressed_keys, i);
+ break;
+ }
+ }
+ }
+}
+
+static gboolean remmina_plugin_vnc_on_key(GtkWidget *widget, GdkEventKey *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+ RemminaKeyVal *k;
+ guint event_keyval;
+ guint keyval;
+
+ if (!gpdata->connected || !gpdata->client)
+ return FALSE;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE))
+ return FALSE;
+
+ gpdata->scroll_x_accumulator = 0;
+ gpdata->scroll_y_accumulator = 0;
+
+ /* When sending key release, try first to find out a previously sent keyval
+ * to workaround bugs like https://bugs.freedesktop.org/show_bug.cgi?id=7430 */
+
+ event_keyval = event->keyval;
+ if (event->type == GDK_KEY_RELEASE) {
+ for (int i = 0; i < gpdata->pressed_keys->len; i++) {
+ k = g_ptr_array_index(gpdata->pressed_keys, i);
+ if (k->keycode == event->hardware_keycode) {
+ event_keyval = k->keyval;
+ break;
+ }
+ }
+ }
+
+ keyval = remmina_plugin_service->pref_keymap_get_keyval(remmina_plugin_service->file_get_string(remminafile, "keymap"),
+ event_keyval);
+
+ remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_KEY, GUINT_TO_POINTER(keyval),
+ GINT_TO_POINTER(event->type == GDK_KEY_PRESS ? TRUE : FALSE), NULL);
+
+ /* Register/unregister the pressed key */
+ if (event->type == GDK_KEY_PRESS) {
+ k = g_new(RemminaKeyVal, 1);
+ k->keyval = keyval;
+ k->keycode = event->hardware_keycode;
+ g_ptr_array_add(gpdata->pressed_keys, k);
+ } else {
+ remmina_plugin_vnc_release_key(gp, event->hardware_keycode);
+ }
+ return TRUE;
+}
+
+static void remmina_plugin_vnc_on_cuttext_request(GtkClipboard *clipboard, const gchar *text, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ GDateTime *t;
+ glong diff;
+ gsize br, bw;
+ gchar *latin1_text;
+ const char *cur_charset;
+
+ if (text) {
+ /* A timer (1 second) to avoid clipboard "loopback": text cut out from VNC won’t paste back into VNC */
+ t = g_date_time_new_now_utc();
+ diff = g_date_time_difference(t, gpdata->clipboard_timer) / 100000; // tenth of second
+ if (diff < 10)
+ return;
+ g_date_time_unref(gpdata->clipboard_timer);
+ gpdata->clipboard_timer = t;
+ /* Convert text from current charset to latin-1 before sending to remote server.
+ * See RFC6143 7.5.6 */
+ g_get_charset(&cur_charset);
+ latin1_text = g_convert_with_fallback(text, -1, "ISO-8859-1", cur_charset, "?", &br, &bw, NULL);
+ remmina_plugin_vnc_event_push(gp, REMMINA_PLUGIN_VNC_EVENT_CUTTEXT, (gpointer)latin1_text, NULL, NULL);
+ g_free(latin1_text);
+ }
+}
+
+static void remmina_plugin_vnc_on_cuttext(GtkClipboard *clipboard, GdkEvent *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+
+ if (!gpdata->connected || !gpdata->client)
+ return;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (remmina_plugin_service->file_get_int(remminafile, "viewonly", FALSE))
+ return;
+
+ gtk_clipboard_request_text(clipboard, (GtkClipboardTextReceivedFunc)remmina_plugin_vnc_on_cuttext_request, gp);
+}
+
+static void remmina_plugin_vnc_on_realize(RemminaProtocolWidget *gp, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+ GdkCursor *cursor;
+ GdkPixbuf *pixbuf;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "showcursor", FALSE)) {
+ /* Hide local cursor (show a small dot instead) */
+ pixbuf = gdk_pixbuf_new_from_xpm_data(dot_cursor_xpm);
+ cursor = gdk_cursor_new_from_pixbuf(gdk_display_get_default(), pixbuf, dot_cursor_x_hot, dot_cursor_y_hot);
+ g_object_unref(pixbuf);
+ gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(gp)), cursor);
+ g_object_unref(cursor);
+ }
+}
+
+/******************************************************************************************/
+
+static gboolean remmina_plugin_vnc_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ gpdata->connected = TRUE;
+ gchar *server;
+ gint port;
+ const gchar* raw_server;
+
+ remmina_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->drawing_area);
+
+ g_signal_connect(G_OBJECT(gp), "realize", G_CALLBACK(remmina_plugin_vnc_on_realize), NULL);
+ g_signal_connect(G_OBJECT(gpdata->drawing_area), "motion-notify-event", G_CALLBACK(remmina_plugin_vnc_on_motion), gp);
+ g_signal_connect(G_OBJECT(gpdata->drawing_area), "button-press-event", G_CALLBACK(remmina_plugin_vnc_on_button), gp);
+ g_signal_connect(G_OBJECT(gpdata->drawing_area), "button-release-event", G_CALLBACK(remmina_plugin_vnc_on_button), gp);
+ g_signal_connect(G_OBJECT(gpdata->drawing_area), "scroll-event", G_CALLBACK(remmina_plugin_vnc_on_scroll), gp);
+ g_signal_connect(G_OBJECT(gpdata->drawing_area), "key-press-event", G_CALLBACK(remmina_plugin_vnc_on_key), gp);
+ g_signal_connect(G_OBJECT(gpdata->drawing_area), "key-release-event", G_CALLBACK(remmina_plugin_vnc_on_key), gp);
+
+ if (!remmina_plugin_service->file_get_int(remminafile, "disableclipboard", FALSE))
+ gpdata->clipboard_handler = g_signal_connect(G_OBJECT(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)),
+ "owner-change", G_CALLBACK(remmina_plugin_vnc_on_cuttext), gp);
+
+
+ if (pthread_create(&gpdata->thread, NULL, remmina_plugin_vnc_main_thread, gp)) {
+ /* I don’t think this will ever happen… */
+ g_print("Could not initialize pthread. Falling back to non-thread mode…\n");
+ g_timeout_add(0, (GSourceFunc)remmina_plugin_vnc_main, gp);
+ gpdata->thread = 0;
+ }
+
+ raw_server = remmina_plugin_service->file_get_string(remminafile, "server");
+
+ if (raw_server && strstr(raw_server, "unix://") == raw_server) {
+ REMMINA_PLUGIN_AUDIT(_("Connected to %s via VNC"), raw_server);
+ } else {
+ remmina_plugin_service->get_server_port(raw_server,
+ VNC_DEFAULT_PORT,
+ &server,
+ &port);
+
+ REMMINA_PLUGIN_AUDIT(_("Connected to %s:%d via VNC"), server, port);
+ g_free(server), server = NULL;
+ }
+#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14)
+ remmina_plugin_service->protocol_plugin_unlock_dynres(gp);
+#endif
+ return TRUE;
+}
+
+static gboolean remmina_plugin_vnc_close_connection_timeout(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+
+ gchar *server;
+ gint port;
+
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ remmina_plugin_service->get_server_port(remmina_plugin_service->file_get_string(remminafile, "server"),
+ VNC_DEFAULT_PORT,
+ &server,
+ &port);
+
+ REMMINA_PLUGIN_AUDIT(_("Disconnected from %s:%d via VNC"), server, port);
+ g_free(server), server = NULL;
+
+ /* wait until the running attribute is set to false by the VNC thread */
+ if (gpdata->running)
+ return TRUE;
+
+ /* unregister the clipboard monitor */
+ if (gpdata->clipboard_handler) {
+ g_signal_handler_disconnect(G_OBJECT(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)), gpdata->clipboard_handler);
+ gpdata->clipboard_handler = 0;
+ }
+
+ if (gpdata->queuecursor_handler) {
+ g_source_remove(gpdata->queuecursor_handler);
+ gpdata->queuecursor_handler = 0;
+ }
+ if (gpdata->queuecursor_surface) {
+ cairo_surface_destroy(gpdata->queuecursor_surface);
+ gpdata->queuecursor_surface = NULL;
+ }
+
+ if (gpdata->queuedraw_handler) {
+ g_source_remove(gpdata->queuedraw_handler);
+ gpdata->queuedraw_handler = 0;
+ }
+ if (gpdata->listen_sock >= 0)
+ close(gpdata->listen_sock);
+ if (gpdata->client) {
+ rfbClientCleanup((rfbClient *)gpdata->client);
+ gpdata->client = NULL;
+ }
+ if (gpdata->rgb_buffer) {
+ cairo_surface_destroy(gpdata->rgb_buffer);
+ gpdata->rgb_buffer = NULL;
+ }
+ if (gpdata->vnc_buffer) {
+ g_free(gpdata->vnc_buffer);
+ gpdata->vnc_buffer = NULL;
+ }
+ g_ptr_array_free(gpdata->pressed_keys, TRUE);
+ g_date_time_unref(gpdata->clipboard_timer);
+ remmina_plugin_vnc_event_free_all(gp);
+ g_queue_free(gpdata->vnc_event_queue);
+ pthread_mutex_destroy(&gpdata->vnc_event_queue_mutex);
+ close(gpdata->vnc_event_pipe[0]);
+ close(gpdata->vnc_event_pipe[1]);
+
+
+ pthread_mutex_destroy(&gpdata->buffer_mutex);
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+
+ return FALSE;
+}
+
+static gboolean remmina_plugin_vnc_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+
+ gpdata->connected = FALSE;
+
+ if (gpdata->thread) {
+ pthread_cancel(gpdata->thread);
+ if (gpdata->thread) pthread_join(gpdata->thread, NULL);
+ gpdata->running = FALSE;
+ remmina_plugin_vnc_close_connection_timeout(gp);
+ } else {
+ g_timeout_add(200, (GSourceFunc)remmina_plugin_vnc_close_connection_timeout, gp);
+ }
+
+ return FALSE;
+}
+
+static gboolean remmina_plugin_vnc_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ switch (feature->id) {
+ case REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT:
+ return SupportsClient2Server((rfbClient *)(gpdata->client), rfbSetServerInput) ? TRUE : FALSE;
+ case REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT:
+ return SupportsClient2Server((rfbClient *)(gpdata->client), rfbTextChat) ? TRUE : FALSE;
+ default:
+ return TRUE;
+ }
+}
+
+static void remmina_plugin_vnc_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+ rfbClient* client;
+ uint8_t previous_bpp;
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ switch (feature->id) {
+ case REMMINA_PLUGIN_VNC_FEATURE_PREF_QUALITY:
+ remmina_plugin_vnc_update_quality((rfbClient *)(gpdata->client),
+ remmina_plugin_service->file_get_int(remminafile, "quality", 9));
+ remmina_plugin_vnc_update_colordepth((rfbClient *)(gpdata->client),
+ remmina_plugin_service->file_get_int(remminafile, "colordepth", 32));
+ SetFormatAndEncodings((rfbClient *)(gpdata->client));
+ break;
+ case REMMINA_PLUGIN_VNC_FEATURE_PREF_COLOR:
+ client = (rfbClient *)(gpdata->client);
+ previous_bpp = client->format.bitsPerPixel;
+ remmina_plugin_vnc_update_colordepth(client,
+ remmina_plugin_service->file_get_int(remminafile, "colordepth", 32));
+ SetFormatAndEncodings(client);
+ //Need to clear away old and reallocate if we're increasing bpp
+ if (client->format.bitsPerPixel > previous_bpp){
+ remmina_plugin_vnc_rfb_allocfb((rfbClient *)(gpdata->client));
+ SendFramebufferUpdateRequest((rfbClient *)(gpdata->client), 0, 0,
+ remmina_plugin_service->protocol_plugin_get_width(gp),
+ remmina_plugin_service->protocol_plugin_get_height(gp), FALSE);
+ }
+ break;
+ case REMMINA_PLUGIN_VNC_FEATURE_PREF_VIEWONLY:
+ break;
+ case REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT:
+ PermitServerInput((rfbClient *)(gpdata->client),
+ remmina_plugin_service->file_get_int(remminafile, "disableserverinput", FALSE) ? 1 : 0);
+ break;
+ case REMMINA_PLUGIN_VNC_FEATURE_UNFOCUS:
+ remmina_plugin_vnc_release_key(gp, 0);
+ break;
+ case REMMINA_PLUGIN_VNC_FEATURE_SCALE:
+ remmina_plugin_vnc_update_scale(gp, remmina_plugin_service->file_get_int(remminafile, "scale", FALSE));
+ break;
+ case REMMINA_PLUGIN_VNC_FEATURE_TOOL_REFRESH:
+ SendFramebufferUpdateRequest((rfbClient *)(gpdata->client), 0, 0,
+ remmina_plugin_service->protocol_plugin_get_width(gp),
+ remmina_plugin_service->protocol_plugin_get_height(gp), FALSE);
+ break;
+ case REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT:
+ remmina_plugin_vnc_open_chat(gp);
+ break;
+ case REMMINA_PLUGIN_VNC_FEATURE_TOOL_SENDCTRLALTDEL:
+ remmina_plugin_vnc_send_ctrlaltdel(gp);
+ break;
+ default:
+ break;
+ }
+}
+
+/* Send a keystroke to the plugin window */
+static void remmina_plugin_vnc_keystroke(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+
+ remmina_plugin_service->protocol_plugin_send_keys_signals(gpdata->drawing_area,
+ keystrokes, keylen, GDK_KEY_PRESS | GDK_KEY_RELEASE);
+ return;
+}
+
+#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14)
+static gboolean remmina_plugin_vnc_on_size_allocate(GtkWidget *widget, GtkAllocation *alloc, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaScaleMode scale_mode = remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (scale_mode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES){
+ char str[1024];
+ sprintf(str, "DEBUG: %d x %d", alloc->width, alloc->height);
+ TRACE_CALL(str);
+ if (gpdata->client){
+ SendExtDesktopSize(gpdata->client, alloc->width, alloc->height);
+ }
+ }
+ return TRUE;
+}
+#endif
+
+static gboolean remmina_plugin_vnc_on_draw(GtkWidget *widget, cairo_t *context, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata = GET_PLUGIN_DATA(gp);
+ cairo_surface_t *surface;
+ gint width, height;
+ GtkAllocation widget_allocation;
+
+ LOCK_BUFFER(FALSE);
+
+ surface = gpdata->rgb_buffer;
+ if (!surface) {
+ UNLOCK_BUFFER(FALSE);
+ return FALSE;
+ }
+
+ width = remmina_plugin_service->protocol_plugin_get_width(gp);
+ height = remmina_plugin_service->protocol_plugin_get_height(gp);
+
+ if ((remmina_plugin_service->remmina_protocol_widget_get_current_scale_mode(gp) != REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE)) {
+ gtk_widget_get_allocation(widget, &widget_allocation);
+ cairo_scale(context,
+ (double)widget_allocation.width / width,
+ (double)widget_allocation.height / height);
+ }
+
+ cairo_rectangle(context, 0, 0, width, height);
+ cairo_set_source_surface(context, surface, 0, 0);
+ cairo_fill(context);
+
+ UNLOCK_BUFFER(FALSE);
+ return TRUE;
+}
+
+static void remmina_plugin_vnc_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginVncData *gpdata;
+ gint flags;
+ gdouble aspect_ratio;
+
+ gpdata = g_new0(RemminaPluginVncData, 1);
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
+
+ gboolean disable_smooth_scrolling = FALSE;
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ disable_smooth_scrolling = remmina_plugin_service->file_get_int(remminafile, "disablesmoothscrolling", FALSE);
+ REMMINA_PLUGIN_DEBUG("Disable smooth scrolling is set to %d", disable_smooth_scrolling);
+
+ gpdata->drawing_area = gtk_drawing_area_new();
+ gtk_widget_show(gpdata->drawing_area);
+
+ aspect_ratio = remmina_plugin_service->file_get_double(remminafile, "aspect_ratio", 0);
+ if (aspect_ratio > 0){
+ GtkWidget* aspectframe = gtk_aspect_frame_new(NULL, 0, 0, aspect_ratio, FALSE);
+
+ gtk_frame_set_shadow_type(GTK_FRAME(aspectframe), GTK_SHADOW_NONE);
+ gtk_widget_show(aspectframe);
+ gtk_container_add(GTK_CONTAINER(aspectframe), gpdata->drawing_area);
+ gtk_container_add(GTK_CONTAINER(gp), aspectframe);
+ }
+ else{
+ gtk_container_add(GTK_CONTAINER(gp), gpdata->drawing_area);
+ }
+
+ gtk_widget_add_events(
+ gpdata->drawing_area,
+ GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK
+ | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK
+ | GDK_KEY_RELEASE_MASK
+ | GDK_SCROLL_MASK);
+ gtk_widget_set_can_focus(gpdata->drawing_area, TRUE);
+
+ if (!disable_smooth_scrolling) {
+ REMMINA_PLUGIN_DEBUG("Adding GDK_SMOOTH_SCROLL_MASK");
+ gtk_widget_add_events(gpdata->drawing_area, GDK_SMOOTH_SCROLL_MASK);
+ }
+
+
+ g_signal_connect(G_OBJECT(gpdata->drawing_area), "draw", G_CALLBACK(remmina_plugin_vnc_on_draw), gp);
+#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14)
+ g_signal_connect(G_OBJECT(gpdata->drawing_area), "size-allocate", G_CALLBACK(remmina_plugin_vnc_on_size_allocate), gp);
+#endif
+ gpdata->auth_first = TRUE;
+ gpdata->clipboard_timer = g_date_time_new_now_utc();
+ gpdata->listen_sock = -1;
+ gpdata->pressed_keys = g_ptr_array_new();
+ gpdata->vnc_event_queue = g_queue_new();
+ pthread_mutex_init(&gpdata->vnc_event_queue_mutex, NULL);
+ if (pipe(gpdata->vnc_event_pipe)) {
+ g_print("Error creating pipes.\n");
+ gpdata->vnc_event_pipe[0] = 0;
+ gpdata->vnc_event_pipe[1] = 0;
+ }
+ flags = fcntl(gpdata->vnc_event_pipe[0], F_GETFL, 0);
+ fcntl(gpdata->vnc_event_pipe[0], F_SETFL, flags | O_NONBLOCK);
+
+ pthread_mutex_init(&gpdata->buffer_mutex, NULL);
+}
+
+/* Array of key/value pairs for color depths */
+static gpointer colordepth_list[] =
+{
+ "32", N_("True colour (32 bpp)"),
+ "16", N_("High colour (16 bpp)"),
+ "8", N_("256 colours (8 bpp)"),
+ NULL
+};
+
+/* Array of key/value pairs for quality selection */
+static gpointer quality_list[] =
+{
+ "2", N_("Good"),
+ "9", N_("Best (slowest)"),
+ "1", N_("Medium"),
+ "0", N_("Poor (fastest)"),
+ NULL
+};
+
+static gchar repeater_tooltip[] =
+ N_("Connect to VNC using a repeater:\n"
+ " • The server field must contain the repeater ID, e.g. ID:123456789\n"
+ " • The repeater field have to be set to the repeater IP and port, like:\n"
+ " 10.10.10.12:5901\n"
+ " • From the remote VNC server, you will connect to\n"
+ " the repeater, e.g. with x11vnc:\n"
+ " x11vnc -connect repeater=ID:123456789+10.10.10.12:5500");
+
+static gchar vnciport_tooltip[] =
+ N_("Listening for remote VNC connection:\n"
+ " • The “Listen on port” field is the port Remmina will listen to,\n"
+ " e.g. 8888\n"
+ " • From the remote VNC server, you will connect to\n"
+ " Remmina, e.g. with x11vnc:\n"
+ " x11vnc -display :0 -connect 192.168.1.36:8888");
+
+static gchar aspect_ratio_tooltip[] =
+ N_("Lock the aspect ratio when dynamic resolution is enabled:\n"
+ "\n"
+ " • The aspect ratio should be entered as a decimal number, e.g. 1.777\n"
+ " • 16:9 corresponds roughly to 1.7777, 4:3 corresponds roughly to 1.333\n"
+ " • The default value of 0 does not enforce any aspect ratio");
+
+static gchar vncencodings_tooltip[] =
+ N_("Overriding the pre-set VNC encoding quality:\n"
+ "\n"
+ " • “Poor (fastest)” sets encoding to “copyrect zlib hextile raw”\n"
+ " • “Medium” sets encoding to “tight zrle ultra copyrect hextile zlib corre rre raw”\n"
+ " • “Good” sets encoding to “tight zrle ultra copyrect hextile zlib corre rre raw”\n"
+ " • “Best (slowest)” sets encoding to “copyrect zrle ultra zlib hextile corre rre raw”");
+
+/* Array of RemminaProtocolSetting for basic settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting tooltip
+ * g) Validation data pointer, will be passed to the validation callback method.
+ * h) Validation callback method (Can be NULL. Every entry will be valid then.)
+ * use following prototype:
+ * gboolean mysetting_validator_method(gpointer key, gpointer value,
+ * gpointer validator_data);
+ * gpointer key is a gchar* containing the setting's name,
+ * gpointer value contains the value which should be validated,
+ * gpointer validator_data contains your passed data.
+ */
+static const RemminaProtocolSetting remmina_plugin_vnc_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, "_rfb._tcp", NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "proxy", N_("Repeater"), FALSE, NULL, repeater_tooltip, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("User password"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "colordepth", N_("Colour depth"), FALSE, colordepth_list, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "quality", N_("Quality"), FALSE, quality_list, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP, "keymap", NULL, FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL }
+};
+
+// Same as above.
+static const RemminaProtocolSetting remmina_plugin_vnci_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "listenport", N_("Listen on port"), FALSE, NULL, vnciport_tooltip, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("User password"), FALSE, NULL, NULL, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "colordepth", N_("Colour depth"), FALSE, colordepth_list, NULL, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "quality", N_("Quality"), FALSE, quality_list, NULL, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP, "keymap", NULL, FALSE, NULL, NULL, NULL, NULL},
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL}
+};
+
+/* Array of RemminaProtocolSetting for advanced settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting Tooltip
+ */
+static const RemminaProtocolSetting remmina_plugin_vnc_advanced_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "encodings", N_("Override pre-set VNC encodings"), FALSE, NULL, vncencodings_tooltip },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "aspect_ratio", N_("Dynamic resolution enforced aspec ratio"), FALSE, NULL, aspect_ratio_tooltip },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "tightencoding", N_("Force tight encoding"), TRUE, NULL, N_("Enabling this may help when the remote desktop looks scrambled") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablesmoothscrolling", N_("Disable smooth scrolling"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableserverbell", N_("Ignore remote bell messages"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableserverinput", N_("Prevent local interaction on the server"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "showcursor", N_("Show remote cursor"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableclipboard", N_("Turn off clipboard sync"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disableencryption", N_("Turn off encryption"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "viewonly", N_("View only"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL }
+};
+
+/* Array for available features.
+ * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */
+static const RemminaProtocolFeature remmina_plugin_vnc_features[] =
+{
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_QUALITY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_RADIO), "quality",
+ quality_list },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_COLOR, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_RADIO), "colordepth",
+ colordepth_list },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_VIEWONLY, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "viewonly",
+ N_("View only") },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_VNC_FEATURE_PREF_DISABLESERVERINPUT, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "disableserverinput",N_("Prevent local interaction on the server") },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_VNC_FEATURE_TOOL_REFRESH, N_("Refresh"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_VNC_FEATURE_TOOL_CHAT, N_("Open Chat…"), "face-smile", NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_VNC_FEATURE_TOOL_SENDCTRLALTDEL, N_("Send Ctrl+Alt+Delete"), NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, REMMINA_PLUGIN_VNC_FEATURE_SCALE, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_UNFOCUS, REMMINA_PLUGIN_VNC_FEATURE_UNFOCUS, NULL, NULL, NULL },
+#if LIBVNCSERVER_CHECK_VERSION_VERSION(0, 9, 14)
+ { REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE, REMMINA_PLUGIN_VNC_FEATURE_DYNRESUPDATE, NULL, NULL, NULL },
+#endif
+ { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL }
+};
+
+/* Protocol plugin definition and features */
+static RemminaProtocolPlugin remmina_plugin_vnc =
+{
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ VNC_PLUGIN_NAME, // Name
+ VNC_PLUGIN_DESCRIPTION, // Description
+ GETTEXT_PACKAGE, // Translation domain
+ VNC_PLUGIN_VERSION, // Version number
+ VNC_PLUGIN_APPICON, // Icon for normal connection
+ VNC_PLUGIN_SSH_APPICON, // Icon for SSH connection
+ remmina_plugin_vnc_basic_settings, // Array for basic settings
+ remmina_plugin_vnc_advanced_settings, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type
+ remmina_plugin_vnc_features, // Array for available features
+ remmina_plugin_vnc_init, // Plugin initialization
+ remmina_plugin_vnc_open_connection, // Plugin open connection
+ remmina_plugin_vnc_close_connection, // Plugin close connection
+ remmina_plugin_vnc_query_feature, // Query for available features
+ remmina_plugin_vnc_call_feature, // Call a feature
+ remmina_plugin_vnc_keystroke // Send a keystroke
+};
+
+/* Protocol plugin definition and features */
+static RemminaProtocolPlugin remmina_plugin_vnci =
+{
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ VNCI_PLUGIN_NAME, // Name
+ VNCI_PLUGIN_DESCRIPTION, // Description
+ GETTEXT_PACKAGE, // Translation domain
+ VERSION, // Version number
+ VNCI_PLUGIN_APPICON, // Icon for normal connection
+ VNCI_PLUGIN_SSH_APPICON, // Icon for SSH connection
+ remmina_plugin_vnci_basic_settings, // Array for basic settings
+ remmina_plugin_vnc_advanced_settings, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_REVERSE_TUNNEL, // SSH settings type
+ remmina_plugin_vnc_features, // Array for available features
+ remmina_plugin_vnc_init, // Plugin initialization
+ remmina_plugin_vnc_open_connection, // Plugin open connection
+ remmina_plugin_vnc_close_connection, // Plugin close connection
+ remmina_plugin_vnc_query_feature, // Query for available features
+ remmina_plugin_vnc_call_feature, // Call a feature
+ remmina_plugin_vnc_keystroke, // Send a keystroke
+ NULL, // No screenshot support available
+ NULL, // RCW map event
+ NULL // RCW unmap event
+};
+
+G_MODULE_EXPORT gboolean
+remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_service = service;
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+ if (!service->register_plugin((RemminaPlugin *)&remmina_plugin_vnc))
+ return FALSE;
+
+ if (!service->register_plugin((RemminaPlugin *)&remmina_plugin_vnci))
+ return FALSE;
+
+ return TRUE;
+}
diff --git a/plugins/vnc/vnc_plugin.h b/plugins/vnc/vnc_plugin.h
new file mode 100644
index 0000000..a0a1732
--- /dev/null
+++ b/plugins/vnc/vnc_plugin.h
@@ -0,0 +1,158 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+#include "common/remmina_plugin.h"
+
+#ifndef __PLUGIN_CONFIG_H
+#define __PLUGIN_CONFIG_H
+
+#define VNC_PLUGIN_NAME "VNC"
+#define VNC_PLUGIN_DESCRIPTION N_("Remmina VNC Plugin")
+#define VNC_PLUGIN_VERSION VERSION
+#define VNC_PLUGIN_APPICON "org.remmina.Remmina-vnc-symbolic"
+#define VNC_PLUGIN_SSH_APPICON "org.remmina.Remmina-vnc-ssh-symbolic"
+#define VNCI_PLUGIN_NAME "VNCI"
+#define VNCI_PLUGIN_DESCRIPTION N_("Remmina VNC listener Plugin")
+#define VNCI_PLUGIN_VERSION VERSION
+#define VNCI_PLUGIN_APPICON "org.remmina.Remmina-vnc-symbolic"
+#define VNCI_PLUGIN_SSH_APPICON "org.remmina.Remmina-vnc-ssh-symbolic"
+#endif
+
+#define REMMINA_PLUGIN_INFO(fmt, ...) \
+ remmina_plugin_service->_remmina_info(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_MESSAGE(fmt, ...) \
+ remmina_plugin_service->_remmina_message(__func, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) \
+ remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_WARNING(fmt, ...) \
+ remmina_plugin_service->_remmina_warning(__func__, fmt, ##__VA_ARGS__)
+
+/* This will intentionally crash Remmina */
+#define REMMINA_PLUGIN_ERROR(fmt, ...) \
+ remmina_plugin_service->_remmina_error(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_CRITICAL(fmt, ...) \
+ remmina_plugin_service->_remmina_critical(__func__, fmt, ##__VA_ARGS__)
+#define REMMINA_PLUGIN_AUDIT(fmt, ...) \
+ remmina_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__)
+
+#define LIBVNCSERVER_CHECK_VERSION_VERSION(major,minor,patchlevel) \
+ (LIBVNC_INT_MAJOR > (major) || \
+ (LIBVNC_INT_MAJOR == (major) && LIBVNC_INT_MINOR > (minor)) || \
+ (LIBVNC_INT_MAJOR == (major) && LIBVNC_INT_MINOR == (minor) && \
+ LIBVNC_INT_PATCH >= (patchlevel)))
+
+typedef struct _RemminaPluginVncData {
+ /* Whether the user requests to connect/disconnect */
+ gboolean connected;
+ /* Whether the vnc process is running */
+ gboolean running;
+ /* Whether the initialization calls the authentication process */
+ gboolean auth_called;
+ /* Whether it is the first attempt for authentication. Only first attempt will try to use cached credentials */
+ gboolean auth_first;
+
+ GtkWidget * drawing_area;
+ guchar * vnc_buffer;
+ cairo_surface_t * rgb_buffer;
+
+ gint queuedraw_x, queuedraw_y, queuedraw_w, queuedraw_h;
+ guint queuedraw_handler;
+
+ gulong clipboard_handler;
+ GDateTime *clipboard_timer;
+
+ cairo_surface_t * queuecursor_surface;
+ gint queuecursor_x, queuecursor_y;
+ guint queuecursor_handler;
+
+ gpointer client;
+ gint listen_sock;
+
+ gint button_mask;
+
+ GPtrArray * pressed_keys;
+
+ pthread_mutex_t vnc_event_queue_mutex;
+ GQueue * vnc_event_queue;
+ gint vnc_event_pipe[2];
+
+ pthread_t thread;
+ pthread_mutex_t buffer_mutex;
+
+ float scroll_x_accumulator, scroll_y_accumulator;
+
+} RemminaPluginVncData;
+
+enum {
+ REMMINA_PLUGIN_VNC_EVENT_KEY,
+ REMMINA_PLUGIN_VNC_EVENT_POINTER,
+ REMMINA_PLUGIN_VNC_EVENT_CUTTEXT,
+ REMMINA_PLUGIN_VNC_EVENT_CHAT_OPEN,
+ REMMINA_PLUGIN_VNC_EVENT_CHAT_SEND,
+ REMMINA_PLUGIN_VNC_EVENT_CHAT_CLOSE
+};
+
+typedef struct _RemminaPluginVncEvent {
+ gint event_type;
+ union {
+ struct {
+ guint keyval;
+ gboolean pressed;
+ } key;
+ struct {
+ gint x;
+ gint y;
+ gint button_mask;
+ } pointer;
+ struct {
+ gchar *text;
+ } text;
+ } event_data;
+} RemminaPluginVncEvent;
+
+typedef struct _RemminaPluginVncCoordinates {
+ gint x, y;
+} RemminaPluginVncCoordinates;
+
+G_BEGIN_DECLS
+
+/* --------- Support for execution on main thread of GUI functions -------------- */
+static void remmina_plugin_vnc_update_scale(RemminaProtocolWidget *gp, gboolean scale);
+
+G_END_DECLS
diff --git a/plugins/www/CMakeLists.txt b/plugins/www/CMakeLists.txt
new file mode 100644
index 0000000..f9aa907
--- /dev/null
+++ b/plugins/www/CMakeLists.txt
@@ -0,0 +1,72 @@
+# remmina-plugin-www - The GTK+ Remote Desktop Client
+#
+# Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. If you
+# do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+
+set(REMMINA_PLUGIN_WWW_SRCS
+ www_utils.c
+ www_utils.h
+ www_config.h
+ www_plugin.c
+ www_plugin.h
+)
+
+add_library(remmina-plugin-www MODULE ${REMMINA_PLUGIN_WWW_SRCS})
+set_target_properties(remmina-plugin-www PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-www PROPERTIES NO_SONAME 1)
+
+add_definitions(${WEBKIT2GTK_CFLAGS_OTHER})
+
+find_required_package(LIBSOUP)
+if(LIBSOUP_FOUND)
+ include_directories(${REMMINA_COMMON_INCLUDE_DIRS} ${WEBKIT2GTK_INCLUDE_DIRS} ${LIBSOUP_INCLUDE_DIRS})
+ target_link_libraries(remmina-plugin-www ${REMMINA_COMMON_LIBRARIES} ${LIBSOUP_LIBRARIES} ${WEBKIT2GTK_LIBRARIES})
+else()
+ message(FATAL_ERROR "libsoup library not found")
+endif()
+
+install(TARGETS remmina-plugin-www DESTINATION ${REMMINA_PLUGINDIR})
+
+install(FILES
+ scalable/emblems/org.remmina.Remmina-www-symbolic.svg
+ DESTINATION ${APPICONSCALE_EMBLEMS_DIR})
+
+if(WITH_ICON_CACHE)
+ gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor")
+endif()
+
+# In the feature we will have more resources to add, so I use a GLOB
+file(GLOB WWW_RES_FILES "${CMAKE_CURRENT_SOURCE_DIR}/resources/js/www-js.js")
+foreach(ITEM ${WWW_RES_FILES})
+ list( APPEND FILES_TO_DEPLOY "${ITEM}" )
+endforeach()
+install( FILES ${FILES_TO_DEPLOY} DESTINATION "${REMMINA_RESDIR}" )
+
diff --git a/plugins/www/resources/js/.eslintrc.json b/plugins/www/resources/js/.eslintrc.json
new file mode 100644
index 0000000..8f6a415
--- /dev/null
+++ b/plugins/www/resources/js/.eslintrc.json
@@ -0,0 +1,13 @@
+{
+ "env": {
+ "browser": true,
+ "es2021": true
+ },
+ "extends": "eslint:recommended",
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "sourceType": "module"
+ },
+ "rules": {
+ }
+}
diff --git a/plugins/www/resources/js/package.json b/plugins/www/resources/js/package.json
new file mode 100644
index 0000000..70b8fe8
--- /dev/null
+++ b/plugins/www/resources/js/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "js",
+ "version": "1.0.0",
+ "description": "",
+ "main": "www-js.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Antenore Gatta",
+ "license": "GPL-2.0"
+}
diff --git a/plugins/www/resources/js/www-js.js b/plugins/www/resources/js/www-js.js
new file mode 100644
index 0000000..06980c9
--- /dev/null
+++ b/plugins/www/resources/js/www-js.js
@@ -0,0 +1,98 @@
+/* jshint esversion: 6 */
+
+/** A quick function to find and fill login fields */
+function setLoginFields () {
+ const evt = new Event('change')
+
+ const frames = window.frames
+
+ let doc
+ let pswdField
+ let usrField
+ let formNode
+ let userFound = false
+
+ if (frames.length !== 0) {
+ for (let i = 0; i < frames.length; i++) {
+ doc = frames[i].document
+ pswdField = doc.querySelectorAll('input[type=\'password\']')
+ if ((pswdField !== undefined) && (pswdField !== null)) {
+ break
+ }
+ }
+ if ((pswdField === undefined) || (pswdField === null)) {
+ /* What if we don't have login forms in the iFrame? -> window */
+ doc = window.document
+ pswdField = doc.querySelectorAll('input[type=\'password\']')
+ }
+ } else {
+ doc = window.document
+ pswdField = doc.querySelectorAll('input[type=\'password\']')
+ }
+
+ if (pswdField !== undefined) {
+ pswdField.forEach(function (pswdElement) {
+ if (pswdElement.getAttribute('autocomplete') !== 'new-password') {
+ pswdElement.value = 'PWDPLACEHOLDER'
+ }
+
+ formNode = pswdElement.form
+ if (formNode !== null) {
+ console.debug('Form elements found')
+ usrField = formNode.querySelectorAll('input[type=\'text\']')
+
+ usrField.forEach(function (usrElement) {
+ usrElement.value = 'USRPLACEHOLDER'
+ if (usrElement !== null) {
+ usrElement.dispatchEvent(evt)
+ userFound = true
+ }
+ })
+ pswdElement.dispatchEvent(evt)
+ }
+ if (formNode === null || !userFound) {
+ console.debug('Form elements found')
+ console.debug('Inputs elements may be in other containers')
+ const inputs = doc.getElementsByTagName('input')
+ for (let i = 0; i < inputs.length; i += 1) {
+ console.debug('input type: ' + inputs[i].type)
+ switch (inputs[i].type) {
+ case 'new-password':
+ continue
+ case 'password':
+ continue
+ case 'hidden':
+ continue
+ case 'email':
+ inputs[i].value = 'USRPLACEHOLDER'
+ userFound = true
+ break
+ case 'text':
+ if (!userFound) {
+ inputs[i].value = 'USRPLACEHOLDER'
+ userFound = true
+ }
+ break
+ default:
+ console.debug('Tentativily add username if no userFound')
+ if (!userFound) {
+ inputs[i].value = 'USRPLACEHOLDER'
+ userFound = true
+ }
+ // code block
+ }
+ if (userFound) {
+ inputs[i].dispatchEvent(evt)
+ console.debug('Username field found and set(?)')
+ break
+ }
+ }
+ }
+ })
+ } else {
+ console.debug('We already have a password')
+
+ }
+}
+
+setLoginFields()
diff --git a/plugins/www/scalable/emblems/org.remmina.Remmina-www-symbolic.svg b/plugins/www/scalable/emblems/org.remmina.Remmina-www-symbolic.svg
new file mode 100644
index 0000000..23a1e7b
--- /dev/null
+++ b/plugins/www/scalable/emblems/org.remmina.Remmina-www-symbolic.svg
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="100"
+ height="100"
+ viewBox="0 0 26.458334 26.458333"
+ version="1.1"
+ id="svg8"
+ inkscape:version="0.92.4 5da689c313, 2019-01-14"
+ sodipodi:docname="remmina-www-symbolic.svg">
+ <title
+ id="title4674">remmina-resail-symbolic</title>
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="4.1973765"
+ inkscape:cx="-123.70227"
+ inkscape:cy="21.504897"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ borderlayer="true"
+ inkscape:showpageshadow="true"
+ units="px"
+ inkscape:pagecheckerboard="false"
+ showguides="false"
+ inkscape:window-width="3840"
+ inkscape:window-height="2070"
+ inkscape:window-x="0"
+ inkscape:window-y="30"
+ inkscape:window-maximized="0"
+ objecttolerance="10"
+ guidetolerance="10"
+ inkscape:snap-tangential="true"
+ inkscape:snap-perpendicular="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid10"
+ dotted="false"
+ originx="-190.61239"
+ originy="-78.157086" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>remmina-resail-symbolic</dc:title>
+ <cc:license
+ rdf:resource="https://creativecommons.org/licenses/by-sa/3.0/" />
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Antenore Gatta</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:rights>
+ <cc:Agent>
+ <dc:title>Antenore Gatta 2019</dc:title>
+ </cc:Agent>
+ </dc:rights>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-190.61238,-192.38458)">
+ <g
+ style="isolation:isolate"
+ id="Group"
+ transform="matrix(0.26458333,0,0,0.26458333,153.78079,124.26165)" />
+ <g
+ style="isolation:isolate"
+ id="g62"
+ transform="matrix(0.26458333,0,0,0.26458333,153.78079,124.26165)">
+ <g
+ id="g52" />
+ <g
+ id="g54" />
+ <g
+ id="g56" />
+ <g
+ id="g60">
+ <g
+ id="g58" />
+ </g>
+ </g>
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:0.87999998;stroke:none;stroke-width:2.91110802;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 50 1.6816406 A 48.655695 48.318765 0 0 0 1.34375 50 A 48.655695 48.318765 0 0 0 50 98.318359 A 48.655695 48.318765 0 0 0 98.65625 50 A 48.655695 48.318765 0 0 0 50 1.6816406 z M 29.576172 15.396484 L 31.576172 67.396484 L 43.203125 16.888672 L 56.087891 16.888672 L 67.576172 67.396484 L 69.576172 15.396484 L 83.330078 18.236328 L 74.576172 85.396484 L 61.576172 85.132812 L 49.361328 33.128906 L 37.576172 85.396484 L 24.576172 85.396484 L 15.322266 18.236328 L 29.576172 15.396484 z "
+ transform="matrix(0.26458333,0,0,0.26458333,190.61238,192.38458)"
+ id="path4668" />
+ </g>
+</svg>
diff --git a/plugins/www/www_config.h b/plugins/www/www_config.h
new file mode 100644
index 0000000..225b7b2
--- /dev/null
+++ b/plugins/www/www_config.h
@@ -0,0 +1,46 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * Initially based on the plugin "Remmina Plugin EXEC", created and written by
+ * Fabio Castelli (Muflone) <muflone@vbsimple.net>.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+#ifndef __PLUGIN_CONFIG_H
+#define __PLUGIN_CONFIG_H
+
+#define PLUGIN_NAME "WWW"
+#define PLUGIN_DESCRIPTION N_("Remmina web-browser plugin")
+#define PLUGIN_VERSION "0.1.0"
+#define PLUGIN_APPICON "org.remmina.Remmina-www-symbolic"
+#endif
diff --git a/plugins/www/www_plugin.c b/plugins/www/www_plugin.c
new file mode 100644
index 0000000..b248e6b
--- /dev/null
+++ b/plugins/www/www_plugin.c
@@ -0,0 +1,987 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL.
+ * If you modify file(s) with this exception, you may extend this exception
+ * to your version of the file(s), but you are not obligated to do so.
+ * If you do not wish to do so, delete this exception statement from your
+ * version.
+ * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "www_config.h"
+
+#include "common/remmina_plugin.h"
+
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtk.h>
+#include <glib.h>
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <stdlib.h>
+
+#include <webkit2/webkit2.h>
+#if WEBKIT_CHECK_VERSION(2, 21, 1)
+#include <jsc/jsc.h>
+#endif
+#include "www_utils.h"
+#include "www_plugin.h"
+
+#define UNUSED(x) (void)(x)
+
+#define GET_PLUGIN_DATA(gp) (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data")
+
+typedef struct _RemminaPluginWWWData {
+ WWWWebViewDocumentType document_type;
+ GtkWidget * box;
+ WebKitSettings * settings;
+ WebKitWebContext * context;
+ WebKitWebsiteDataManager * data_mgr;
+ WebKitCredential * credentials;
+ WebKitAuthenticationRequest * request;
+ WebKitWebView * webview;
+ WebKitLoadEvent load_event;
+
+ gchar * url;
+ gboolean authenticated;
+ gboolean formauthenticated;
+} RemminaPluginWWWData;
+
+RemminaPluginService *remmina_plugin_service = NULL;
+
+void remmina_plugin_www_download_started(WebKitWebContext *context,
+ WebKitDownload *download, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ webkit_download_set_allow_overwrite(download, TRUE);
+ g_signal_connect(G_OBJECT(download), "notify::response",
+ G_CALLBACK(remmina_plugin_www_response_received), gp);
+ g_signal_connect(download, "created-destination",
+ G_CALLBACK(remmina_plugin_www_notify_download), gp);
+}
+
+void remmina_plugin_www_response_received(WebKitDownload *download, GParamSpec *ps, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Download response received");
+}
+
+void remmina_plugin_www_notify_download(WebKitDownload *download, gchar *destination, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Download is finished");
+ const gchar *dest = webkit_download_get_destination(download);
+
+ www_utils_send_notification("www-plugin-download-completed-id", _("File downloaded"), dest);
+ //download(gp, webkit_download_get_response(download));
+ //webkit_download_cancel(download);
+}
+
+static gboolean remmina_plugin_www_decide_policy_cb(
+ WebKitWebView * webview,
+ WebKitPolicyDecision * decision,
+ WebKitPolicyDecisionType decision_type,
+ RemminaProtocolWidget * gp)
+{
+ TRACE_CALL(__func__);
+
+ gboolean res = TRUE;
+
+ switch (decision_type) {
+ case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION:
+ remmina_plugin_www_decide_nav(decision, gp);
+ break;
+ case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION:
+ remmina_plugin_www_decide_newwin(decision, gp);
+ break;
+ case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
+ res = remmina_plugin_www_decide_resource(decision, gp);
+ break;
+ default:
+ webkit_policy_decision_ignore(decision);
+ break;
+ }
+ return res;
+}
+void remmina_plugin_www_decide_nav(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Policy decision navigation");
+ const gchar *url = NULL;
+ WebKitNavigationAction *a =
+ webkit_navigation_policy_decision_get_navigation_action(
+ WEBKIT_NAVIGATION_POLICY_DECISION(decision));
+
+ switch (webkit_navigation_action_get_navigation_type(a)) {
+ case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_LINK_CLICKED");
+ url = webkit_uri_request_get_uri(
+ webkit_navigation_action_get_request(a));
+ REMMINA_PLUGIN_DEBUG("url is %s ", url);
+ break;
+ case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED");
+ break;
+ case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_BACK_FORWARD");
+ break;
+ case WEBKIT_NAVIGATION_TYPE_RELOAD:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_RELOAD");
+ break;
+ case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED");
+ break;
+ case WEBKIT_NAVIGATION_TYPE_OTHER:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_OTHER");
+ break;
+ default:
+ /* Do not navigate to links with a "_blank" target (popup) */
+ if (webkit_navigation_policy_decision_get_frame_name(
+ WEBKIT_NAVIGATION_POLICY_DECISION(decision))) {
+ webkit_policy_decision_ignore(decision);
+ } else {
+ /* Filter out navigation to different domain ? */
+ /* get action→urirequest, copy and load in new window+view
+ * on Ctrl+Click ? */
+ webkit_policy_decision_use(decision);
+ }
+ break;
+ }
+}
+
+void remmina_plugin_www_on_create(WebKitWebView *webview, WebKitNavigationAction *a, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("New web-view");
+
+ const gchar *url = NULL;
+
+ RemminaPluginWWWData *gpdata;
+
+ gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data");
+
+ switch (webkit_navigation_action_get_navigation_type(a)) {
+ case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_LINK_CLICKED");
+ url = webkit_uri_request_get_uri(
+ webkit_navigation_action_get_request(a));
+ REMMINA_PLUGIN_DEBUG("Downloading url %s ", url);
+ WebKitDownload *d = webkit_web_view_download_uri(gpdata->webview, url);
+ UNUSED(d);
+
+ break;
+ case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED");
+ break;
+ case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_BACK_FORWARD");
+ break;
+ case WEBKIT_NAVIGATION_TYPE_RELOAD:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_RELOAD");
+ break;
+ case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED");
+ /* Filter domains here */
+ /* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event.
+ * test for link clicked but no button ? */
+ url = webkit_uri_request_get_uri(
+ webkit_navigation_action_get_request(a));
+ REMMINA_PLUGIN_DEBUG("Trying to open url: %s", url);
+ webkit_web_view_load_uri(gpdata->webview, url);
+ break;
+ case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_OTHER");
+ /* Filter domains here */
+ /* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event.
+ * test for link clicked but no button ? */
+ url = webkit_uri_request_get_uri(
+ webkit_navigation_action_get_request(a));
+ REMMINA_PLUGIN_DEBUG("Trying to open url: %s", url);
+ webkit_web_view_load_uri(gpdata->webview, url);
+ break;
+ default:
+ break;
+ }
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE is %d", webkit_navigation_action_get_navigation_type(a));
+}
+
+void remmina_plugin_www_decide_newwin(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Policy decision new window");
+
+ const gchar *url = NULL;
+
+ RemminaPluginWWWData *gpdata;
+
+ gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data");
+
+ WebKitNavigationAction *a =
+ webkit_navigation_policy_decision_get_navigation_action(
+ WEBKIT_NAVIGATION_POLICY_DECISION(decision));
+
+ switch (webkit_navigation_action_get_navigation_type(a)) {
+ case WEBKIT_NAVIGATION_TYPE_LINK_CLICKED:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_LINK_CLICKED");
+ url = webkit_uri_request_get_uri(
+ webkit_navigation_action_get_request(a));
+ REMMINA_PLUGIN_DEBUG("Downloading url %s ", url);
+ WebKitDownload *d = webkit_web_view_download_uri(gpdata->webview, url);
+ UNUSED(d);
+
+
+ break;
+ case WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_SUBMITTED");
+ break;
+ case WEBKIT_NAVIGATION_TYPE_BACK_FORWARD:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_BACK_FORWARD");
+ break;
+ case WEBKIT_NAVIGATION_TYPE_RELOAD:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_RELOAD");
+ break;
+ case WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED:
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_FORM_RESUBMITTED");
+ /* Filter domains here */
+ /* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event.
+ * test for link clicked but no button ? */
+ url = webkit_uri_request_get_uri(
+ webkit_navigation_action_get_request(a));
+ REMMINA_PLUGIN_DEBUG("Trying to open url: %s", url);
+ webkit_web_view_load_uri(gpdata->webview, url);
+ break;
+ case WEBKIT_NAVIGATION_TYPE_OTHER: /* fallthrough */
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE_OTHER");
+ /* Filter domains here */
+ /* If the value of “mouse-button” is not 0, then the navigation was triggered by a mouse event.
+ * test for link clicked but no button ? */
+ url = webkit_uri_request_get_uri(
+ webkit_navigation_action_get_request(a));
+ REMMINA_PLUGIN_DEBUG("Trying to open url: %s", url);
+ webkit_web_view_load_uri(gpdata->webview, url);
+ break;
+ default:
+ break;
+ }
+ REMMINA_PLUGIN_DEBUG("WEBKIT_NAVIGATION_TYPE is %d", webkit_navigation_action_get_navigation_type(a));
+
+ webkit_policy_decision_ignore(decision);
+}
+gboolean remmina_plugin_www_decide_resource(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Policy decision resource");
+ WebKitResponsePolicyDecision *response_decision =
+ WEBKIT_RESPONSE_POLICY_DECISION(decision);
+ WebKitURIResponse *response =
+ webkit_response_policy_decision_get_response(response_decision);;
+ const gchar *request_uri = webkit_uri_response_get_uri(response);
+
+ WebKitURIRequest *request;
+ WebKitWebResource *main_resource;
+ WWWWebViewDocumentType type;
+ const char *mime_type;
+
+ RemminaPluginWWWData *gpdata;
+
+ gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data");
+
+ mime_type = webkit_uri_response_get_mime_type(response);
+
+ REMMINA_PLUGIN_DEBUG("The media type is %s", mime_type);
+
+ /* If WebKit can't handle the media type, start the download
+ * process */
+ if (webkit_response_policy_decision_is_mime_type_supported(response_decision))
+ return FALSE;
+
+ /* If it's not the main resource we don't need to set the document type. */
+ request = webkit_response_policy_decision_get_request(response_decision);
+ request_uri = webkit_uri_request_get_uri(request);
+ main_resource = webkit_web_view_get_main_resource(gpdata->webview);
+ if (g_strcmp0(webkit_web_resource_get_uri(main_resource), request_uri) != 0)
+ return FALSE;
+
+ type = WWW_WEB_VIEW_DOCUMENT_OTHER;
+ if (!strcmp(mime_type, "text/html") || !strcmp(mime_type, "text/plain"))
+ type = WWW_WEB_VIEW_DOCUMENT_HTML;
+ else if (!strcmp(mime_type, "application/xhtml+xml"))
+ type = WWW_WEB_VIEW_DOCUMENT_XML;
+ else if (!strncmp(mime_type, "image/", 6))
+ type = WWW_WEB_VIEW_DOCUMENT_IMAGE;
+ else if (!strncmp(mime_type, "application/octet-stream", 6))
+ type = WWW_WEB_VIEW_DOCUMENT_OCTET_STREAM;
+
+ REMMINA_PLUGIN_DEBUG("Document type is %i", type);
+
+ /* FIXME: Maybe it makes more sense to have an API to query the media
+ * type when the load of a page starts than doing this here.
+ */
+ if (gpdata->document_type != type) {
+ gpdata->document_type = type;
+
+ //g_object_notify_by_pspec (G_OBJECT (webview), obj_properties[PROP_DOCUMENT_TYPE]);
+ }
+
+ webkit_policy_decision_download(decision);
+ return TRUE;
+}
+
+static void remmina_www_web_view_js_finished(GObject *object, GAsyncResult *result, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ WebKitJavascriptResult *js_result;
+ GError *error = NULL;
+
+ js_result = webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(object), result, &error);
+ if (!js_result) {
+ REMMINA_PLUGIN_DEBUG("Could not run JavaScript code: %s", error->message);
+ g_error_free(error);
+ return;
+ }
+
+#if WEBKIT_CHECK_VERSION(2, 21, 1)
+ gchar *str_value;
+ JSCValue *value = webkit_javascript_result_get_js_value(js_result);
+ if (jsc_value_is_string(value) || jsc_value_is_boolean(value)) {
+ JSCException *exception;
+
+ str_value = jsc_value_to_string(value);
+ exception = jsc_context_get_exception(jsc_value_get_context(value));
+ if (exception)
+ REMMINA_PLUGIN_DEBUG("Could not run JavaScript code: %s", jsc_exception_get_message(exception));
+ else
+ g_print("Script result: %s\n", str_value);
+ g_free(str_value);
+ } else {
+ str_value = jsc_value_to_string(value);
+ REMMINA_PLUGIN_DEBUG("Received something other than a string from JavaScript: %s", str_value);
+ g_free(str_value);
+ }
+#endif
+ if (js_result) webkit_javascript_result_unref(js_result);
+}
+
+static gboolean remmina_www_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ return TRUE;
+}
+
+static gboolean remmina_plugin_www_load_failed_tls_cb(WebKitWebView *webview, gchar *failing_uri, GTlsCertificate *certificate, GTlsCertificateFlags errors, RemminaProtocolWidget *gp) __attribute__ ((unused));
+static gboolean remmina_plugin_www_load_failed_tls_cb(WebKitWebView *webview,
+ gchar *failing_uri, GTlsCertificate *certificate,
+ GTlsCertificateFlags errors, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ /* Avoid failing if certificate is not good. TODO: Add widgets to let the user decide */
+ REMMINA_PLUGIN_DEBUG("Ignoring certificate and return TRUE");
+ return TRUE;
+}
+
+static void remmina_plugin_www_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginWWWData *gpdata;
+ RemminaFile *remminafile;
+ gchar *datapath;
+ gchar *cache_dir;
+
+ gpdata = g_new0(RemminaPluginWWWData, 1);
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ /* RemminaPluginWWWData initialization */
+
+ gpdata->authenticated = FALSE;
+ gpdata->formauthenticated = FALSE;
+ gpdata->document_type = WWW_WEB_VIEW_DOCUMENT_HTML;
+
+ datapath = g_build_path("/",
+ g_path_get_dirname(remmina_plugin_service->file_get_path(remminafile)),
+ PLUGIN_NAME,
+ NULL);
+ cache_dir = g_build_path("/", datapath, "cache", NULL);
+ REMMINA_PLUGIN_DEBUG("WWW data path is %s", datapath);
+
+ if (datapath) {
+ gchar *indexeddb_dir = g_build_filename(datapath, "indexeddb", NULL);
+ gchar *local_storage_dir = g_build_filename(datapath, "local_storage", NULL);
+ gchar *applications_dir = g_build_filename(datapath, "applications", NULL);
+ gchar *websql_dir = g_build_filename(datapath, "websql", NULL);
+ gpdata->data_mgr = webkit_website_data_manager_new(
+ "disk-cache-directory", cache_dir,
+ "indexeddb-directory", indexeddb_dir,
+ "local-storage-directory", local_storage_dir,
+ "offline-application-cache-directory", applications_dir,
+ "websql-directory", websql_dir,
+ NULL
+ );
+ g_free(indexeddb_dir);
+ g_free(local_storage_dir);
+ g_free(applications_dir);
+ g_free(websql_dir);
+ g_free(datapath);
+ } else {
+ gpdata->data_mgr = webkit_website_data_manager_new_ephemeral();
+ }
+
+
+ if (remmina_plugin_service->file_get_string(remminafile, "server"))
+ gpdata->url = g_strdup(remmina_plugin_service->file_get_string(remminafile, "server"));
+ else
+ gpdata->url = "https://remmina.org";
+ REMMINA_PLUGIN_DEBUG("URL is set to %s", gpdata->url);
+
+ gpdata->settings = webkit_settings_new();
+ gpdata->context = webkit_web_context_new_with_website_data_manager(gpdata->data_mgr);
+
+ /* enable-fullscreen, default TRUE, TODO: Try FALSE */
+
+#ifdef DEBUG
+ /* Turn on the developer extras */
+ webkit_settings_set_enable_developer_extras(gpdata->settings, TRUE);
+ webkit_settings_set_enable_write_console_messages_to_stdout(gpdata->settings, TRUE);
+#endif
+
+ /* allow-file-access-from-file-urls */
+ webkit_settings_set_allow_file_access_from_file_urls(gpdata->settings, TRUE);
+ /* allow-modal-dialogs */
+ webkit_settings_set_allow_modal_dialogs(gpdata->settings, TRUE);
+ /* enable-caret-browsing */
+ webkit_settings_set_enable_caret_browsing(gpdata->settings, TRUE);
+ /* enable-html5-database */
+ webkit_settings_set_enable_html5_database(gpdata->settings, TRUE);
+
+ /* user-agent. */
+ if (remmina_plugin_service->file_get_string(remminafile, "user-agent")) {
+ gchar *useragent = g_strdup(remmina_plugin_service->file_get_string(remminafile, "user-agent"));
+ webkit_settings_set_user_agent(gpdata->settings, useragent);
+ REMMINA_PLUGIN_DEBUG("User Agent set to: %s", useragent);
+ g_free(useragent);
+ }
+ /* enable-java */
+ if (remmina_plugin_service->file_get_int(remminafile, "enable-java", FALSE)) {
+ webkit_settings_set_enable_java(gpdata->settings, TRUE);
+ REMMINA_PLUGIN_DEBUG("Enable Java");
+ }
+ /* enable-smooth-scrolling */
+ if (remmina_plugin_service->file_get_int(remminafile, "enable-smooth-scrolling", FALSE)) {
+ webkit_settings_set_enable_smooth_scrolling(gpdata->settings, TRUE);
+ REMMINA_PLUGIN_DEBUG("enable-smooth-scrolling enabled");
+ }
+ /* enable-spatial-navigation */
+ if (remmina_plugin_service->file_get_int(remminafile, "enable-spatial-navigation", FALSE)) {
+ webkit_settings_set_enable_spatial_navigation(gpdata->settings, TRUE);
+ REMMINA_PLUGIN_DEBUG("enable-spatial-navigation enabled");
+ }
+ /* enable-webaudio */
+ if (remmina_plugin_service->file_get_int(remminafile, "enable-webaudio", FALSE)) {
+ webkit_settings_set_enable_webaudio(gpdata->settings, TRUE);
+ REMMINA_PLUGIN_DEBUG("enable-webaudio enabled");
+ }
+ /* enable-plugins */
+#if WEBKIT_CHECK_VERSION(2, 32, 0)
+#else
+ if (remmina_plugin_service->file_get_int(remminafile, "enable-plugins", FALSE)) {
+ webkit_settings_set_enable_plugins(gpdata->settings, TRUE);
+ REMMINA_PLUGIN_DEBUG("Enable plugins");
+ }
+#endif
+ /* enable-webgl */
+#if WEBKIT_CHECK_VERSION(2, 32, 0)
+#else
+ if (remmina_plugin_service->file_get_int(remminafile, "enable-webgl", FALSE)) {
+ webkit_settings_set_enable_webgl(gpdata->settings, TRUE);
+ webkit_settings_set_enable_accelerated_2d_canvas(gpdata->settings, TRUE);
+ REMMINA_PLUGIN_DEBUG("enable-webgl enabled");
+ }
+#endif
+
+ if (remmina_plugin_service->file_get_int(remminafile, "ignore-tls-errors", FALSE)) {
+#if WEBKIT_CHECK_VERSION(2, 32, 0)
+ webkit_website_data_manager_set_tls_errors_policy(
+ gpdata->data_mgr, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
+#else
+ webkit_web_context_set_tls_errors_policy(
+ gpdata->context, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
+#endif
+ REMMINA_PLUGIN_DEBUG("Ignore TLS errors");
+ }
+ if (remmina_plugin_service->file_get_string(remminafile, "proxy-url")) {
+ gchar *proxyurl = g_strdup(remmina_plugin_service->file_get_string(remminafile, "proxy-url"));
+ WebKitNetworkProxySettings *proxy_settings = webkit_network_proxy_settings_new(proxyurl, NULL);
+#if WEBKIT_CHECK_VERSION(2, 32, 0)
+ webkit_website_data_manager_set_network_proxy_settings(
+ gpdata->data_mgr, WEBKIT_NETWORK_PROXY_MODE_CUSTOM, proxy_settings);
+#else
+ webkit_web_context_set_network_proxy_settings(
+ gpdata->context, WEBKIT_NETWORK_PROXY_MODE_CUSTOM, proxy_settings);
+#endif
+ webkit_network_proxy_settings_free(proxy_settings);
+ g_free(proxyurl);
+ }
+
+ webkit_web_context_set_automation_allowed(gpdata->context, TRUE);
+ webkit_settings_set_javascript_can_open_windows_automatically(gpdata->settings, TRUE);
+ webkit_settings_set_allow_modal_dialogs(gpdata->settings, TRUE);
+
+#if !WEBKIT_CHECK_VERSION(2, 38, 0)
+ /** Frames flattening
+ * Some websites engage in embedding frames-inside-of-frames. WebKit has
+ * the ability to flatten them so they behave, when scrolling, as one big
+ * frame. If for some reason it is not enabled, go ahead and turn it on.
+ */
+ if (!webkit_settings_get_enable_frame_flattening(gpdata->settings))
+ webkit_settings_set_enable_frame_flattening(gpdata->settings, true);
+#endif
+
+ webkit_settings_set_enable_resizable_text_areas(gpdata->settings, true);
+
+ g_signal_connect(G_OBJECT(gpdata->context), "download-started",
+ G_CALLBACK(remmina_plugin_www_download_started), gp);
+}
+
+static gboolean remmina_plugin_www_on_auth(WebKitWebView *webview, WebKitAuthenticationRequest *request, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gchar *s_username, *s_password;
+ gint ret;
+ RemminaPluginWWWData *gpdata;
+ gboolean save;
+ gboolean disablepasswordstoring;
+ RemminaFile *remminafile;
+
+ gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data");
+
+ REMMINA_PLUGIN_DEBUG("Authenticate");
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ disablepasswordstoring = remmina_plugin_service->file_get_int(remminafile, "disablepasswordstoring", FALSE);
+ ret = remmina_plugin_service->protocol_plugin_init_auth(gp,
+ (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD) | REMMINA_MESSAGE_PANEL_FLAG_USERNAME,
+ _("Enter WWW authentication credentials"),
+ remmina_plugin_service->file_get_string(remminafile, "username"),
+ remmina_plugin_service->file_get_string(remminafile, "password"),
+ NULL,
+ NULL);
+ if (ret == GTK_RESPONSE_OK) {
+ s_username = remmina_plugin_service->protocol_plugin_init_get_username(gp);
+ s_password = remmina_plugin_service->protocol_plugin_init_get_password(gp);
+
+ save = remmina_plugin_service->protocol_plugin_init_get_savepassword(gp);
+ if (save) {
+ remmina_plugin_service->file_set_string(remminafile, "username", s_username);
+ remmina_plugin_service->file_set_string(remminafile, "password", s_password);
+ } else {
+ remmina_plugin_service->file_set_string(remminafile, "username", NULL);
+ remmina_plugin_service->file_set_string(remminafile, "password", NULL);
+ }
+ if (request) {
+ gpdata->credentials = webkit_credential_new(
+ g_strdup(s_username),
+ g_strdup(s_password),
+ WEBKIT_CREDENTIAL_PERSISTENCE_FOR_SESSION);
+ webkit_authentication_request_authenticate(request, gpdata->credentials);
+ webkit_credential_free(gpdata->credentials);
+ }
+ if (s_username) g_free(s_username);
+ if (s_password) g_free(s_password);
+ gpdata->authenticated = TRUE;
+ } else {
+ gpdata->authenticated = FALSE;
+ }
+
+ return gpdata->authenticated;
+}
+
+static void remmina_plugin_www_form_auth(WebKitWebView *webview,
+ WebKitLoadEvent load_event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gchar *s_js;
+ GString *jsstr;
+ RemminaPluginWWWData *gpdata;
+ RemminaFile *remminafile;
+ gchar *remmina_dir;
+ gchar *www_js_file = NULL;
+ GError *error = NULL;
+
+ gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data");
+ if (gpdata && !gpdata->formauthenticated)
+ gpdata->formauthenticated = FALSE;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ REMMINA_PLUGIN_DEBUG("load-changed emitted");
+
+
+ const gchar *const *dirs = g_get_system_data_dirs();
+
+ unsigned int i = 0;
+
+ for (i = 0; dirs[i] != NULL; ++i) {
+ remmina_dir = g_build_path("/", dirs[i], "remmina", "res", NULL);
+ GDir *system_data_dir = g_dir_open(remmina_dir, 0, &error);
+ // ignoring this error is ok, because the folder may not exists
+ if (error) {
+ g_error_free(error);
+ error = NULL;
+ } else {
+ if (system_data_dir) {
+ g_dir_close(system_data_dir);
+ g_free(www_js_file);
+ www_js_file = g_strdup_printf("%s/www-js.js", remmina_dir);
+ if (g_file_test(www_js_file, G_FILE_TEST_EXISTS))
+ break;
+ }
+ }
+ g_free(remmina_dir);
+ }
+
+ switch (load_event) {
+ case WEBKIT_LOAD_STARTED:
+ REMMINA_PLUGIN_DEBUG("Load started");
+ break;
+ case WEBKIT_LOAD_REDIRECTED:
+ REMMINA_PLUGIN_DEBUG("Load redirected");
+ break;
+ case WEBKIT_LOAD_COMMITTED:
+ /* The load is being performed. Current URI is
+ * the final one and it won't change unless a new
+ * load is requested or a navigation within the
+ * same page is performed
+ * uri = webkit_web_view_get_uri (webview); */
+ REMMINA_PLUGIN_DEBUG("Load committed");
+ break;
+ case WEBKIT_LOAD_FINISHED:
+ /* Load finished, we can now set user/password
+ * in the HTML form */
+ REMMINA_PLUGIN_DEBUG("Load finished");
+ if (gpdata && gpdata->formauthenticated == TRUE)
+ break;
+
+ if (remmina_plugin_service->file_get_string(remminafile, "username") ||
+ remmina_plugin_service->file_get_string(remminafile, "password")) {
+ REMMINA_PLUGIN_DEBUG("Authentication is enabled");
+ if (www_js_file) {
+ error = NULL;
+ if (g_file_get_contents(www_js_file, &s_js, NULL, &error)) {
+ jsstr = g_string_new(s_js);
+ if (remmina_plugin_service->file_get_string(remminafile, "username"))
+ www_utils_string_replace_all(jsstr, "USRPLACEHOLDER",
+ remmina_plugin_service->file_get_string(remminafile, "username"));
+ if (remmina_plugin_service->file_get_string(remminafile, "password"))
+ www_utils_string_replace_all(jsstr, "PWDPLACEHOLDER",
+ remmina_plugin_service->file_get_string(remminafile, "password"));
+ s_js = g_string_free(jsstr, FALSE);
+
+ if (!s_js || s_js[0] == '\0') {
+ break;
+ } else {
+ REMMINA_PLUGIN_DEBUG("Trying to send this JavaScript: %s", s_js);
+ webkit_web_view_run_javascript(
+ webview,
+ s_js,
+ NULL,
+ remmina_www_web_view_js_finished,
+ gp);
+ g_free(s_js);
+ }
+ } else {
+ if (error) {
+ REMMINA_PLUGIN_DEBUG("Unable to read file: %s\n", error->message);
+ g_error_free(error);
+ } else {
+ REMMINA_PLUGIN_DEBUG("Unable to read file. No error returned from glib.\n");
+ }
+ }
+ }
+ }
+ break;
+ }
+}
+
+static gboolean remmina_plugin_www_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginWWWData *gpdata;
+
+ gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data");
+
+ webkit_web_view_stop_loading(gpdata->webview);
+ webkit_web_view_try_close(gpdata->webview);
+
+ if (gpdata->url) g_free(gpdata->url);
+ gpdata->authenticated = FALSE;
+ gpdata->formauthenticated = FALSE;
+ gpdata->webview = NULL;
+ gpdata->data_mgr = NULL;
+ gpdata->settings = NULL;
+ gpdata->context = NULL;
+
+ /* Remove instance->context from gp object data to avoid double free */
+ g_object_steal_data(G_OBJECT(gp), "plugin-data");
+
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ return FALSE;
+}
+
+static gboolean remmina_plugin_www_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaPluginWWWData *gpdata;
+ RemminaFile *remminafile;
+
+ gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data");
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ gpdata->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add(GTK_CONTAINER(gp), gpdata->box);
+
+ remmina_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->box);
+
+ gpdata->webview = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(gpdata->context));
+ webkit_web_view_set_settings(gpdata->webview, gpdata->settings);
+
+ if (remmina_plugin_service->file_get_string(remminafile, "username") ||
+ remmina_plugin_service->file_get_string(remminafile, "password")) {
+ REMMINA_PLUGIN_DEBUG("Authentication is enabled");
+ remmina_plugin_www_on_auth(gpdata->webview, NULL, gp);
+ }
+
+ //"signal::load-failed-with-tls-errors", G_CALLBACK(remmina_plugin_www_load_failed_tls_cb), gp,
+ g_object_connect(
+ G_OBJECT(gpdata->webview),
+ "signal::create", G_CALLBACK(remmina_plugin_www_on_create), gp,
+ "signal::load-changed", G_CALLBACK(remmina_plugin_www_form_auth), gp,
+ "signal::authenticate", G_CALLBACK(remmina_plugin_www_on_auth), gp,
+ "signal::decide-policy", G_CALLBACK(remmina_plugin_www_decide_policy_cb), gp,
+ NULL);
+
+ gtk_widget_set_hexpand(GTK_WIDGET(gpdata->webview), TRUE);
+ gtk_widget_set_vexpand(GTK_WIDGET(gpdata->webview), TRUE);
+ gtk_container_add(GTK_CONTAINER(gpdata->box), GTK_WIDGET(gpdata->webview));
+ webkit_web_view_load_uri(gpdata->webview, gpdata->url);
+#ifdef DEBUG
+ if (remmina_plugin_service->file_get_int(remminafile, "enable-webinspector", FALSE)) {
+ REMMINA_PLUGIN_DEBUG("WebInspector enabled");
+ WebKitWebInspector *inspector = webkit_web_view_get_inspector(WEBKIT_WEB_VIEW(gpdata->webview));
+ webkit_web_inspector_attach(inspector);
+ webkit_web_inspector_show(WEBKIT_WEB_INSPECTOR(inspector));
+ }
+#endif
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+ gtk_widget_show_all(gpdata->box);
+
+ return TRUE;
+}
+
+static void remmina_plugin_www_save_snapshot(GObject *object, GAsyncResult *result, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ WebKitWebView *webview = WEBKIT_WEB_VIEW(object);
+
+ RemminaFile *remminafile;
+
+ GError *err = NULL;
+ cairo_surface_t *surface;
+ //unsigned char* buffer;
+ int width;
+ int height;
+ GdkPixbuf *screenshot;
+ GString *pngstr;
+ gchar *pngname;
+ //cairo_forma_t* cairo_format;
+ GDateTime *date = g_date_time_new_now_utc();
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ surface = webkit_web_view_get_snapshot_finish(WEBKIT_WEB_VIEW(webview), result, &err);
+ if (err)
+ REMMINA_PLUGIN_DEBUG("An error happened generating the snapshot: %s\n", err->message);
+ //buffer = cairo_image_surface_get_data (surface);
+ width = cairo_image_surface_get_width(surface);
+ height = cairo_image_surface_get_height(surface);
+ //cairo_format = cairo_image_surface_get_format (surface);
+
+ screenshot = gdk_pixbuf_get_from_surface(surface, 0, 0, width, height);
+ if (screenshot == NULL)
+ REMMINA_PLUGIN_DEBUG("WWW: gdk_pixbuf_get_from_surface failed");
+
+ pngstr = g_string_new(g_strdup_printf("%s/%s.png",
+ remmina_plugin_service->pref_get_value("screenshot_path"),
+ remmina_plugin_service->pref_get_value("screenshot_name")));
+ www_utils_string_replace_all(pngstr, "%p",
+ remmina_plugin_service->file_get_string(remminafile, "name"));
+ www_utils_string_replace_all(pngstr, "%h", "URL");
+ www_utils_string_replace_all(pngstr, "%Y",
+ g_strdup_printf("%d", g_date_time_get_year(date)));
+ www_utils_string_replace_all(pngstr, "%m", g_strdup_printf("%d",
+ g_date_time_get_month(date)));
+ www_utils_string_replace_all(pngstr, "%d",
+ g_strdup_printf("%d", g_date_time_get_day_of_month(date)));
+ www_utils_string_replace_all(pngstr, "%H",
+ g_strdup_printf("%d", g_date_time_get_hour(date)));
+ www_utils_string_replace_all(pngstr, "%M",
+ g_strdup_printf("%d", g_date_time_get_minute(date)));
+ www_utils_string_replace_all(pngstr, "%S",
+ g_strdup_printf("%f", g_date_time_get_seconds(date)));
+ g_date_time_unref(date);
+ pngname = g_string_free(pngstr, FALSE);
+ REMMINA_PLUGIN_DEBUG("Saving screenshot as %s", pngname);
+
+ cairo_surface_write_to_png(surface, pngname);
+ if (g_file_test(pngname, G_FILE_TEST_EXISTS))
+ www_utils_send_notification("www-plugin-screenshot-is-ready-id", _("Screenshot taken"), pngname);
+
+ cairo_surface_destroy(surface);
+}
+static gboolean remmina_plugin_www_get_snapshot(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginWWWData *gpdata;
+
+ gpdata = (RemminaPluginWWWData *)g_object_get_data(G_OBJECT(gp), "plugin-data");
+
+ webkit_web_view_get_snapshot(gpdata->webview,
+ WEBKIT_SNAPSHOT_REGION_FULL_DOCUMENT,
+ WEBKIT_SNAPSHOT_OPTIONS_NONE,
+ NULL,
+ (GAsyncReadyCallback)remmina_plugin_www_save_snapshot,
+ gp);
+ return FALSE;
+}
+
+/* Array of RemminaProtocolSetting for basic settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting tooltip
+ * g) Validation data pointer, will be passed to the validation callback method.
+ * h) Validation callback method (Can be NULL. Every entry will be valid then.)
+ * use following prototype:
+ * gboolean mysetting_validator_method(gpointer key, gpointer value,
+ * gpointer validator_data);
+ * gpointer key is a gchar* containing the setting's name,
+ * gpointer value contains the value which should be validated,
+ * gpointer validator_data contains your passed data.
+ */
+static const RemminaProtocolSetting remmina_plugin_www_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "server", N_("URL"), FALSE, NULL, N_("http://address or https://address"), NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("Password"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL }
+};
+
+/* Array of RemminaProtocolSetting for advanced settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting Tooltip
+ */
+static const RemminaProtocolSetting remmina_plugin_www_advanced_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "user-agent", N_("User agent"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "proxy-url", N_("Proxy URL"), FALSE, NULL, N_("E.g. https://example.org, socks://mysocks:1080") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-java", N_("Turn on Java support"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-smooth-scrolling", N_("Turn on smooth scrolling"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-spatial-navigation", N_("Turn on spatial navigation"), TRUE, NULL, NULL },
+#if WEBKIT_CHECK_VERSION(2, 32, 0)
+#else
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-plugins", N_("Turn on plugin support"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-webgl", N_("Turn on WebGL support"), TRUE, NULL, NULL },
+#endif
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-webaudio", N_("Turn on HTML5 audio support"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "ignore-tls-errors", N_("Ignore TLS errors"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Forget passwords after use"), TRUE, NULL, NULL },
+#ifdef DEBUG
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "enable-webinspector", N_("Turn on Web Inspector"), TRUE, NULL, NULL },
+#endif
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL }
+};
+
+/* Array for available features.
+ * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */
+static const RemminaProtocolFeature remmina_www_features[] =
+{
+ { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL }
+};
+
+/* Protocol plugin definition and features */
+static RemminaProtocolPlugin remmina_plugin =
+{
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ PLUGIN_NAME, // Name
+ PLUGIN_DESCRIPTION, // Description
+ GETTEXT_PACKAGE, // Translation domain
+ PLUGIN_VERSION, // Version number
+ PLUGIN_APPICON, // Icon for normal connection
+ NULL, // Icon for SSH connection
+ remmina_plugin_www_basic_settings, // Array for basic settings
+ remmina_plugin_www_advanced_settings, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type
+ remmina_www_features, // Array for available features
+ remmina_plugin_www_init, // Plugin initialization
+ remmina_plugin_www_open_connection, // Plugin open connection
+ remmina_plugin_www_close_connection, // Plugin close connection
+ remmina_www_query_feature, // Query for available features
+ NULL, // Call feature
+ NULL, // Send keystroke
+ remmina_plugin_www_get_snapshot, // Capture screenshot
+ NULL, // RCW map event
+ NULL // RCW unmap event
+};
+
+G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_service = service;
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+
+ if (!service->register_plugin((RemminaPlugin *)&remmina_plugin))
+ return FALSE;
+ return TRUE;
+}
diff --git a/plugins/www/www_plugin.h b/plugins/www/www_plugin.h
new file mode 100644
index 0000000..73caeba
--- /dev/null
+++ b/plugins/www/www_plugin.h
@@ -0,0 +1,80 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#include "common/remmina_plugin.h"
+
+#include <glib.h>
+#include <webkit2/webkit2.h>
+
+typedef enum {
+ WWW_WEB_VIEW_DOCUMENT_HTML,
+ WWW_WEB_VIEW_DOCUMENT_XML,
+ WWW_WEB_VIEW_DOCUMENT_IMAGE,
+ WWW_WEB_VIEW_DOCUMENT_OCTET_STREAM,
+ WWW_WEB_VIEW_DOCUMENT_OTHER
+} WWWWebViewDocumentType;
+
+extern RemminaPluginService *remmina_plugin_service;
+
+#define REMMINA_PLUGIN_INFO(fmt, ...) \
+ remmina_plugin_service->_remmina_info(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_MESSAGE(fmt, ...) \
+ remmina_plugin_service->_remmina_message(__func, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) \
+ remmina_plugin_service->_remmina_debug(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_WARNING(fmt, ...) \
+ remmina_plugin_service->_remmina_warning(__func__, fmt, ##__VA_ARGS__)
+
+/* This will intentionally crash Remmina */
+#define REMMINA_PLUGIN_ERROR(fmt, ...) \
+ remmina_plugin_service->_remmina_error(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_CRITICAL(fmt, ...) \
+ remmina_plugin_service->_remmina_critical(__func__, fmt, ##__VA_ARGS__)
+#define REMMINA_PLUGIN_AUDIT(fmt, ...) \
+ remmina_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__)
+
+G_BEGIN_DECLS
+void remmina_plugin_www_decide_nav(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp);
+void remmina_plugin_www_decide_newwin(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp);
+gboolean remmina_plugin_www_decide_resource(WebKitPolicyDecision *decision, RemminaProtocolWidget *gp);
+void remmina_plugin_www_response_received(WebKitDownload *download, GParamSpec *ps, RemminaProtocolWidget *gp);
+void remmina_plugin_www_notify_download(WebKitDownload *download, gchar *destination, RemminaProtocolWidget *gp);
+
+G_END_DECLS
diff --git a/plugins/www/www_utils.c b/plugins/www/www_utils.c
new file mode 100644
index 0000000..fe5fa0a
--- /dev/null
+++ b/plugins/www/www_utils.c
@@ -0,0 +1,154 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+/* Some utils taken form remmina_utils
+* TODO: use directly remmina_utils */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/utsname.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+#include "remmina/remmina_trace_calls.h"
+
+/** Returns @c TRUE if @a ptr is @c NULL or @c *ptr is @c FALSE. */
+#define EMPTY(ptr) \
+ (!(ptr) || !*(ptr))
+
+/* Used to send desktop notifications */
+void www_utils_send_notification(const gchar *notification_id,
+ const gchar *notification_title, const gchar *notification_message)
+{
+ TRACE_CALL(__func__);
+
+ GNotification *notification = g_notification_new(notification_title);
+ g_notification_set_body(notification, notification_message);
+#if GLIB_CHECK_VERSION(2, 42, 0)
+ g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_NORMAL);
+#endif
+ g_application_send_notification(g_application_get_default(), notification_id, notification);
+ g_object_unref(notification);
+}
+
+gint www_utils_strpos(const gchar *haystack, const gchar *needle)
+{
+ TRACE_CALL(__func__);
+ const gchar *sub;
+
+ if (!*needle)
+ return -1;
+
+ sub = strstr(haystack, needle);
+ if (!sub)
+ return -1;
+
+ return sub - haystack;
+}
+
+/* end can be -1 for haystack->len.
+ * returns: position of found text or -1.
+ * (C) Taken from geany */
+gint www_utils_string_find(GString *haystack, gint start, gint end, const gchar *needle)
+{
+ TRACE_CALL(__func__);
+ gint pos;
+
+ g_return_val_if_fail(haystack != NULL, -1);
+ if (haystack->len == 0)
+ return -1;
+
+ g_return_val_if_fail(start >= 0, -1);
+ if (start >= (gint)haystack->len)
+ return -1;
+
+ g_return_val_if_fail(!EMPTY(needle), -1);
+
+ if (end < 0)
+ end = haystack->len;
+
+ pos = www_utils_strpos(haystack->str + start, needle);
+ if (pos == -1)
+ return -1;
+
+ pos += start;
+ if (pos >= end)
+ return -1;
+ return pos;
+}
+
+/* Replaces @len characters from offset @a pos.
+ * len can be -1 to replace the remainder of @a str.
+ * returns: pos + strlen(replace).
+ * (C) Taken from geany */
+gint www_utils_string_replace(GString *str, gint pos, gint len, const gchar *replace)
+{
+ TRACE_CALL(__func__);
+ g_string_erase(str, pos, len);
+ if (replace) {
+ g_string_insert(str, pos, replace);
+ pos += strlen(replace);
+ }
+ return pos;
+}
+
+/**
+ * Replaces all occurrences of @a needle in @a haystack with @a replace.
+ *
+ * @param haystack The input string to operate on. This string is modified in place.
+ * @param needle The string which should be replaced.
+ * @param replace The replacement for @a needle.
+ *
+ * @return Number of replacements made.
+ **/
+guint www_utils_string_replace_all(GString *haystack, const gchar *needle, const gchar *replace)
+{
+ TRACE_CALL(__func__);
+ guint count = 0;
+ gint pos = 0;
+ gsize needle_length = strlen(needle);
+
+ while (1) {
+ pos = www_utils_string_find(haystack, pos, -1, needle);
+
+ if (pos == -1)
+ break;
+
+ pos = www_utils_string_replace(haystack, pos, needle_length, replace);
+ count++;
+ }
+ return count;
+}
diff --git a/plugins/www/www_utils.h b/plugins/www/www_utils.h
new file mode 100644
index 0000000..b5fab5e
--- /dev/null
+++ b/plugins/www/www_utils.h
@@ -0,0 +1,47 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta, Giovanni Panozzo
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+/* Some utils taken form remmina_utils
+* TODO: use directly remmina_utils */
+
+#pragma once
+
+#include "common/remmina_plugin.h"
+
+G_BEGIN_DECLS
+gint www_utils_string_find(GString *haystack, gint start, gint end, const gchar *needle);
+gint www_utils_string_replace(GString *str, gint pos, gint len, const gchar *replace);
+guint www_utils_string_replace_all(GString *haystack, const gchar *needle, const gchar *replace);
+void www_utils_send_notification(const gchar *notification_id, const gchar *notification_title, const gchar *notification_message);
+G_END_DECLS
diff --git a/plugins/x2go/CMakeLists.txt b/plugins/x2go/CMakeLists.txt
new file mode 100644
index 0000000..97df070
--- /dev/null
+++ b/plugins/x2go/CMakeLists.txt
@@ -0,0 +1,66 @@
+#
+# Project: Remmina Plugin X2Go
+# Description: Remmina protocol plugin to connect via X2Go using PyHoca
+# Based on Fabio Castelli Team Viewer Plugin
+# Copyright: 2013-2014 Fabio Castelli (Muflone)
+# Author: Antenore Gatta <antenore@simbiosi.org>
+# Copyright: 2015 Antenore Gatta
+# License: GPL-2+
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the
+# OpenSSL library under certain conditions as described in each
+# individual source file, and distribute linked combinations
+# including the two.
+# You must obey the GNU General Public License in all respects
+# for all of the code used other than OpenSSL. * If you modify
+# file(s) with this exception, you may extend this exception to your
+# version of the file(s), but you are not obligated to do so. * If you
+# do not wish to do so, delete this exception statement from your
+# version. * If you delete this exception statement from all source
+# files in the program, then also delete it here.
+
+set(REMMINA_PLUGIN_X2GO_SRCS
+ x2go_plugin.c
+ x2go_plugin.h
+ )
+
+add_library(remmina-plugin-x2go MODULE ${REMMINA_PLUGIN_X2GO_SRCS})
+set_target_properties(remmina-plugin-x2go PROPERTIES PREFIX "")
+set_target_properties(remmina-plugin-x2go PROPERTIES NO_SONAME 1)
+
+find_package(X11)
+
+include_directories(${REMMINA_COMMON_INCLUDE_DIRS}
+ ${XKBFILE_INCLUDE_DIRS} ${LIBSSH_INCLUDE_DIRS})
+target_link_libraries(remmina-plugin-x2go
+ ${REMMINA_COMMON_LIBRARIES}
+ ${XKBFILE_LIBRARIES}
+ ${LIBSSH_LIBRARIES}
+ ${X11_X11_LIB})
+
+install(TARGETS remmina-plugin-x2go DESTINATION ${REMMINA_PLUGINDIR})
+
+install(FILES
+ scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg
+ scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg
+ DESTINATION ${APPICONSCALE_EMBLEMS_DIR})
+
+if(WITH_ICON_CACHE)
+ gtk_update_icon_cache("${REMMINA_DATADIR}/icons/hicolor")
+endif()
diff --git a/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg
new file mode 100644
index 0000000..2cf9b92
--- /dev/null
+++ b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-ssh-symbolic.svg
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ width="128"
+ height="128"
+ sodipodi:docname="x2go-logo.svg"
+ sodipodi:docbase="/Users/h1/Desktop"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.0"
+ inkscape:export-filename="/Users/h1/Desktop/x2go-logo.png"
+ inkscape:export-xdpi="900"
+ inkscape:export-ydpi="900">
+ <metadata
+ id="metadata87">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>x2go Logo</dc:title>
+ <dc:date>12.06.2007</dc:date>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Heinz-M. Graesing</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:rights>
+ <cc:Agent>
+ <dc:title>obviously-nice</dc:title>
+ </cc:Agent>
+ </dc:rights>
+ <dc:publisher>
+ <cc:Agent>
+ <dc:title>obviously-nice</dc:title>
+ </cc:Agent>
+ </dc:publisher>
+ <dc:source>http://www.x2go.org/artwork</dc:source>
+ <dc:language>DE</dc:language>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>Logo</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ <cc:license
+ rdf:resource="http://creativecommons.org/licenses/by-nd/3.0/" />
+ </cc:Work>
+ <cc:License
+ rdf:about="http://creativecommons.org/licenses/by-nd/3.0/">
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Reproduction" />
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Distribution" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#Notice" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#Attribution" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs85">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 64 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="128 : 64 : 1"
+ inkscape:persp3d-origin="64 : 42.666667 : 1"
+ id="perspective46" />
+ </defs>
+ <sodipodi:namedview
+ inkscape:window-height="834"
+ inkscape:window-width="1295"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="true"
+ inkscape:object-nodes="true"
+ inkscape:grid-points="true"
+ inkscape:guide-points="true"
+ width="128px"
+ height="128px"
+ inkscape:zoom="3.9375"
+ inkscape:cx="64"
+ inkscape:cy="64"
+ inkscape:window-x="395"
+ inkscape:window-y="117"
+ inkscape:current-layer="svg2">
+ <inkscape:grid
+ id="GridFromPre046Settings"
+ type="xygrid"
+ originx="0px"
+ originy="0px"
+ spacingx="1px"
+ spacingy="1px"
+ color="#0000ff"
+ empcolor="#0000ff"
+ opacity="0.2"
+ empopacity="0.4"
+ empspacing="5" />
+ </sodipodi:namedview>
+ <g
+ style="fill:#4d4d4d"
+ id="g4"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 90,977.626 L 103.32,951.85 C 104.256,949.33 104.688,947.53 104.688,946.594 C 104.688,945.082 104.184,943.93 103.248,943.282 C 102.24,942.562 100.512,942.202 98.064,942.202 L 98.064,939.826 L 120.168,939.826 L 120.168,942.202 C 117.144,942.202 114.984,942.85 113.472,944.146 C 111.96,945.442 109.872,948.898 106.992,954.514 L 92.376,982.954 L 110.808,1024.498 C 112.68,1028.386 114.336,1031.122 115.776,1032.634 C 117.216,1034.074 118.944,1034.866 120.888,1034.866 L 120.888,1037.17 L 94.536,1037.17 L 94.536,1034.866 C 96.912,1034.866 98.568,1034.578 99.576,1033.93 C 100.584,1033.354 101.088,1032.346 101.088,1031.05 C 101.088,1029.826 100.368,1027.666 99.072,1024.498 L 86.112,995.482 L 71.28,1024.498 C 69.912,1026.946 69.192,1028.962 69.192,1030.474 C 69.192,1033.426 71.352,1034.866 75.672,1034.866 L 75.672,1037.17 L 55.728,1037.17 L 55.728,1034.866 C 57.672,1034.866 59.4,1034.218 60.84,1032.994 C 62.28,1031.77 63.792,1029.682 65.232,1026.586 L 83.736,990.01 L 67.248,952.498 C 65.52,948.466 63.936,945.802 62.424,944.362 C 60.84,942.922 58.608,942.202 55.728,942.202 L 55.728,939.826 L 84.024,939.826 L 84.024,942.202 C 79.632,942.202 77.4,943.714 77.4,946.738 C 77.4,948.25 77.832,949.978 78.696,951.85 L 90,977.626 z "
+ id="path6"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g8"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,940.474 L 50.976,939.25 L 50.976,941.698 L 6.984,940.474 z "
+ id="path10"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g12"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,946.45 L 50.976,945.298 L 50.976,946.954 L 56.16,947.026 L 56.52,947.026 L 56.808,947.026 L 57.096,947.098 L 57.384,947.098 L 57.672,947.17 L 57.888,947.242 L 58.104,947.314 L 58.32,947.386 L 58.464,947.458 L 58.608,947.53 L 58.752,947.602 L 58.896,947.674 L 58.968,947.746 L 59.112,947.818 L 59.112,947.89 L 59.184,947.89 L 6.984,946.45 z "
+ id="path14"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g16"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,952.498 L 61.272,950.986 L 61.488,951.346 L 61.776,951.922 L 62.064,952.498 L 62.352,953.146 L 62.64,953.794 L 62.784,954.01 L 6.984,952.498 z "
+ id="path18"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g20"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,958.474 L 63.936,956.89 L 65.376,960.058 L 6.984,958.474 z "
+ id="path22"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g24"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,964.45 L 66.528,962.866 L 67.968,966.106 L 6.984,964.45 z "
+ id="path26"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g28"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,970.498 L 69.12,968.77 L 70.632,972.226 L 6.984,970.498 z "
+ id="path30"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g32"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,976.474 L 71.712,974.674 L 73.296,978.274 L 6.984,976.474 z "
+ id="path34"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g36"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,982.45 L 74.304,980.65 L 75.96,984.322 L 6.984,982.45 z "
+ id="path38"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g40"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,988.426 L 76.896,986.554 L 78.408,989.866 L 78.192,990.37 L 6.984,988.426 z "
+ id="path42"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g44"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,994.402 L 77.112,992.458 L 75.168,996.274 L 6.984,994.402 z "
+ id="path46"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g48"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1000.378 L 74.016,998.578 L 72.216,1002.178 L 6.984,1000.378 z "
+ id="path50"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g52"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1006.354 L 70.92,1004.626 L 69.192,1008.082 L 6.984,1006.354 z "
+ id="path54"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g56"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1012.402 L 67.896,1010.746 L 66.168,1013.986 L 6.984,1012.402 z "
+ id="path58"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g60"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1036.378 L 50.976,1035.154 L 50.976,1037.602 L 6.984,1036.378 z "
+ id="path62"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g64"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1030.402 L 58.104,1028.962 L 58.032,1029.106 L 57.888,1029.178 L 57.744,1029.322 L 57.6,1029.394 L 57.528,1029.538 L 57.384,1029.61 L 57.312,1029.682 L 57.168,1029.682 L 57.024,1029.754 L 56.952,1029.826 L 56.808,1029.898 L 56.736,1029.898 L 56.592,1029.97 L 56.448,1029.97 L 56.376,1029.97 L 56.232,1030.042 L 56.088,1030.042 L 55.944,1030.042 L 50.976,1030.114 L 50.976,1031.554 L 6.984,1030.402 z "
+ id="path66"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g68"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1024.354 L 61.776,1022.914 L 60.984,1024.498 L 60.696,1025.002 L 60.48,1025.506 L 60.264,1025.866 L 6.984,1024.354 z "
+ id="path70"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g72"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1018.378 L 64.8,1016.794 L 63.288,1019.89 L 6.984,1018.378 z "
+ id="path74"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g76"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 63.936,924.49 L 118.872,924.49 C 123.84,924.49 127.872,928.522 127.872,933.49 L 127.872,1043.362 C 127.872,1048.33 123.84,1052.362 118.872,1052.362 L 63.936,1052.362 L 63.936,1050.418 L 117.864,1050.418 C 122.256,1050.418 125.856,1046.818 125.856,1042.426 L 125.856,934.498 C 125.856,930.106 122.256,926.506 117.864,926.506 L 63.936,926.506 L 63.936,924.49 z "
+ id="path78"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g80"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 9,924.49 L 63.936,924.49 L 63.936,926.506 L 9.936,926.506 C 5.544,926.506 1.944,930.106 1.944,934.498 L 1.944,1042.426 C 1.944,1046.818 5.544,1050.418 9.936,1050.418 L 63.936,1050.418 L 63.936,1052.362 L 9,1052.362 C 4.032,1052.362 0,1048.33 0,1043.362 L 0,933.49 C 0,928.522 4.032,924.49 9,924.49 z "
+ id="path82"
+ style="fill:#4d4d4d" />
+ </g>
+</svg>
diff --git a/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg
new file mode 100644
index 0000000..2cf9b92
--- /dev/null
+++ b/plugins/x2go/scalable/emblems/org.remmina.Remmina-x2go-symbolic.svg
@@ -0,0 +1,297 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ width="128"
+ height="128"
+ sodipodi:docname="x2go-logo.svg"
+ sodipodi:docbase="/Users/h1/Desktop"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.0"
+ inkscape:export-filename="/Users/h1/Desktop/x2go-logo.png"
+ inkscape:export-xdpi="900"
+ inkscape:export-ydpi="900">
+ <metadata
+ id="metadata87">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>x2go Logo</dc:title>
+ <dc:date>12.06.2007</dc:date>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Heinz-M. Graesing</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:rights>
+ <cc:Agent>
+ <dc:title>obviously-nice</dc:title>
+ </cc:Agent>
+ </dc:rights>
+ <dc:publisher>
+ <cc:Agent>
+ <dc:title>obviously-nice</dc:title>
+ </cc:Agent>
+ </dc:publisher>
+ <dc:source>http://www.x2go.org/artwork</dc:source>
+ <dc:language>DE</dc:language>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>Logo</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ <cc:license
+ rdf:resource="http://creativecommons.org/licenses/by-nd/3.0/" />
+ </cc:Work>
+ <cc:License
+ rdf:about="http://creativecommons.org/licenses/by-nd/3.0/">
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Reproduction" />
+ <cc:permits
+ rdf:resource="http://creativecommons.org/ns#Distribution" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#Notice" />
+ <cc:requires
+ rdf:resource="http://creativecommons.org/ns#Attribution" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs85">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 64 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="128 : 64 : 1"
+ inkscape:persp3d-origin="64 : 42.666667 : 1"
+ id="perspective46" />
+ </defs>
+ <sodipodi:namedview
+ inkscape:window-height="834"
+ inkscape:window-width="1295"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="true"
+ inkscape:object-nodes="true"
+ inkscape:grid-points="true"
+ inkscape:guide-points="true"
+ width="128px"
+ height="128px"
+ inkscape:zoom="3.9375"
+ inkscape:cx="64"
+ inkscape:cy="64"
+ inkscape:window-x="395"
+ inkscape:window-y="117"
+ inkscape:current-layer="svg2">
+ <inkscape:grid
+ id="GridFromPre046Settings"
+ type="xygrid"
+ originx="0px"
+ originy="0px"
+ spacingx="1px"
+ spacingy="1px"
+ color="#0000ff"
+ empcolor="#0000ff"
+ opacity="0.2"
+ empopacity="0.4"
+ empspacing="5" />
+ </sodipodi:namedview>
+ <g
+ style="fill:#4d4d4d"
+ id="g4"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 90,977.626 L 103.32,951.85 C 104.256,949.33 104.688,947.53 104.688,946.594 C 104.688,945.082 104.184,943.93 103.248,943.282 C 102.24,942.562 100.512,942.202 98.064,942.202 L 98.064,939.826 L 120.168,939.826 L 120.168,942.202 C 117.144,942.202 114.984,942.85 113.472,944.146 C 111.96,945.442 109.872,948.898 106.992,954.514 L 92.376,982.954 L 110.808,1024.498 C 112.68,1028.386 114.336,1031.122 115.776,1032.634 C 117.216,1034.074 118.944,1034.866 120.888,1034.866 L 120.888,1037.17 L 94.536,1037.17 L 94.536,1034.866 C 96.912,1034.866 98.568,1034.578 99.576,1033.93 C 100.584,1033.354 101.088,1032.346 101.088,1031.05 C 101.088,1029.826 100.368,1027.666 99.072,1024.498 L 86.112,995.482 L 71.28,1024.498 C 69.912,1026.946 69.192,1028.962 69.192,1030.474 C 69.192,1033.426 71.352,1034.866 75.672,1034.866 L 75.672,1037.17 L 55.728,1037.17 L 55.728,1034.866 C 57.672,1034.866 59.4,1034.218 60.84,1032.994 C 62.28,1031.77 63.792,1029.682 65.232,1026.586 L 83.736,990.01 L 67.248,952.498 C 65.52,948.466 63.936,945.802 62.424,944.362 C 60.84,942.922 58.608,942.202 55.728,942.202 L 55.728,939.826 L 84.024,939.826 L 84.024,942.202 C 79.632,942.202 77.4,943.714 77.4,946.738 C 77.4,948.25 77.832,949.978 78.696,951.85 L 90,977.626 z "
+ id="path6"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g8"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,940.474 L 50.976,939.25 L 50.976,941.698 L 6.984,940.474 z "
+ id="path10"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g12"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,946.45 L 50.976,945.298 L 50.976,946.954 L 56.16,947.026 L 56.52,947.026 L 56.808,947.026 L 57.096,947.098 L 57.384,947.098 L 57.672,947.17 L 57.888,947.242 L 58.104,947.314 L 58.32,947.386 L 58.464,947.458 L 58.608,947.53 L 58.752,947.602 L 58.896,947.674 L 58.968,947.746 L 59.112,947.818 L 59.112,947.89 L 59.184,947.89 L 6.984,946.45 z "
+ id="path14"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g16"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,952.498 L 61.272,950.986 L 61.488,951.346 L 61.776,951.922 L 62.064,952.498 L 62.352,953.146 L 62.64,953.794 L 62.784,954.01 L 6.984,952.498 z "
+ id="path18"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g20"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,958.474 L 63.936,956.89 L 65.376,960.058 L 6.984,958.474 z "
+ id="path22"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g24"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,964.45 L 66.528,962.866 L 67.968,966.106 L 6.984,964.45 z "
+ id="path26"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g28"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,970.498 L 69.12,968.77 L 70.632,972.226 L 6.984,970.498 z "
+ id="path30"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g32"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,976.474 L 71.712,974.674 L 73.296,978.274 L 6.984,976.474 z "
+ id="path34"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g36"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,982.45 L 74.304,980.65 L 75.96,984.322 L 6.984,982.45 z "
+ id="path38"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g40"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,988.426 L 76.896,986.554 L 78.408,989.866 L 78.192,990.37 L 6.984,988.426 z "
+ id="path42"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g44"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,994.402 L 77.112,992.458 L 75.168,996.274 L 6.984,994.402 z "
+ id="path46"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g48"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1000.378 L 74.016,998.578 L 72.216,1002.178 L 6.984,1000.378 z "
+ id="path50"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g52"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1006.354 L 70.92,1004.626 L 69.192,1008.082 L 6.984,1006.354 z "
+ id="path54"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g56"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1012.402 L 67.896,1010.746 L 66.168,1013.986 L 6.984,1012.402 z "
+ id="path58"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g60"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1036.378 L 50.976,1035.154 L 50.976,1037.602 L 6.984,1036.378 z "
+ id="path62"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g64"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1030.402 L 58.104,1028.962 L 58.032,1029.106 L 57.888,1029.178 L 57.744,1029.322 L 57.6,1029.394 L 57.528,1029.538 L 57.384,1029.61 L 57.312,1029.682 L 57.168,1029.682 L 57.024,1029.754 L 56.952,1029.826 L 56.808,1029.898 L 56.736,1029.898 L 56.592,1029.97 L 56.448,1029.97 L 56.376,1029.97 L 56.232,1030.042 L 56.088,1030.042 L 55.944,1030.042 L 50.976,1030.114 L 50.976,1031.554 L 6.984,1030.402 z "
+ id="path66"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g68"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1024.354 L 61.776,1022.914 L 60.984,1024.498 L 60.696,1025.002 L 60.48,1025.506 L 60.264,1025.866 L 6.984,1024.354 z "
+ id="path70"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g72"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 6.984,1018.378 L 64.8,1016.794 L 63.288,1019.89 L 6.984,1018.378 z "
+ id="path74"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g76"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 63.936,924.49 L 118.872,924.49 C 123.84,924.49 127.872,928.522 127.872,933.49 L 127.872,1043.362 C 127.872,1048.33 123.84,1052.362 118.872,1052.362 L 63.936,1052.362 L 63.936,1050.418 L 117.864,1050.418 C 122.256,1050.418 125.856,1046.818 125.856,1042.426 L 125.856,934.498 C 125.856,930.106 122.256,926.506 117.864,926.506 L 63.936,926.506 L 63.936,924.49 z "
+ id="path78"
+ style="fill:#4d4d4d" />
+ </g>
+ <g
+ style="fill:#4d4d4d"
+ id="g80"
+ transform="translate(0,-924.362)">
+ <path
+ d="M 9,924.49 L 63.936,924.49 L 63.936,926.506 L 9.936,926.506 C 5.544,926.506 1.944,930.106 1.944,934.498 L 1.944,1042.426 C 1.944,1046.818 5.544,1050.418 9.936,1050.418 L 63.936,1050.418 L 63.936,1052.362 L 9,1052.362 C 4.032,1052.362 0,1048.33 0,1043.362 L 0,933.49 C 0,928.522 4.032,924.49 9,924.49 z "
+ id="path82"
+ style="fill:#4d4d4d" />
+ </g>
+</svg>
diff --git a/plugins/x2go/x2go_plugin.c b/plugins/x2go/x2go_plugin.c
new file mode 100644
index 0000000..37ca272
--- /dev/null
+++ b/plugins/x2go/x2go_plugin.c
@@ -0,0 +1,3463 @@
+/*
+ * Project: Remmina Plugin X2Go
+ * Description: Remmina protocol plugin to connect via X2Go using PyHocaCLI
+ * Author: Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
+ * Antenore Gatta <antenore@simbiosi.org>
+ * Copyright: 2010-2011 Vic Lee
+ * 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * 2015 Antenore Gatta
+ * 2016-2018 Antenore Gatta, Giovanni Panozzo
+ * 2019 Mike Gabriel
+ * 2021 Daniel Teichmann
+ * License: GPL-2+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#include "x2go_plugin.h"
+#include "common/remmina_plugin.h"
+
+#include <gtk/gtkx.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#elif defined(GDK_WINDOWING_WAYLAND)
+#include <gdk/gdkwayland.h>
+#endif
+
+#include <X11/Xlib.h>
+#include <X11/XKBlib.h>
+#include <X11/extensions/XKBrules.h>
+
+#include <sys/types.h>
+#include <signal.h>
+#include <time.h>
+#include <ctype.h>
+
+#define FEATURE_AVAILABLE(gpdata, feature) \
+ gpdata->available_features ? (g_list_find_custom( \
+ gpdata->available_features, \
+ feature, \
+ (GCompareFunc) g_strcmp0 \
+ ) ? TRUE : FALSE) : FALSE
+
+#define FEATURE_NOT_AVAIL_STR(feature) \
+ g_strdup_printf(_("The command-line feature '%s' is not available! Attempting " \
+ "to start PyHoca-CLI without using this feature…"), feature)
+
+#define GET_PLUGIN_DATA(gp) \
+ (RemminaPluginX2GoData*) g_object_get_data(G_OBJECT(gp), "plugin-data")
+
+// --------- SESSIONS ------------
+#define SET_RESUME_SESSION(gp, resume_data) \
+ g_object_set_data_full(G_OBJECT(gp), "resume-session-data", \
+ resume_data, \
+ g_free)
+
+#define GET_RESUME_SESSION(gp) \
+ (gchar*) g_object_get_data(G_OBJECT(gp), "resume-session-data")
+
+// A session is selected if the returning value is something other than 0.
+#define IS_SESSION_SELECTED(gp) \
+ g_object_get_data(G_OBJECT(gp), "session-selected") ? TRUE : FALSE
+
+// We don't use the function as a real pointer but rather as a boolean value.
+#define SET_SESSION_SELECTED(gp, is_session_selected) \
+ g_object_set_data_full(G_OBJECT(gp), "session-selected", \
+ is_session_selected, \
+ NULL)
+// -------------------
+
+#define REMMINA_PLUGIN_INFO(fmt, ...) \
+ rm_plugin_service->_remmina_info("[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_MESSAGE(fmt, ...) \
+ rm_plugin_service->_remmina_message("[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_DEBUG(fmt, ...) \
+ rm_plugin_service->_remmina_debug(__func__, "[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_WARNING(fmt, ...) \
+ rm_plugin_service->_remmina_warning(__func__, "[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_AUDIT(fmt, ...) \
+ rm_plugin_service->_remmina_audit(__func__, fmt, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_ERROR(fmt, ...) \
+ rm_plugin_service->_remmina_error(__func__, "[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define REMMINA_PLUGIN_CRITICAL(fmt, ...) \
+ rm_plugin_service->_remmina_critical(__func__, "[%s] " fmt, \
+ PLUGIN_NAME, ##__VA_ARGS__)
+
+#define GET_PLUGIN_STRING(value) \
+ g_strdup(rm_plugin_service->file_get_string(remminafile, value))
+
+#define GET_PLUGIN_PASSWORD(value) \
+ GET_PLUGIN_STRING(value)
+
+#define GET_PLUGIN_INT(value, default_value) \
+ rm_plugin_service->file_get_int(remminafile, value, default_value)
+
+#define GET_PLUGIN_BOOLEAN(value) \
+ rm_plugin_service->file_get_int(remminafile, value, FALSE)
+
+static RemminaPluginService *rm_plugin_service = NULL;
+
+typedef struct _RemminaPluginX2GoData {
+ GtkWidget *socket;
+ gint socket_id;
+
+ pthread_t thread;
+
+ Display *display;
+ Window window_id;
+ int (*orig_handler)(Display *, XErrorEvent *);
+
+ GPid pidx2go;
+
+ gboolean disconnected;
+
+ GList* available_features;
+} RemminaPluginX2GoData;
+
+/**
+ * @brief Can be used to pass custom user data between functions and threads.
+ * *AND* pass the useful RemminaProtocolWidget with it along.
+ */
+typedef struct _X2GoCustomUserData {
+ RemminaProtocolWidget* gp;
+ gpointer dialog_data;
+ gpointer connect_data;
+ gpointer opt1;
+ gpointer opt2;
+} X2GoCustomUserData;
+
+/**
+ * @brief Used for the session chooser dialog (GtkListStore)
+ * See the example at: https://docs.gtk.org/gtk3/class.ListStore.html
+ * The order is the exact same as the user sees in the dialog.
+ * SESSION_NUM_PROPERTIES is used to keep count of the properties
+ * and it must be the last object.
+ */
+enum SESSION_PROPERTIES {
+ SESSION_DISPLAY = 0,
+ SESSION_STATUS,
+ SESSION_SESSION_ID,
+ SESSION_SUSPENDED_SINCE,
+ SESSION_CREATE_DATE,
+ SESSION_AGENT_PID,
+ SESSION_USERNAME,
+ SESSION_HOSTNAME,
+ SESSION_COOKIE,
+ SESSION_GRAPHIC_PORT,
+ SESSION_SND_PORT,
+ SESSION_SSHFS_PORT,
+ SESSION_DIALOG_IS_VISIBLE,
+ SESSION_NUM_PROPERTIES // Must be last. Counts all enum elements.
+};
+
+// Following str2int code was adapted from Stackoverflow:
+// https://stackoverflow.com/questions/7021725/how-to-convert-a-string-to-integer-in-c
+typedef enum _str2int_errno {
+ STR2INT_SUCCESS,
+ STR2INT_OVERFLOW,
+ STR2INT_UNDERFLOW,
+ STR2INT_INCONVERTIBLE,
+ STR2INT_INVALID_DATA
+} str2int_errno;
+
+/**
+ * @brief Convert string s to int out.
+ *
+ * @param out The converted int. Cannot be NULL.
+ *
+ * @param s Input string to be converted. \n
+ * The format is the same as strtol,
+ * except that the following are inconvertible: \n
+ * * empty string \n
+ * * leading whitespace \n
+ * * or any trailing characters that are not part of the number \n
+ * Cannot be NULL.
+ * @param base Base to interpret string in. Same range as strtol (2 to 36).
+ *
+ * @return Indicates if the operation succeeded, or why it failed with str2int_errno enum.
+ */
+str2int_errno str2int(gint *out, gchar *s, gint base)
+{
+ gchar *end;
+
+ if (!s || !out || base <= 0) return STR2INT_INVALID_DATA;
+
+ if (s[0] == '\0' || isspace(s[0])) return STR2INT_INCONVERTIBLE;
+
+ errno = 0;
+ glong l = strtol(s, &end, base);
+
+ /* Both checks are needed because INT_MAX == LONG_MAX is possible. */
+ if (l > INT_MAX || (errno == ERANGE && l == LONG_MAX)) return STR2INT_OVERFLOW;
+ if (l < INT_MIN || (errno == ERANGE && l == LONG_MIN)) return STR2INT_UNDERFLOW;
+ if (*end != '\0') return STR2INT_INCONVERTIBLE;
+
+ *out = l;
+ return STR2INT_SUCCESS;
+}
+
+/**
+ * DialogData:
+ * @param flags see GtkDialogFlags
+ * @param type see GtkMessageType
+ * @param buttons see GtkButtonsType
+ * @param title Title of the Dialog
+ * @param message Message of the Dialog
+ * @param callbackfunc A GCallback function which will be executed on the dialogs
+ * 'response' signal. Allowed to be NULL. \n
+ * The callback function is obliged to destroy the dialog widget. \n
+ * @param dialog_factory A user-defined callback function that is called when it is time
+ * to build the actual GtkDialog. \n
+ * Can be used to build custom dialogs. Allowed to be NULL.
+ *
+ *
+ * The `DialogData` structure contains all info needed to open a GTK dialog with
+ * rmplugin_x2go_open_dialog()
+ *
+ * Quick example of a callback function: \n
+ * static gboolean rmplugin_x2go_test_callback(RemminaProtocolWidget *gp, gint response_id, \n
+ * GtkDialog *self) { \n
+ * REMMINA_PLUGIN_DEBUG("response: %i", response_id); \n
+ * if (response_id == GTK_RESPONSE_OK) { \n
+ * REMMINA_PLUGIN_DEBUG("OK!"); \n
+ * } \n
+ * gtk_widget_destroy(self); \n
+ * return G_SOURCE_REMOVE; \n
+ * }
+ *
+ */
+struct _DialogData
+{
+ GtkWindow *parent;
+ GtkDialogFlags flags;
+ GtkMessageType type;
+ GtkButtonsType buttons;
+ gchar *title;
+ gchar *message;
+ GCallback callbackfunc;
+
+ // If the dialog needs to be custom.
+ GCallback dialog_factory_func;
+ gpointer dialog_factory_data;
+};
+
+/**
+ * @param custom_data X2GoCustomUserData structure with the following: \n
+ * gp -> gp (RemminaProtocolWidget*) \n
+ * dialog_data -> dialog data (struct _DialogData*)
+ * @returns: FALSE. This source should be removed from main loop.
+ * #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable
+ * names for the return value.
+ */
+static gboolean rmplugin_x2go_open_dialog(X2GoCustomUserData *custom_data)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!custom_data || !custom_data->gp || !custom_data->dialog_data) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Parameter 'custom_data' is not initialized!")
+ ));
+
+ return G_SOURCE_REMOVE;
+ }
+
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget*) custom_data->gp;
+ struct _DialogData *ddata = (struct _DialogData*) custom_data->dialog_data;
+
+ if (ddata) {
+ // Can't check type, flags or buttons
+ // because they are enums and '0' is a valid value
+ if (!ddata->title || !ddata->message) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Broken `DialogData`! Aborting…"));
+ return G_SOURCE_REMOVE;
+ }
+ } else {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Can't retrieve `DialogData`! Aborting…"));
+ return G_SOURCE_REMOVE;
+ }
+
+ REMMINA_PLUGIN_DEBUG("`DialogData` checks passed. Now showing dialog…");
+
+ GtkWidget* widget_gtk_dialog = NULL;
+
+ if (ddata->dialog_factory_func != NULL) {
+ REMMINA_PLUGIN_DEBUG("Calling *custom* dialog factory function…");
+ GCallback dialog_factory_func = G_CALLBACK(ddata->dialog_factory_func);
+ gpointer dialog_factory_data = ddata->dialog_factory_data;
+
+ // Calling dialog_factory_func(custom_data, dialog_factory_data);
+ widget_gtk_dialog = ((GtkWidget* (*)(X2GoCustomUserData*, gpointer))
+ dialog_factory_func)(custom_data, dialog_factory_data);
+ } else {
+ widget_gtk_dialog = gtk_message_dialog_new(ddata->parent,
+ ddata->flags,
+ ddata->type,
+ ddata->buttons,
+ "%s", ddata->title);
+
+ gtk_message_dialog_format_secondary_text(
+ GTK_MESSAGE_DIALOG(widget_gtk_dialog), "%s", ddata->message);
+ }
+
+ if (!widget_gtk_dialog) {
+ REMMINA_PLUGIN_CRITICAL("Error! Aborting.");
+ return G_SOURCE_REMOVE;
+ }
+
+ if (ddata->callbackfunc) {
+ g_signal_connect_swapped(G_OBJECT(widget_gtk_dialog), "response",
+ G_CALLBACK(ddata->callbackfunc),
+ custom_data);
+ } else {
+ g_signal_connect(G_OBJECT(widget_gtk_dialog), "response",
+ G_CALLBACK(gtk_widget_destroy),
+ NULL);
+ }
+
+ gtk_widget_show_all(widget_gtk_dialog);
+
+ // Delete ddata object and reference 'dialog-data' in gp.
+ g_object_set_data(G_OBJECT(gp), "dialog-data", NULL);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * @brief These define the responses of session-chooser-dialog's buttons.
+ */
+enum SESSION_CHOOSER_RESPONSE_TYPE {
+ SESSION_CHOOSER_RESPONSE_NEW = 0,
+ SESSION_CHOOSER_RESPONSE_CHOOSE,
+ SESSION_CHOOSER_RESPONSE_TERMINATE,
+};
+
+/**
+ * @brief Finds a child GtkWidget of a parent GtkWidget.
+ * Copied from https://stackoverflow.com/a/23497087 ;)
+ *
+ * @param parent Parent GtkWidget*
+ * @param name Name string of child. (Must be set before, er else it will be a
+ * default string)
+ * @return GtkWidget*
+ */
+static GtkWidget* rmplugin_x2go_find_child(GtkWidget* parent, const gchar* name)
+{
+ const gchar* parent_name = gtk_widget_get_name((GtkWidget*) parent);
+ if (g_ascii_strcasecmp(parent_name, (gchar*) name) == 0) {
+ return parent;
+ }
+
+ if (GTK_IS_BIN(parent)) {
+ GtkWidget *child = gtk_bin_get_child(GTK_BIN(parent));
+ return rmplugin_x2go_find_child(child, name);
+ }
+
+ if (GTK_IS_CONTAINER(parent)) {
+ GList *children = gtk_container_get_children(GTK_CONTAINER(parent));
+ while (children != NULL) {
+ GtkWidget *widget = rmplugin_x2go_find_child(children->data, name);
+ if (widget != NULL) {
+ return widget;
+ }
+
+ children = g_list_next(children);
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * @brief Gets executed on "row-activated" signal. It is emitted when the method when
+ * the user double clicks a treeview row. It is also emitted when a non-editable
+ * row is selected and one of the keys: Space, Shift+Space, Return or Enter is
+ * pressed.
+ *
+ * @param custom_data X2GoCustomUserData structure with the following: \n
+ * gp -> gp (RemminaProtocolWidget*) \n
+ * opt1 -> dialog widget (GtkWidget*)
+ */
+static gboolean rmplugin_x2go_session_chooser_row_activated(GtkTreeView *treeview,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column,
+ X2GoCustomUserData *custom_data)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!custom_data || !custom_data->gp || !custom_data->opt1) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Parameter 'custom_data' is not initialized!")
+ ));
+
+ return G_SOURCE_REMOVE;
+ }
+
+ RemminaProtocolWidget* gp = (RemminaProtocolWidget*) custom_data->gp;
+ // dialog_data (unused)
+ // connect_data (unused)
+ GtkWidget* dialog = GTK_WIDGET(custom_data->opt1);
+
+ gchar *session_id;
+ GtkTreeIter iter;
+ GtkTreeModel *model = gtk_tree_view_get_model(treeview);
+
+ if (gtk_tree_model_get_iter(model, &iter, path)) {
+ gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
+ SESSION_SESSION_ID, &session_id, -1);
+
+ // Silent bail out.
+ if (!session_id || strlen(session_id) <= 0) return G_SOURCE_REMOVE;
+
+ SET_RESUME_SESSION(gp, session_id);
+
+ // Unstucking main process. Telling it that a session has been selected.
+ // We use a trick here. As long as there is something other than 0
+ // stored, a session is selected. So we use the gpointer as a gboolean.
+ SET_SESSION_SELECTED(gp, (gpointer) TRUE);
+ gtk_widget_hide(GTK_WIDGET(dialog));
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * @brief Translates a session property (described by SESSION_PROPERTIES enum) to a
+ * string containing it's display name.
+ *
+ * @param session_property A session property. (as described by SESSION_PROPERTIES enum)
+ * @return gchar* Translated display name. (Can be NULL, if session_property is invalid!)
+ */
+static gchar *rmplugin_x2go_session_property_to_string(guint session_property) {
+ gchar* return_char = NULL;
+
+ switch (session_property) {
+ // I think we can close one eye here regarding max line-length.
+ case SESSION_DISPLAY: return_char = g_strdup(_("X Display")); break;
+ case SESSION_STATUS: return_char = g_strdup(_("Status")); break;
+ case SESSION_SESSION_ID: return_char = g_strdup(_("Session ID")); break;
+ case SESSION_CREATE_DATE: return_char = g_strdup(_("Create date")); break;
+ case SESSION_SUSPENDED_SINCE: return_char = g_strdup(_("Suspended since")); break;
+ case SESSION_AGENT_PID: return_char = g_strdup(_("Agent PID")); break;
+ case SESSION_USERNAME: return_char = g_strdup(_("Username")); break;
+ case SESSION_HOSTNAME: return_char = g_strdup(_("Hostname")); break;
+ case SESSION_COOKIE: return_char = g_strdup(_("Cookie")); break;
+ case SESSION_GRAPHIC_PORT: return_char = g_strdup(_("Graphic port")); break;
+ case SESSION_SND_PORT: return_char = g_strdup(_("SND port")); break;
+ case SESSION_SSHFS_PORT: return_char = g_strdup(_("SSHFS port")); break;
+ case SESSION_DIALOG_IS_VISIBLE: return_char = g_strdup(_("Visible")); break;
+ }
+
+ return return_char;
+}
+
+/**
+ * @brief Builds a dialog which contains all found X2Go-Sessions of the remote server.
+ * The dialog gives the user the option to choose between resuming or terminating
+ * an existing session or to create a new one.
+ *
+ * @param custom_data X2GoCustomUserData structure with the following: \n
+ * gp -> gp (RemminaProtocolWidget*) \n
+ * dialog_data -> dialog data (struct _DialogData*) \n
+ * connect_data -> connection data (struct _ConnectionData*)
+ * @param sessions_list The GList* Should contain all found X2Go-Sessions.
+ * Sessions are string arrays of properties.
+ * The type of the GList is gchar**.
+ *
+ * @returns GtkWidget* custom dialog.
+ */
+static GtkWidget* rmplugin_x2go_choose_session_dialog_factory(X2GoCustomUserData *custom_data,
+ GList *sessions_list)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!custom_data || !custom_data->gp ||
+ !custom_data->dialog_data || !custom_data->connect_data) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Parameter 'custom_data' is not initialized!")
+ ));
+
+ return NULL;
+ }
+
+ struct _DialogData* ddata = (struct _DialogData*) custom_data->dialog_data;
+
+ if (!ddata || !sessions_list || !ddata->title) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Could not retrieve valid `DialogData` or "
+ "`sessions_list`! Aborting…"));
+ return NULL;
+ }
+
+ GtkWidget *widget_gtk_dialog = NULL;
+ widget_gtk_dialog = gtk_dialog_new_with_buttons(ddata->title, ddata->parent,
+ ddata->flags,
+ // TRANSLATORS: Stick to x2goclient's translation for terminate.
+ _("_Terminate"),
+ SESSION_CHOOSER_RESPONSE_TERMINATE,
+ // TRANSLATORS: Stick to x2goclient's translation for resume.
+ _("_Resume"),
+ SESSION_CHOOSER_RESPONSE_CHOOSE,
+ _("_New"),
+ SESSION_CHOOSER_RESPONSE_NEW,
+ NULL);
+
+ GtkWidget *button = gtk_dialog_get_widget_for_response(
+ GTK_DIALOG(widget_gtk_dialog),
+ SESSION_CHOOSER_RESPONSE_TERMINATE);
+ // TRANSLATORS: Tooltip for terminating button inside Session-Chooser-Dialog.
+ // TRANSLATORS: Please stick to X2GoClient's way of translating.
+ gtk_widget_set_tooltip_text(button, _("Terminating X2Go sessions can take a moment."));
+
+ #define DEFAULT_DIALOG_WIDTH 720
+ #define DEFAULT_DIALOG_HEIGHT (DEFAULT_DIALOG_WIDTH * 9) / 16
+
+ gtk_widget_set_size_request(GTK_WIDGET(widget_gtk_dialog),
+ DEFAULT_DIALOG_WIDTH, DEFAULT_DIALOG_HEIGHT);
+ gtk_window_set_default_size(GTK_WINDOW(widget_gtk_dialog),
+ DEFAULT_DIALOG_WIDTH, DEFAULT_DIALOG_HEIGHT);
+
+ gtk_window_set_resizable(GTK_WINDOW(widget_gtk_dialog), TRUE);
+
+ GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL);
+ //gtk_widget_show(scrolled_window);
+
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(
+ GTK_DIALOG(widget_gtk_dialog))
+ ), GTK_WIDGET(scrolled_window), TRUE, TRUE, 5);
+
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (scrolled_window),
+ GTK_POLICY_AUTOMATIC,
+ GTK_POLICY_AUTOMATIC);
+
+
+ GType types[SESSION_NUM_PROPERTIES];
+
+ // First to last in SESSION_PROPERTIES.
+ for (gint i = 0; i < SESSION_NUM_PROPERTIES; ++i) {
+ // Everything is a String. (Except IS_VISIBLE flag)
+ // If that changes one day, you could extent the if statement here.
+ // But you would propably need a *lot* of refactoring.
+ // Especially in the session parser.
+ if (i == SESSION_DIALOG_IS_VISIBLE) {
+ types[i] = G_TYPE_BOOLEAN;
+ } else {
+ types[i] = G_TYPE_STRING;
+ }
+ }
+
+ // create tree view
+ GtkListStore *store = gtk_list_store_newv(SESSION_NUM_PROPERTIES, types);
+
+ GtkTreeModelFilter *filter = GTK_TREE_MODEL_FILTER(
+ gtk_tree_model_filter_new(GTK_TREE_MODEL(store),
+ NULL)
+ );
+ gtk_tree_model_filter_set_visible_column(filter, SESSION_DIALOG_IS_VISIBLE);
+
+ GtkWidget *tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(filter));
+ g_object_unref (G_OBJECT (store)); // tree now holds reference
+ gtk_widget_set_size_request(tree_view, -1, 300);
+
+ // Gets name to be findable by rmplugin_x2go_find_child()
+ gtk_widget_set_name(GTK_WIDGET(tree_view), "session_chooser_treeview");
+
+ // create list view columns
+ gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(tree_view), TRUE);
+ gtk_tree_view_set_headers_clickable (GTK_TREE_VIEW(tree_view), FALSE);
+ gtk_tree_view_set_enable_search(GTK_TREE_VIEW(tree_view), TRUE);
+ gtk_widget_show (tree_view);
+ gtk_container_add (GTK_CONTAINER(scrolled_window), tree_view);
+
+ GtkTreeViewColumn *tree_view_col = NULL;
+ GtkCellRenderer *cell_renderer = NULL;
+ gchar *header_title = NULL;
+
+ // First to last in SESSION_PROPERTIES.
+ for (guint i = 0; i < SESSION_NUM_PROPERTIES; ++i) {
+ // Do not display SESSION_DIALOG_IS_VISIBLE.
+ if (i == SESSION_DIALOG_IS_VISIBLE) continue;
+
+ header_title = rmplugin_x2go_session_property_to_string(i);
+ if (!header_title) {
+ REMMINA_PLUGIN_WARNING("%s", g_strdup_printf(
+ _("Internal error: %s"), g_strdup_printf(
+ _("Unknown property '%i'"), i
+ )));
+ header_title = g_strdup_printf(_("Unknown property '%i'"), i);
+ }
+
+ tree_view_col = gtk_tree_view_column_new();
+ gtk_tree_view_column_set_title(tree_view_col, header_title);
+ gtk_tree_view_column_set_clickable(tree_view_col, FALSE);
+ gtk_tree_view_column_set_sizing (tree_view_col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
+ gtk_tree_view_column_set_resizable(tree_view_col, TRUE);
+
+ cell_renderer = gtk_cell_renderer_text_new();
+ gtk_tree_view_column_pack_start(tree_view_col, cell_renderer, TRUE);
+ gtk_tree_view_column_add_attribute(tree_view_col, cell_renderer, "text", i);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree_view), tree_view_col);
+ }
+
+ GList *elem = NULL;
+ GtkTreeIter iter;
+
+ for (elem = sessions_list; elem; elem = elem->next) {
+ gchar** session = (gchar**) elem->data;
+ g_assert(session != NULL);
+
+ gtk_list_store_append(store, &iter);
+
+ for (gint i = 0; i < SESSION_NUM_PROPERTIES; i++) {
+ gchar* property = session[i];
+ GValue a = G_VALUE_INIT;
+
+ // Everything here is a string (except SESSION_DIALOG_IS_VISIBLE)
+
+ if (i == SESSION_DIALOG_IS_VISIBLE) {
+ g_value_init(&a, G_TYPE_BOOLEAN);
+ g_assert(G_VALUE_HOLDS_BOOLEAN(&a) && "GValue does not "
+ "hold a boolean!");
+ // Default is to show every new session.
+ g_value_set_boolean(&a, TRUE);
+ } else {
+ g_value_init(&a, G_TYPE_STRING);
+ g_assert(G_VALUE_HOLDS_STRING(&a) && "GValue does not "
+ "hold a string!");
+ g_value_set_static_string (&a, property);
+ }
+
+ gtk_list_store_set_value(store, &iter, i, &a);
+ }
+ }
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ * connect_data -> connection data (struct _ConnectionData*)
+ * opt1 -> dialog widget (GtkWidget*)
+ */
+ // everything else is already initialized.
+ custom_data->opt1 = widget_gtk_dialog;
+
+ g_signal_connect(tree_view, "row-activated",
+ G_CALLBACK(rmplugin_x2go_session_chooser_row_activated),
+ custom_data);
+
+ return widget_gtk_dialog;
+}
+
+/**
+ * @brief Uses either 'dialog' or 'treeview' to return the GtkTreeModel of the
+ * Session-Chooser-Dialog. Directly giving 'treeview' as a parameter is faster.
+ * Only *one* parameter has to be given. The other one can be NULL.
+ * Error messages are all handled already.
+ *
+ * @param dialog The Session-Chooser-Dialog itself. (Slower) Can be NULL.
+ * @param treeview The GtkTreeView of the Session-Chooser-Dialog. (faster) Can be NULL.
+ * @return GtkTreeModelFilter* (Does not contains all rows, only the visible ones!!!) \n
+ * But you can get all rows with: \n
+ * gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(return_model));
+ */
+static GtkTreeModelFilter* rmplugin_x2go_session_chooser_get_filter_model(GtkWidget *dialog,
+ GtkTreeView* treeview)
+{
+ //REMMINA_PLUGIN_DEBUG("Function entry.");
+ GtkTreeModel *return_model = NULL;
+
+ if (!treeview && dialog) {
+ GtkWidget *treeview_new = rmplugin_x2go_find_child(GTK_WIDGET(dialog),
+ "session_chooser_treeview");
+
+ if (!treeview_new) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Could not find child GtkTreeView of "
+ "session chooser dialog.")
+ ));
+ return NULL;
+ }
+
+ return_model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview_new));
+ } else if (treeview) {
+ return_model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
+ } else {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Neither the 'dialog' nor 'treeview' parameters are initialized! "
+ "At least one of them must be given.")
+ ));
+ return NULL;
+ }
+
+ if (!return_model || !GTK_TREE_MODEL_FILTER(return_model)) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Could not obtain \"GtkTreeModelFilter*\" of the session chooser dialog, "
+ "for unknown reason.")
+ ));
+ }
+
+ return GTK_TREE_MODEL_FILTER(return_model);
+}
+
+/**
+ * @brief Gets the selected row of the Session-Chooser-Dialog.
+ * The path gets converted with gtk_tree_model_filter_convert_child_path_to_path()
+ * before it gets returned. So path describes a row of 'filter' and *not* its
+ * child GtkTreeModel.
+ *
+ * @param dialog The Session-Chooser-Dialog.
+ * @return GtkTreePath* describing the path to the row.
+ */
+static GtkTreePath* rmplugin_x2go_session_chooser_get_selected_row(GtkWidget *dialog)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ GtkWidget *treeview = rmplugin_x2go_find_child(GTK_WIDGET(dialog),
+ "session_chooser_treeview");
+ if (!treeview) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Could not find child GtkTreeView of session chooser dialog.")
+ ));
+ return NULL;
+ }
+
+ GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
+ if (!selection) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Could not get currently selected row (session)!")
+ ));
+ return NULL;
+ }
+
+ GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(
+ NULL, GTK_TREE_VIEW(treeview));
+ GtkTreeModel *model = gtk_tree_model_filter_get_model(filter);
+ if (!model) return NULL; // error message was already handled.
+
+ GtkTreeModel *filter_model = GTK_TREE_MODEL(filter);
+ g_assert(filter_model && "Could not cast 'filter' to a GtkTreeModel!");
+ GList *selected_rows = gtk_tree_selection_get_selected_rows(selection, &filter_model);
+
+ // We only support single selection.
+ gint selected_rows_num = gtk_tree_selection_count_selected_rows(selection);
+ if (selected_rows_num != 1) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"), g_strdup_printf(
+ _("Exactly one session should be selectable but '%i' rows "
+ "(sessions) are selected."),
+ selected_rows_num
+ )));
+ return NULL;
+ }
+
+ // This would be very dangerous (we didn't check for NULL) if we hadn't just
+ // checked that only one row is selected.
+ GtkTreePath *path = selected_rows->data;
+
+ // Convert to be path of GtkTreeModelFilter and *not* its child GtkTreeModel.
+ path = gtk_tree_model_filter_convert_child_path_to_path(filter, path);
+
+ return path;
+}
+
+/**
+ * @brief Finds the GtkTreeView inside of the session chooser dialog,
+ * determines the selected row and extracts a property.
+ *
+ * @param dialog GtkWidget* the dialog itself.
+ * @param property_index Index of property.
+ * @param row A specific row to get the property of. (Can be NULL)
+ *
+ * @return GValue The value of property. (Can be non-initialized!)
+ */
+static GValue rmplugin_x2go_session_chooser_get_property(GtkWidget *dialog,
+ gint property_index,
+ GtkTreePath *row)
+{
+ //REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ GValue ret_value = G_VALUE_INIT;
+
+ if (!row) {
+ GtkTreePath *selected_row = rmplugin_x2go_session_chooser_get_selected_row(dialog);
+ if (!selected_row) return ret_value; // error message was already handled.
+ row = selected_row;
+ }
+
+ GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(dialog, NULL);
+ GtkTreeModel *model = gtk_tree_model_filter_get_model(filter);
+ if (!model) return ret_value; // error message was already handled.
+
+ GtkTreeIter iter;
+ gboolean success = gtk_tree_model_get_iter(model, &iter, row);
+ if (!success) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Failed to fill 'GtkTreeIter'.")
+ ));
+
+ return ret_value;
+ }
+
+ GValue property = G_VALUE_INIT;
+ gtk_tree_model_get_value(model, &iter, property_index, &property);
+
+ return property;
+}
+
+/**
+ * @brief This function dumps all properties of a session to the console.
+ * It can/should be used with: \n
+ * gtk_tree_model_foreach(GTK_TREE_MODEL(model), (GtkTreeModelForeachFunc) \n
+ * rmplugin_x2go_dump_session_properties, \n
+ * dialog);
+ */
+/*static void rmplugin_x2go_dump_session_properties(GtkTreeModel *model, GtkTreePath *path,
+ GtkTreeIter *iter, GtkWidget *dialog)
+{
+ //REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ g_debug(_("Properties for session with path '%s':"), gtk_tree_path_to_string(path));
+ for (guint i = 0; i < SESSION_NUM_PROPERTIES; i++) {
+ GValue property = G_VALUE_INIT;
+ property = rmplugin_x2go_session_chooser_get_property(dialog, i, path);
+
+ gchar* display_name = rmplugin_x2go_session_property_to_string(i);
+ g_assert(display_name && "Could not get display name for a property!");
+
+ if (i == SESSION_DIALOG_IS_VISIBLE) {
+ g_assert(G_VALUE_HOLDS_BOOLEAN(&property) && "GValue does not "
+ "hold a boolean!");
+ g_debug("\t%s: '%s'", display_name,
+ g_value_get_boolean(&property) ? "TRUE" : "FALSE");
+ } else {
+ g_assert(G_VALUE_HOLDS_STRING(&property) && "GValue does not "
+ "hold a string!");
+ g_debug("\t%s: '%s'", display_name, g_value_get_string(&property));
+ }
+ }
+}*/
+
+/**
+ * @brief This function synchronously spawns a pyhoca-cli process with argv as arguments.
+ * @param argc Number of arguments.
+ * @param argv Arguments as string array. \n
+ * Last elements has to be NULL. \n
+ * Strings will get freed automatically.
+ * @param error Will be filled with an error message on fail.
+ * @param env String array of enviroment variables. \n
+ * The list is NULL terminated and each item in
+ * the list is of the form `NAME=VALUE`.
+ *
+ * @returns Returns either standard output string or NULL if it failed.
+ */
+static gchar* rmplugin_x2go_spawn_pyhoca_process(guint argc, gchar* argv[],
+ GError** error, gchar** env)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!argv) {
+ gchar* errmsg = g_strdup_printf(
+ _("Internal error: %s"),
+ _("parameter 'argv' is 'NULL'.")
+ );
+ REMMINA_PLUGIN_CRITICAL("%s", errmsg);
+ g_set_error(error, 1, 1, "%s", errmsg);
+ return NULL;
+ }
+
+ if (!error) {
+ // Can't report error message back since 'error' is NULL.
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("parameter 'error' is 'NULL'.")
+ ));
+ return NULL;
+ }
+
+ if (!env || !env[0]) {
+ gchar* errmsg = g_strdup_printf(
+ _("Internal error: %s"),
+ _("parameter 'env' is either invalid or uninitialized.")
+ );
+ REMMINA_PLUGIN_CRITICAL("%s", errmsg);
+ g_set_error(error, 1, 1, "%s", errmsg);
+ return NULL;
+ }
+
+ gint exit_code = 0;
+ gchar *standard_out;
+ // Just supresses pyhoca-cli's help message when pyhoca-cli's version is too old.
+ gchar *standard_err;
+
+ gboolean success_ret = g_spawn_sync(NULL, argv, env, G_SPAWN_SEARCH_PATH, NULL,
+ NULL, &standard_out, &standard_err,
+ &exit_code, error);
+
+ REMMINA_PLUGIN_INFO("%s", _("Started PyHoca-CLI with the following arguments:"));
+ // Print every argument except passwords. Free all arg strings.
+ for (gint i = 0; i < argc - 1; i++) {
+ gchar* curr_arg = argv[i];
+
+ if (g_str_equal(curr_arg, "--password") ||
+ g_str_equal(curr_arg, "--ssh-passphrase")) {
+ g_printf("%s ", curr_arg);
+ g_printf("XXXXXX ");
+ g_free(curr_arg);
+ g_free(argv[++i]);
+ continue;
+ } else {
+ g_printf("%s ", curr_arg);
+ g_free(curr_arg);
+ }
+ }
+ g_printf("\n");
+
+ /* TOO VERBOSE: */
+ /*
+ REMMINA_PLUGIN_DEBUG("%s", _("Started PyHoca-CLI with the "
+ "following environment variables:"));
+ REMMINA_PLUGIN_DEBUG("%s", g_strjoinv("\n", env));
+ */
+
+ if (standard_err && strlen(standard_err) > 0) {
+ if (g_str_has_prefix(standard_err, "pyhoca-cli: error: a socket error "
+ "occured while establishing the connection:")) {
+ // Log error into GUI.
+ gchar* errmsg = g_strdup_printf(
+ _("The necessary PyHoca-CLI process has encountered a "
+ "internet connection problem.")
+ );
+
+ // Log error into debug window and stdout
+ REMMINA_PLUGIN_CRITICAL("%s:\n%s", errmsg, standard_err);
+ g_set_error(error, 1, 1, "%s", errmsg);
+ return NULL;
+ } else {
+ gchar* errmsg = g_strdup_printf(
+ _("Could not start "
+ "PyHoca-CLI:\n%s"),
+ standard_err
+ );
+ REMMINA_PLUGIN_CRITICAL("%s", errmsg);
+ g_set_error(error, 1, 1, "%s", errmsg);
+ return NULL;
+ }
+ } else if (!success_ret || (*error) || strlen(standard_out) <= 0 || exit_code) {
+ if (!(*error)) {
+ gchar* errmsg = g_strdup_printf(
+ _("An unknown error occured while trying "
+ "to start PyHoca-CLI. Exit code: %i"),
+ exit_code);
+ REMMINA_PLUGIN_WARNING("%s", errmsg);
+ g_set_error(error, 1, 1, "%s", errmsg);
+ } else {
+ gchar* errmsg = g_strdup_printf(
+ _("An unknown error occured while trying to start "
+ "PyHoca-CLI. Exit code: %i. Error: '%s'"),
+ exit_code, (*error)->message);
+ REMMINA_PLUGIN_WARNING("%s", errmsg);
+ }
+
+ return NULL;
+ }
+
+ return standard_out;
+}
+
+/**
+ * @brief Stores all necessary information needed for retrieving sessions from
+ * a X2Go server.
+ */
+struct _ConnectionData {
+ gchar* host;
+ gchar* username;
+ gchar* password;
+ gchar* ssh_privatekey;
+ gchar* ssh_passphrase;
+};
+
+/**
+ * @brief Either sets a specific row visible or invisible.
+ * Also handles 'terminate' and 'resume' buttons of session-chooser-dialog.
+ * If there are no sessions available anymore, disable all buttons which are not 'new'
+ * and if a session is available again, enable them.
+ *
+ * @param path Describes which row. (GtkTreePath*) \n
+ * Should be from GtkTreeModelFilter's perspective!
+ * @param value TRUE = row is visible & FALSE = row is invisible (gboolean)
+ * @param dialog Session-Chooser-Dialog (GtkDialog*)
+ * @return Returns TRUE if successful. (gboolean)
+ */
+static gboolean rmplugin_x2go_session_chooser_set_row_visible(GtkTreePath *path,
+ gboolean value,
+ GtkDialog *dialog) {
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!path || !dialog) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Neither the 'path' nor 'dialog' parameters are initialized.")
+ ));
+ return FALSE;
+ }
+
+ GtkTreeModelFilter *filter = rmplugin_x2go_session_chooser_get_filter_model(
+ GTK_WIDGET(dialog), NULL);
+ GtkTreeModel *model = gtk_tree_model_filter_get_model(filter);
+
+ // error message was already handled.
+ if (!model) return FALSE;
+
+ GtkTreeIter iter;
+ if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path)) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("GtkTreePath 'path' describes a non-existing row!")
+ ));
+ return FALSE;
+ }
+
+
+ // Make session either visible or invisible.
+ gtk_list_store_set(GTK_LIST_STORE(model), &iter,
+ SESSION_DIALOG_IS_VISIBLE, value, -1);
+
+ // Update row.
+ gtk_tree_model_row_changed(GTK_TREE_MODEL(model), path, &iter);
+
+ /* Get IS_VISIBLE flag of a session. */
+ // GValue ret_value = G_VALUE_INIT;
+ // ret_value = rmplugin_x2go_session_chooser_get_property(GTK_WIDGET(dialog),
+ // SESSION_DIALOG_IS_VISIBLE,
+ // path);
+ // g_debug("Is visible: %s", g_value_get_boolean(&ret_value) ? "TRUE" : "FALSE");
+
+
+ GtkWidget *term_button = gtk_dialog_get_widget_for_response(
+ GTK_DIALOG(dialog),
+ SESSION_CHOOSER_RESPONSE_TERMINATE);
+ GtkWidget *resume_button = gtk_dialog_get_widget_for_response(
+ GTK_DIALOG(dialog),
+ SESSION_CHOOSER_RESPONSE_CHOOSE);
+
+ // If no (visible) row is left to terminate disable terminate and resume buttons.
+ gint rows_amount = gtk_tree_model_iter_n_children(GTK_TREE_MODEL(filter), NULL);
+ if (rows_amount <= 0) {
+ gtk_widget_set_sensitive(term_button, FALSE);
+ gtk_widget_set_sensitive(resume_button, FALSE);
+ } else {
+ gtk_widget_set_sensitive(term_button, TRUE);
+ gtk_widget_set_sensitive(resume_button, TRUE);
+ }
+
+ // Success, yay!
+ return TRUE;
+}
+
+static gboolean rmplugin_x2go_verify_connection_data(struct _ConnectionData *connect_data) {
+ /* Check connect_data. */
+ if (!connect_data ||
+ !connect_data->host ||
+ !connect_data->username ||
+ strlen(connect_data->host) <= 0 ||
+ strlen(connect_data->username) <= 0)
+ {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("'Invalid connection data.'")
+ ));
+
+ return FALSE;
+ }
+
+ if (!connect_data->password && (!connect_data->ssh_privatekey ||
+ strlen(connect_data->ssh_privatekey) <= 0)) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("'Invalid connection data.'")
+ ));
+ } else {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * @brief Terminates a specific X2Go session using pyhoca-cli.
+ *
+ * @param custom_data X2GoCustomUserData structure with the following: \n
+ * gp -> gp (RemminaProtocolWidget*) \n
+ * dialog_data -> dialog data (struct _DialogData*) \n
+ * connect_data -> connection data (struct _ConnectionData*) \n
+ * opt1 -> selected row (GtkTreePath*) \n
+ * opt2 -> session-selection-dialog (GtkDialog*)
+ *
+ * @return G_SOURCE_REMOVE (FALSE), #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more
+ * memorable names for the return value. See GLib docs. \n
+ * https://docs.gtk.org/glib/const.SOURCE_REMOVE.html
+ */
+static gboolean rmplugin_x2go_pyhoca_terminate_session(X2GoCustomUserData *custom_data)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!custom_data || !custom_data->gp ||
+ !custom_data->dialog_data || !custom_data->connect_data ||
+ !custom_data->opt1 || !custom_data->opt2) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Parameter 'custom_data' is not fully initialized!")
+ ));
+
+ return G_SOURCE_REMOVE;
+ }
+
+ // Extract data passed by X2GoCustomUserData *custom_data.
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(custom_data->gp);
+ //struct _DialogData *ddata = (struct _DialogData*) custom_data->dialog_data;
+ struct _ConnectionData *connect_data = (struct _ConnectionData*) custom_data->connect_data;
+ GtkTreePath* selected_row = (GtkTreePath*) custom_data->opt1;
+ GtkDialog *dialog = GTK_DIALOG(custom_data->opt2);
+
+ gchar *host = NULL;
+ gchar *username = NULL;
+ gchar *password = NULL;
+ gchar *ssh_privatekey = NULL;
+ gchar *ssh_passphrase = NULL;
+ gboolean valid = rmplugin_x2go_verify_connection_data(connect_data);
+ if (valid) {
+ if (connect_data->password) password = connect_data->password;
+ if (connect_data->ssh_privatekey) {
+ ssh_privatekey = connect_data->ssh_privatekey;
+
+ if (connect_data->ssh_passphrase) {
+ ssh_passphrase = connect_data->ssh_passphrase;
+ }
+ }
+
+ host = connect_data->host;
+ username = connect_data->username;
+ } else {
+ return G_SOURCE_REMOVE;
+ }
+
+ GValue value = rmplugin_x2go_session_chooser_get_property(GTK_WIDGET(dialog),
+ SESSION_SESSION_ID,
+ selected_row);
+ // error message was handled already.
+ if (!G_VALUE_HOLDS_STRING(&value)) return G_SOURCE_REMOVE;
+ const gchar *session_id = g_value_get_string(&value);
+
+ // We will now start pyhoca-cli with only the '--terminate $SESSION_ID' option.
+ // (and of course auth related stuff)
+ gchar *argv[50];
+ gint argc = 0;
+
+ argv[argc++] = g_strdup("pyhoca-cli");
+
+ argv[argc++] = g_strdup("--server"); // Not listed as feature.
+ argv[argc++] = g_strdup_printf("%s", host);
+
+ if (FEATURE_AVAILABLE(gpdata, "USERNAME")) {
+ argv[argc++] = g_strdup("-u");
+ if (username) {
+ argv[argc++] = g_strdup_printf("%s", username);
+ } else {
+ argv[argc++] = g_strdup_printf("%s", g_get_user_name());
+ }
+ } else {
+ REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
+ return G_SOURCE_REMOVE;
+ }
+
+ if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) {
+ if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) {
+ argv[argc++] = g_strdup("--auth-attempts");
+ argv[argc++] = g_strdup_printf ("%i", 0);
+ } else {
+ REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS"));
+ }
+ if (strlen(password) > 0) {
+ argv[argc++] = g_strdup("--force-password");
+ argv[argc++] = g_strdup("--password");
+ argv[argc++] = g_strdup_printf("%s", password);
+ }
+ } else if (!password) {
+ REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
+ return G_SOURCE_REMOVE;
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "TERMINATE")) {
+ argv[argc++] = g_strdup("--terminate");
+ argv[argc++] = g_strdup_printf("%s", session_id);
+ } else {
+ REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("TERMINATE"));
+ return G_SOURCE_REMOVE;
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) {
+ argv[argc++] = g_strdup("--non-interactive");
+ } else {
+ REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) {
+ if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) {
+ argv[argc++] = g_strdup("--ssh-privkey");
+ argv[argc++] = g_strdup_printf("%s", ssh_privatekey);
+
+ if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) {
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) {
+ argv[argc++] = g_strdup("--ssh-passphrase");
+ argv[argc++] = g_strdup_printf("%s", ssh_passphrase);
+ } else {
+ REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE"));
+ }
+ }
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY"));
+ }
+
+ argv[argc++] = NULL;
+
+ GError* error = NULL;
+ gchar** envp = g_get_environ();
+ rmplugin_x2go_spawn_pyhoca_process(argc, argv, &error, envp);
+ g_strfreev(envp);
+
+ if (error) {
+ gchar *err_msg = g_strdup_printf(
+ _("Could not terminate X2Go session '%s':\n%s"),
+ session_id,
+ error->message
+ );
+
+ REMMINA_PLUGIN_CRITICAL("%s", err_msg);
+
+ struct _DialogData *err_ddata = g_new0(struct _DialogData, 1);
+ err_ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(dialog)));
+ err_ddata->flags = GTK_DIALOG_MODAL;
+ err_ddata->type = GTK_MESSAGE_ERROR;
+ err_ddata->buttons = GTK_BUTTONS_OK;
+ err_ddata->title = _("An error occured.");
+ err_ddata->message = err_msg;
+ // We don't need the response.
+ err_ddata->callbackfunc = NULL;
+ // We don't need a custom dialog either.
+ err_ddata->dialog_factory_func = NULL;
+ err_ddata->dialog_factory_data = NULL;
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ */
+ custom_data->gp = custom_data->gp;
+ custom_data->dialog_data = err_ddata;
+ custom_data->connect_data = NULL;
+ custom_data->opt1 = NULL;
+ custom_data->opt2 = NULL;
+
+ IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data);
+
+ // Too verbose:
+ // GtkTreeModel *model = gtk_tree_model_filter_get_model(
+ // GTK_TREE_MODEL_FILTER(filter));
+ // gtk_tree_model_foreach(GTK_TREE_MODEL(model), (GtkTreeModelForeachFunc)
+ // rmplugin_x2go_dump_session_properties, dialog);
+
+ // Set row visible again since we could not terminate the session.
+ if (!rmplugin_x2go_session_chooser_set_row_visible(selected_row, TRUE,
+ dialog)) {
+ // error message was already handled.
+ return G_SOURCE_REMOVE;
+ }
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * @brief Gets executed on dialog's 'response' signal
+ *
+ * @param custom_data X2GoCustomUserData*: \n
+ * gp -> gp (RemminaProtocolWidget*) \n
+ * dialog_data -> dialog data (struct _DialogData*) \n
+ * connect_data -> connection data (struct _ConnectionData*)
+ * @param response_id See GTK 'response' signal.
+ * @param self The dialog itself.
+ *
+ * @return gboolean, #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable
+ * names for the return value. See GLib docs. \n
+ * https://docs.gtk.org/glib/const.SOURCE_REMOVE.html
+ */
+static gboolean rmplugin_x2go_session_chooser_callback(X2GoCustomUserData* custom_data,
+ gint response_id,
+ GtkDialog *self)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (!custom_data || !custom_data->gp || !custom_data->dialog_data ||
+ !custom_data->connect_data) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Parameter 'custom_data' is not initialized!")
+ ));
+
+ return G_SOURCE_REMOVE;
+ }
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget*) custom_data->gp;
+
+ // Don't need to run other stuff, if the user just wants a new session.
+ // Also it can happen, that no session is there anymore which can be selected!
+ if (response_id == SESSION_CHOOSER_RESPONSE_NEW) {
+ REMMINA_PLUGIN_DEBUG("The user explicitly requested a new session. "
+ "Creating a new session…");
+ SET_RESUME_SESSION(gp, NULL);
+
+ // Unstucking main process. Telling it that a session has been selected.
+ // We use a trick here. As long as there is something other
+ // than 0 stored, a session is selected. So we use the gpointer as a gboolean.
+ SET_SESSION_SELECTED(gp, (gpointer) TRUE);
+
+ gtk_widget_destroy(GTK_WIDGET(self));
+
+ return G_SOURCE_REMOVE;
+ }
+
+ // This assumes that there are sessions which can be selected!
+ GValue value = rmplugin_x2go_session_chooser_get_property(
+ GTK_WIDGET(self),
+ SESSION_SESSION_ID,
+ NULL // Let the function search for the selected row.
+ );
+
+ // error message was handled already.
+ if (!G_VALUE_HOLDS_STRING(&value)) return G_SOURCE_REMOVE;
+
+ gchar *session_id = (gchar*) g_value_get_string(&value);
+
+ if (response_id == SESSION_CHOOSER_RESPONSE_CHOOSE) {
+ if (!session_id || strlen(session_id) <= 0) {
+ REMMINA_PLUGIN_DEBUG(
+ "%s",
+ _("Could not get session ID from session chooser dialog.")
+ );
+ SET_RESUME_SESSION(gp, NULL);
+ } else {
+ SET_RESUME_SESSION(gp, session_id);
+
+ REMMINA_PLUGIN_MESSAGE("%s", g_strdup_printf(
+ _("Resuming session: '%s'"),
+ session_id
+ ));
+ }
+ } else if (response_id == SESSION_CHOOSER_RESPONSE_TERMINATE) {
+ if (!session_id || strlen(session_id) <= 0) {
+ REMMINA_PLUGIN_DEBUG(
+ "%s",
+ _("Could not get session ID from session chooser dialog.")
+ );
+ SET_RESUME_SESSION(gp, NULL);
+ } else {
+ SET_RESUME_SESSION(gp, session_id);
+
+ REMMINA_PLUGIN_MESSAGE("%s", g_strdup_printf(
+ _("Terminating session: '%s'"),
+ session_id
+ ));
+ }
+
+ GtkTreePath *path = rmplugin_x2go_session_chooser_get_selected_row(
+ GTK_WIDGET(self));
+ // error message was already handled.
+ if (!path) return G_SOURCE_REMOVE;
+
+ // Actually set row invisible.
+ if (!rmplugin_x2go_session_chooser_set_row_visible(path, FALSE, self)) {
+ // error message was already handled.
+ return G_SOURCE_REMOVE;
+ }
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ * connect_data -> connection data (struct _ConnectionData*)
+ * opt1 -> selected row (GtkTreePath*)
+ * opt2 -> session selection dialog (GtkDialog*)
+ */
+ // everything else is already initialized.
+ custom_data->opt1 = path;
+ custom_data->opt2 = self;
+
+ // Actually start pyhoca-cli process with --terminate $session_id.
+ g_thread_new("terminate-session-thread",
+ (GThreadFunc) rmplugin_x2go_pyhoca_terminate_session,
+ custom_data);
+
+ // Dialog should stay open.
+ return G_SOURCE_CONTINUE;
+ } else {
+ REMMINA_PLUGIN_DEBUG("User clicked dialog away. "
+ "Creating a new session then.");
+ SET_RESUME_SESSION(gp, NULL);
+ }
+
+ // Unstucking main process. Telling it that a session has been selected.
+ // We use a trick here. As long as there is something other
+ // than 0 stored, a session is selected. So we use the gpointer as a gboolean.
+ SET_SESSION_SELECTED(gp, (gpointer) TRUE);
+
+ gtk_widget_destroy(GTK_WIDGET(self));
+
+ return G_SOURCE_REMOVE;
+}
+
+#define RMPLUGIN_X2GO_FEATURE_GTKSOCKET 1
+
+/* Forward declaration */
+static RemminaProtocolPlugin rmplugin_x2go;
+
+/* When more than one NX sessions is connecting in progress, we need this mutex and array
+ * to prevent them from stealing the same window ID.
+ */
+static pthread_mutex_t remmina_x2go_init_mutex;
+static GArray *remmina_x2go_window_id_array;
+
+/* ------------- Support for execution on main thread of GTK functions ------------- */
+struct onMainThread_cb_data
+{
+ enum { FUNC_GTK_SOCKET_ADD_ID } func;
+
+ GtkSocket* sk;
+ Window w;
+
+ /* Mutex for thread synchronization */
+ pthread_mutex_t mu;
+ /* Flag to catch cancellations */
+ gboolean cancelled;
+};
+
+static gboolean onMainThread_cb(struct onMainThread_cb_data *d)
+{
+ TRACE_CALL(__func__);
+ if (!d->cancelled) {
+ switch (d->func) {
+ case FUNC_GTK_SOCKET_ADD_ID:
+ gtk_socket_add_id(d->sk, d->w);
+ break;
+ }
+ pthread_mutex_unlock(&d->mu);
+ } else {
+ /* thread has been cancelled, so we must free d memory here */
+ g_free(d);
+ }
+ return G_SOURCE_REMOVE;
+}
+
+
+static void onMainThread_cleanup_handler(gpointer data)
+{
+ TRACE_CALL(__func__);
+ struct onMainThread_cb_data *d = data;
+ d->cancelled = TRUE;
+}
+
+static void onMainThread_schedule_callback_and_wait(struct onMainThread_cb_data *d)
+{
+ TRACE_CALL(__func__);
+ d->cancelled = FALSE;
+ pthread_cleanup_push(onMainThread_cleanup_handler, d);
+ pthread_mutex_init(&d->mu, NULL);
+ pthread_mutex_lock(&d->mu);
+ gdk_threads_add_idle((GSourceFunc)onMainThread_cb, (gpointer) d);
+
+ pthread_mutex_lock(&d->mu);
+
+ pthread_cleanup_pop(0);
+ pthread_mutex_unlock(&d->mu);
+ pthread_mutex_destroy(&d->mu);
+}
+
+static void onMainThread_gtk_socket_add_id(GtkSocket* sk, Window w)
+{
+ TRACE_CALL(__func__);
+
+ struct onMainThread_cb_data *d;
+
+ d = g_new0(struct onMainThread_cb_data, 1);
+ d->func = FUNC_GTK_SOCKET_ADD_ID;
+ d->sk = sk;
+ d->w = w;
+
+ onMainThread_schedule_callback_and_wait(d);
+ g_free(d);
+}
+/* /-/-/-/-/-/-/ Support for execution on main thread of GTK functions /-/-/-/-/-/-/ */
+
+static void rmplugin_x2go_remove_window_id (Window window_id)
+{
+ gint i;
+ gboolean already_seen = FALSE;
+
+ pthread_mutex_lock(&remmina_x2go_init_mutex);
+ for (i = 0; i < remmina_x2go_window_id_array->len; i++) {
+ if (g_array_index(remmina_x2go_window_id_array, Window, i) == window_id) {
+ already_seen = TRUE;
+ REMMINA_PLUGIN_DEBUG("Window of X2Go Agent with ID [0x%lx] seen already.",
+ window_id);
+ break;
+ }
+ }
+
+ if (already_seen) {
+ g_array_remove_index_fast(remmina_x2go_window_id_array, i);
+ REMMINA_PLUGIN_DEBUG("Forgetting about window of X2Go Agent with ID [0x%lx]…",
+ window_id);
+ }
+
+ pthread_mutex_unlock(&remmina_x2go_init_mutex);
+}
+
+/**
+ * @returns: FALSE. This source should be removed from main loop.
+ * #G_SOURCE_CONTINUE and #G_SOURCE_REMOVE are more memorable
+ * names for the return value.
+ */
+static gboolean rmplugin_x2go_cleanup(RemminaProtocolWidget *gp)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ gchar *server;
+ gint port;
+
+ RemminaFile *remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
+ rm_plugin_service->get_server_port(rm_plugin_service->file_get_string(remminafile, "server"),
+ 22,
+ &server,
+ &port);
+
+ REMMINA_PLUGIN_AUDIT(_("Disconnected from %s:%d via X2Go"), server, port);
+ g_free(server), server = NULL;
+
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+ if (gpdata == NULL) {
+ REMMINA_PLUGIN_DEBUG("Exiting since gpdata is already 'NULL'…");
+ return G_SOURCE_REMOVE;
+ }
+
+ if (gpdata->thread) {
+ pthread_cancel(gpdata->thread);
+ if (gpdata->thread) pthread_join(gpdata->thread, NULL);
+ }
+
+ if (gpdata->window_id) {
+ rmplugin_x2go_remove_window_id(gpdata->window_id);
+ }
+
+ if (gpdata->pidx2go) {
+ kill(gpdata->pidx2go, SIGTERM);
+ g_spawn_close_pid(gpdata->pidx2go);
+ gpdata->pidx2go = 0;
+ }
+
+ if (gpdata->display) {
+ XSetErrorHandler(gpdata->orig_handler);
+ XCloseDisplay(gpdata->display);
+ gpdata->display = NULL;
+ }
+
+ g_object_steal_data(G_OBJECT(gp), "plugin-data");
+ rm_plugin_service->protocol_plugin_signal_connection_closed(gp);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean rmplugin_x2go_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ if (gpdata->disconnected) {
+ REMMINA_PLUGIN_DEBUG("Doing nothing since the plugin is already disconnected.");
+ return G_SOURCE_REMOVE;
+ }
+
+ rmplugin_x2go_cleanup(gp);
+
+ // Try again.
+ return G_SOURCE_CONTINUE;
+}
+
+static void rmplugin_x2go_pyhoca_cli_exited(GPid pid,
+ gint status,
+ RemminaProtocolWidget *gp)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+ if (!gpdata) {
+ REMMINA_PLUGIN_DEBUG("Doing nothing as the disconnection "
+ "has already been handled.");
+ return;
+ }
+
+ if (gpdata->pidx2go <= 0) {
+ REMMINA_PLUGIN_DEBUG("Doing nothing since pyhoca-cli was expected to stop.");
+ return;
+ }
+
+ REMMINA_PLUGIN_CRITICAL("%s", _("PyHoca-CLI exited unexpectedly. "
+ "This connection will now be closed."));
+
+ struct _DialogData *ddata = g_new0(struct _DialogData, 1);
+ ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp)));
+ ddata->flags = GTK_DIALOG_MODAL;
+ ddata->type = GTK_MESSAGE_ERROR;
+ ddata->buttons = GTK_BUTTONS_OK;
+ ddata->title = _("An error occured.");
+ ddata->message = _("The necessary child process 'pyhoca-cli' stopped unexpectedly.\n"
+ "Please check your profile settings and PyHoca-CLI's output for "
+ "possible errors. Also ensure the remote server is "
+ "reachable and you're using the right credentials.");
+ // We don't need the response.
+ ddata->callbackfunc = NULL;
+ // We don't need a custom dialog either.
+ ddata->dialog_factory_func = NULL;
+ ddata->dialog_factory_data = NULL;
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ */
+ X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1);
+ g_assert(custom_data && "custom_data could not be initialized.");
+
+ custom_data->gp = gp;
+ custom_data->dialog_data = ddata;
+ custom_data->connect_data = NULL;
+ custom_data->opt1 = NULL;
+
+ IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data);
+
+ // 1 Second. Give `Dialog` chance to open.
+ usleep(1000 * 1000);
+
+ rmplugin_x2go_close_connection(gp);
+}
+
+/**
+ * @brief Saves s_password and s_username if set.
+ * @returns either TRUE or FALSE. If FALSE gets returned, `errmsg` is set.
+ */
+static gboolean rmplugin_x2go_save_credentials(RemminaFile* remminafile,
+ gchar* s_username, gchar* s_password,
+ gchar* errmsg)
+{
+ // User has requested to save credentials. We put all the new credentials
+ // into remminafile->settings. They will be saved later, on successful
+ // connection, by rcw.c
+ if (s_password && s_username) {
+ if (g_strcmp0(s_username, "") == 0) {
+ g_strlcpy(errmsg, _("Can't save empty username!"), 512);
+ //REMMINA_PLUGIN_CRITICAL("%s", errmsg); // No need.
+ return FALSE;
+ }
+
+ // We allow the possibility to set an empty password because a X2Go
+ // session can be still made using keyfiles or similar.
+ rm_plugin_service->file_set_string(remminafile, "password",
+ s_password);
+ rm_plugin_service->file_set_string(remminafile, "username",
+ s_username);
+ } else {
+ g_strlcpy(errmsg, g_strdup_printf(
+ _("Internal error: %s"),
+ _("Could not save new credentials.")
+ ), 512);
+
+ REMMINA_PLUGIN_CRITICAL("%s", _("Could not save "
+ "new credentials: 's_password' or "
+ "'s_username' strings were not set."));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+
+/**
+ * @brief Asks the user for a username and password.
+ *
+ * @param errmsg Pointer to error message string (set if function failed).
+ * @param passphrase gchar** Passphrase which will be used to unlock SSH key.
+ *
+ * @returns FALSE if auth failed and TRUE on success.
+ */
+static gboolean rmplugin_x2go_get_ssh_passphrase(RemminaProtocolWidget *gp, gchar *errmsg,
+ gchar **passphrase)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ g_assert(errmsg != NULL);
+ g_assert(gp != NULL);
+
+ if ((*passphrase) == NULL) {
+ // Just setting NULL password to empty password.
+ (*passphrase) = g_strdup("");
+ }
+
+ gint ret = rm_plugin_service->protocol_plugin_init_auth(
+ gp, 0, _("Enter password to unlock the SSH key:"),
+ NULL, *passphrase, NULL, NULL
+ );
+
+ if (ret == GTK_RESPONSE_OK) {
+ gchar *s_passphrase = rm_plugin_service->protocol_plugin_init_get_password(gp);
+ if (s_passphrase) {
+ (*passphrase) = g_strdup(s_passphrase);
+ g_free(s_passphrase);
+ }
+ } else {
+ g_strlcpy(errmsg, _("Password input cancelled. Aborting…"), 512);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+
+/**
+ * @brief Asks the user for a username and password.
+ *
+ * @param errmsg Pointer to error message string (set if function failed).
+ * @param username Pointer to default username. Gets set to new username on success.
+ * @param password Pointer to default password. Gets set to new password on success.
+ *
+ * @returns FALSE if auth failed and TRUE on success.
+ */
+static gboolean rmplugin_x2go_get_auth(RemminaProtocolWidget *gp, gchar* errmsg,
+ gchar** default_username, gchar** default_password)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ g_assert(errmsg != NULL);
+ g_assert(gp != NULL);
+ g_assert(default_username != NULL);
+ g_assert(default_password != NULL);
+
+ // default_username is probably NULL because the user didn't configure any
+ // username in the profile settings.
+ if ((*default_username) == NULL) {
+ gchar* l_errmsg = g_strdup_printf(
+ _("Tip: Check the 'Save password' checkbox or manually input your "
+ "X2Go username and password in the profile settings to store "
+ "them for faster logins.")
+ );
+ REMMINA_PLUGIN_MESSAGE("%s", l_errmsg);
+ (*default_username) = g_strdup("");
+ }
+
+ // default_password is probably NULL because something did go wrong at the
+ // secret-plugin. For example: The user didn't input a password for keyring or
+ // the user simply didn't configure a password in the profile settings.
+ if ((*default_password) == NULL) {
+ (*default_password) = g_strdup("");
+ }
+
+ gchar *s_username, *s_password;
+ gint ret;
+ gboolean save;
+ gboolean disable_password_storing;
+ RemminaFile *remminafile;
+
+ remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
+
+ disable_password_storing = rm_plugin_service->file_get_int(
+ remminafile, "disablepasswordstoring", FALSE
+ );
+
+ ret = rm_plugin_service->protocol_plugin_init_auth(
+ gp, (disable_password_storing ? 0 :
+ REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD |
+ REMMINA_MESSAGE_PANEL_FLAG_USERNAME),
+ _("Enter X2Go credentials"),
+ (*default_username), (*default_password), NULL, NULL
+ );
+
+ if (ret == GTK_RESPONSE_OK) {
+ s_username = rm_plugin_service->protocol_plugin_init_get_username(gp);
+ s_password = rm_plugin_service->protocol_plugin_init_get_password(gp);
+ if (rm_plugin_service->protocol_plugin_init_get_savepassword(gp))
+ rm_plugin_service->file_set_string(
+ remminafile, "password", s_password
+ );
+
+ // Should be renamed to protocol_plugin_init_get_savecredentials()?!
+ save = rm_plugin_service->protocol_plugin_init_get_savepassword(gp);
+ if (save) {
+ if (!rmplugin_x2go_save_credentials(remminafile, s_username,
+ s_password, errmsg)) {
+ return FALSE;
+ }
+ }
+ if (s_username) {
+ (*default_username) = g_strdup(s_username);
+ g_free(s_username);
+ }
+ if (s_password) {
+ (*default_password) = g_strdup(s_password);
+ g_free(s_password);
+ }
+ } else {
+ g_strlcpy(errmsg, _("Authentication cancelled. Aborting…"), 512);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * @brief Executes 'pyhoca-cli --list-sessions' for username@host.
+ *
+ * @param gp RemminaProtocolWidget* is used to get the x2go-plugin data.
+ * @param error This is where a error message will be when NULL gets returned.
+ * @param connect_data struct _ConnectionData* which stores all necessary information
+ * needed for retrieving sessions from a X2Go server.
+ *
+ * @returns Standard output of pyhoca-cli command.
+ * If NULL then errmsg is set to user-friendly error message.
+ */
+static gchar* rmplugin_x2go_get_pyhoca_sessions(RemminaProtocolWidget* gp, GError **error,
+ struct _ConnectionData* connect_data)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+ RemminaPluginX2GoData* gpdata = GET_PLUGIN_DATA(gp);
+
+ gchar *host = NULL;
+ gchar *username = NULL;
+ gchar *password = NULL;
+ gchar *ssh_privatekey = NULL;
+ gchar *ssh_passphrase = NULL;
+ gboolean valid = rmplugin_x2go_verify_connection_data(connect_data);
+
+ if (valid) {
+ if (connect_data->password) password = connect_data->password;
+ if (connect_data->ssh_privatekey) {
+ ssh_privatekey = connect_data->ssh_privatekey;
+
+ if (connect_data->ssh_passphrase) {
+ ssh_passphrase = connect_data->ssh_passphrase;
+ }
+ }
+
+ host = connect_data->host;
+ username = connect_data->username;
+ } else {
+ return G_SOURCE_REMOVE;
+ }
+
+ // We will now start pyhoca-cli with only the '--list-sessions' option.
+
+ gchar *argv[50];
+ gint argc = 0;
+
+ argv[argc++] = g_strdup("pyhoca-cli");
+ argv[argc++] = g_strdup("--list-sessions");
+
+ argv[argc++] = g_strdup("--server"); // Not listed as feature.
+ argv[argc++] = g_strdup_printf("%s", host);
+
+ if (FEATURE_AVAILABLE(gpdata, "USERNAME")) {
+ argv[argc++] = g_strdup("-u");
+ if (username) {
+ argv[argc++] = g_strdup_printf("%s", username);
+ } else {
+ argv[argc++] = g_strdup_printf("%s", g_get_user_name());
+ }
+ } else {
+ g_set_error(error, 1, 1, "%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
+ REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
+ return NULL;
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) {
+ argv[argc++] = g_strdup("--non-interactive");
+ } else {
+ REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE"));
+ }
+
+ if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) {
+ if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) {
+ argv[argc++] = g_strdup("--auth-attempts");
+ argv[argc++] = g_strdup_printf ("%i", 0);
+ } else {
+ REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS"));
+ }
+ if (strlen(password) > 0) {
+ argv[argc++] = g_strdup("--force-password");
+ argv[argc++] = g_strdup("--password");
+ argv[argc++] = g_strdup_printf("%s", password);
+ }
+ } else if (!password) {
+ g_set_error(error, 1, 1, "%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
+ REMMINA_PLUGIN_CRITICAL("%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
+ return NULL;
+ }
+
+ // No need to catch feature-not-available error.
+ // `--quiet` is not that important.
+ if (FEATURE_AVAILABLE(gpdata, "QUIET")) {
+ argv[argc++] = g_strdup("--quiet");
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) {
+ if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) {
+ argv[argc++] = g_strdup("--ssh-privkey");
+ argv[argc++] = g_strdup_printf("%s", ssh_privatekey);
+
+ if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) {
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) {
+ argv[argc++] = g_strdup("--ssh-passphrase");
+ argv[argc++] = g_strdup_printf("%s", ssh_passphrase);
+ } else {
+ REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE"));
+ }
+ }
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY"));
+ }
+
+
+ argv[argc++] = NULL;
+
+ //#ifndef GLIB_AVAILABLE_IN_2_68
+ gchar** envp = g_get_environ();
+ gchar* envp_splitted = g_strjoinv(";", envp);
+ envp_splitted = g_strconcat(envp_splitted, ";LANG=C", (void*) NULL);
+ envp = g_strsplit(envp_splitted, ";", 0);
+ /*
+ * #else
+ * // Only available after glib version 2.68.
+ * // TODO: FIXME: NOT TESTED!
+ * GStrvBuilder* builder = g_strv_builder_new();
+ * g_strv_builder_add(builder, "LANG=C");
+ * GStrv envp = g_strv_builder_end(builder);
+ * #endif
+ */
+
+ gchar* std_out = rmplugin_x2go_spawn_pyhoca_process(argc, argv, error, envp);
+ g_strfreev(envp);
+
+ if (!std_out || *error) {
+ // If no error is set but std_out is NULL
+ // then something is not right at all.
+ // Most likely the developer forgot to add an error message. Crash.
+ g_assert((*error) != NULL);
+ return NULL;
+ }
+
+ return std_out;
+}
+
+/**
+ * @brief This function is used to parse the output of
+ * rmplugin_x2go_get_pyhoca_sessions().
+ *
+ * @param gp RemminaProtocolWidget* is used to get the x2go-plugin data.
+ * @param error This is where a error message will be when NULL gets returned.
+ * @param connect_data struct _ConnectionData* which stores all necessary information
+ * needed for retrieving sessions from a X2Go server.
+ *
+ * @returns Returns either a GList containing the IDs of every already existing session
+ * found or if the function failes, NULL.
+ *
+ * TODO: If pyhoca-cli (python-x2go) implements `--json` or similar option -> Replace
+ * entire function with JSON parsing.
+ */
+static GList* rmplugin_x2go_parse_pyhoca_sessions(RemminaProtocolWidget* gp,
+ GError **error,
+ struct _ConnectionData* connect_data)
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ gchar *pyhoca_output = NULL;
+
+ pyhoca_output = rmplugin_x2go_get_pyhoca_sessions(gp, error, connect_data);
+ if (!pyhoca_output || *error) {
+ // If no error is set but pyhoca_output is NULL
+ // then something is not right at all.
+ // Most likely the developer forgot to add an error message. Crash.
+ g_assert((*error) != NULL);
+
+ return NULL;
+ }
+
+ gchar **lines_list = g_strsplit(pyhoca_output, "\n", -1);
+ // Assume at least two lines of output.
+ if (lines_list == NULL || lines_list[0] == NULL || lines_list[1] == NULL) {
+ g_set_error(error, 1, 1, "%s", _("Could not parse the output of PyHoca-CLI's "
+ "--list-sessions option. Creating a new "
+ "session now."));
+ return NULL;
+ }
+
+ gboolean found_session = FALSE;
+ GList* sessions = NULL;
+ gchar** session = NULL;
+
+ for (guint i = 0; lines_list[i] != NULL; i++) {
+ gchar* current_line = lines_list[i];
+
+ // TOO VERBOSE:
+ //REMMINA_PLUGIN_DEBUG("pyhoca-cli: %s", current_line);
+
+ // Hardcoded string "Session Name: " comes from python-x2go.
+ if (!g_str_has_prefix(current_line, "Session Name: ") && !found_session) {
+ // Doesn't begin with "Session Name: " and
+ // the current line doesn't come after that either. Skipping.
+ continue;
+ }
+
+ if (g_str_has_prefix(current_line, "Session Name: ")) {
+ gchar* session_id = NULL;
+ gchar** line_list = g_strsplit(current_line, ": ", 0);
+
+ if (line_list == NULL ||
+ line_list[0] == NULL ||
+ line_list[1] == NULL ||
+ strlen(line_list[0]) <= 0 ||
+ strlen(line_list[1]) <= 0)
+ {
+ found_session = FALSE;
+ continue;
+ }
+
+ session = malloc(sizeof(gchar*) * (SESSION_NUM_PROPERTIES+1));
+ if (!session) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Could not allocate "
+ "enough memory!"));
+ }
+ session[SESSION_NUM_PROPERTIES] = NULL;
+ sessions = g_list_append(sessions, session);
+
+ session_id = line_list[1];
+ session[SESSION_SESSION_ID] = session_id;
+
+ REMMINA_PLUGIN_INFO("%s", g_strdup_printf(
+ _("Found already existing X2Go session with ID: '%s'"),
+ session[SESSION_SESSION_ID])
+ );
+
+ found_session = TRUE;
+ continue;
+ }
+
+ if (!found_session) {
+ continue;
+ }
+
+ if (g_strcmp0(current_line, "-------------") == 0) {
+ continue;
+ }
+
+ gchar* value = NULL;
+ gchar** line_list = g_strsplit(current_line, ": ", 0);
+
+ if (line_list == NULL ||
+ line_list[0] == NULL ||
+ line_list[1] == NULL ||
+ strlen(line_list[0]) <= 0 ||
+ strlen(line_list[1]) <= 0)
+ {
+ // Probably the empty line at the end of every session.
+ found_session = FALSE;
+ continue;
+ }
+ value = line_list[1];
+
+ if (g_str_has_prefix(current_line, "cookie: ")) {
+ REMMINA_PLUGIN_DEBUG("cookie:\t'%s'", value);
+ session[SESSION_COOKIE] = value;
+ } else if (g_str_has_prefix(current_line, "agent PID: ")) {
+ REMMINA_PLUGIN_DEBUG("agent PID:\t'%s'", value);
+ session[SESSION_AGENT_PID] = value;
+ } else if (g_str_has_prefix(current_line, "display: ")) {
+ REMMINA_PLUGIN_DEBUG("display:\t'%s'", value);
+ session[SESSION_DISPLAY] = value;
+ } else if (g_str_has_prefix(current_line, "status: ")) {
+ if (g_strcmp0(value, "S") == 0) {
+ // TRANSLATORS: Please stick to X2GoClient's translation.
+ value = _("Suspended");
+ } else if (g_strcmp0(value, "R") == 0) {
+ // TRANSLATORS: Please stick to X2GoClient's translation.
+ value = _("Running");
+ } else if (g_strcmp0(value, "T") == 0) {
+ // TRANSLATORS: Please stick to X2GoClient's translation.
+ value = _("Terminated");
+ }
+ REMMINA_PLUGIN_DEBUG("status:\t'%s'", value);
+ session[SESSION_STATUS] = value;
+ } else if (g_str_has_prefix(current_line, "graphic port: ")) {
+ REMMINA_PLUGIN_DEBUG("graphic port:\t'%s'", value);
+ session[SESSION_GRAPHIC_PORT] = value;
+ } else if (g_str_has_prefix(current_line, "snd port: ")) {
+ REMMINA_PLUGIN_DEBUG("snd port:\t'%s'", value);
+ session[SESSION_SND_PORT] = value;
+ } else if (g_str_has_prefix(current_line, "sshfs port: ")) {
+ REMMINA_PLUGIN_DEBUG("sshfs port:\t'%s'", value);
+ session[SESSION_SSHFS_PORT] = value;
+ } else if (g_str_has_prefix(current_line, "username: ")) {
+ REMMINA_PLUGIN_DEBUG("username:\t'%s'", value);
+ session[SESSION_USERNAME] = value;
+ } else if (g_str_has_prefix(current_line, "hostname: ")) {
+ REMMINA_PLUGIN_DEBUG("hostname:\t'%s'", value);
+ session[SESSION_HOSTNAME] = value;
+ } else if (g_str_has_prefix(current_line, "create date: ")) {
+ REMMINA_PLUGIN_DEBUG("create date:\t'%s'", value);
+ session[SESSION_CREATE_DATE] = value;
+ } else if (g_str_has_prefix(current_line, "suspended since: ")) {
+ REMMINA_PLUGIN_DEBUG("suspended since:\t'%s'", value);
+ session[SESSION_SUSPENDED_SINCE] = value;
+ } else {
+ REMMINA_PLUGIN_DEBUG("Not supported:\t'%s'", value);
+ found_session = FALSE;
+ }
+ }
+
+ if (!sessions) {
+ g_set_error(error, 1, 1,
+ "%s", _("Could not find any sessions on remote machine. Creating a new "
+ "session now.")
+ );
+
+ // returning NULL with `error` set.
+ }
+
+ return sessions;
+}
+
+/**
+ * @brief Asks the user, with the help of a dialog, to continue an already existing
+ * session, terminate or create a new one.
+ *
+ * @param error Is set if there is something to tell the user. \n
+ * Not necessarily an *error* message.
+ * @param connect_data Stores all necessary information needed for
+ * etrieving sessions from a X2Go server.
+ * @return gchar* ID of session. Can be 'NULL' but then 'error' is set.
+ */
+static gchar* rmplugin_x2go_ask_session(RemminaProtocolWidget *gp, GError **error,
+ struct _ConnectionData* connect_data)
+{
+ if (!connect_data ||
+ !connect_data->host ||
+ !connect_data->username ||
+ !connect_data->password ||
+ strlen(connect_data->host) <= 0 ||
+ strlen(connect_data->username) <= 0)
+ // Allow empty passwords. Maybe the user wants to connect via public key?
+ {
+ g_set_error(error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("'Invalid connection data.'")
+ ));
+ return NULL;
+ }
+
+ GList *sessions_list = NULL;
+ sessions_list = rmplugin_x2go_parse_pyhoca_sessions(gp, error, connect_data);
+
+ if (!sessions_list || *error) {
+ // If no error is set but sessions_list is NULL
+ // then something is not right at all.
+ // Most likely the developer forgot to add an error message. Crash.
+ g_assert(*error != NULL);
+ return NULL;
+ }
+
+ // Prep new DialogData struct.
+ struct _DialogData *ddata = g_new0(struct _DialogData, 1);
+ ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp)));
+ ddata->flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT;
+ //ddata->type = GTK_MESSAGE_QUESTION;
+ //ddata->buttons = GTK_BUTTONS_OK; // Doesn't get used in our custom factory.
+ ddata->title = _("Choose a session to resume:");
+ ddata->message = "";
+
+ // gboolean factory(X2GoCustomUserData*, gpointer)
+ // X2GoCustomUserData*:
+ // gp -> gp (RemminaProtocolWidget*)
+ // dialog_data -> dialog data (struct _DialogData*)
+ // connect_data -> connection data (struct _ConnectionData*)
+ // gpointer: dialog_factory_data
+ ddata->callbackfunc = G_CALLBACK(rmplugin_x2go_session_chooser_callback);
+
+ // gboolean factory(X2GoCustomUserData*, gpointer)
+ // X2GoCustomUserData*:
+ // gp -> gp (RemminaProtocolWidget*)
+ // dialog_data -> dialog data (struct _DialogData*)
+ // connect_data -> connection data (struct _ConnectionData*)
+ // gpointer: dialog_factory_data
+ ddata->dialog_factory_data = sessions_list;
+ ddata->dialog_factory_func = G_CALLBACK(rmplugin_x2go_choose_session_dialog_factory);
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ */
+ X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1);
+ g_assert(custom_data && "custom_data could not be initialized.");
+
+ custom_data->gp = gp;
+ custom_data->dialog_data = ddata;
+ custom_data->connect_data = connect_data;
+ custom_data->opt1 = NULL;
+
+ // Open dialog here. Dialog rmplugin_x2go_session_chooser_callback (callbackfunc)
+ // should set SET_RESUME_SESSION.
+ IDLE_ADD((GSourceFunc)rmplugin_x2go_open_dialog, custom_data);
+
+ guint counter = 0;
+ while (!IS_SESSION_SELECTED(gp)) {
+ // 0.5 Seconds. Give dialog chance to open.
+ usleep(500 * 1000);
+
+ // Every 5 seconds
+ if (counter % 10 == 0 || counter == 0) {
+ REMMINA_PLUGIN_MESSAGE("%s", _("Waiting for user to select a session…"));
+ }
+ counter++;
+ }
+
+ gchar* chosen_resume_session = GET_RESUME_SESSION(gp);
+
+ if (!chosen_resume_session || strlen(chosen_resume_session) <= 0) {
+ g_set_error(error, 1, 1, "%s", _("No session was selected. Creating a new one."));
+ return NULL;
+ }
+
+ return chosen_resume_session;
+}
+
+static gboolean rmplugin_x2go_exec_x2go(gchar *host,
+ gint sshport,
+ gchar *username,
+ gchar *password,
+ gchar *command,
+ gchar *kbdlayout,
+ gchar *kbdtype,
+ gchar *audio,
+ gchar *clipboard,
+ gint dpi,
+ gchar *resolution,
+ gchar *ssh_privatekey,
+ RemminaProtocolWidget *gp,
+ gchar *errmsg)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+
+ gchar *argv[50];
+ gint argc = 0;
+
+ // We don't want to save any SSH passphrases on hard drive!
+ // Thats why we will always ask if needed.
+ gchar *ssh_passphrase = NULL;
+
+ if (!username || strlen(username) <= 0) {
+ // Sets `username` and `password`.
+ if (!rmplugin_x2go_get_auth(gp, errmsg, &username, &password)) {
+ return FALSE;
+ }
+ }
+
+ // Password can be *empty* but not NULL.
+ if (!password) {
+ password = g_strdup("");
+ }
+
+ if (ssh_privatekey && strlen(ssh_privatekey) > 0) {
+ // FIXME: Check if file exists and is legit private key.
+ // See: https://security.stackexchange.com/a/245767
+
+ // Get ssh_privatekey now via dialog.
+ if (!rmplugin_x2go_get_ssh_passphrase(gp, errmsg, &ssh_passphrase)) {
+ return FALSE;
+ }
+ }
+
+ struct _ConnectionData* connect_data = g_new0(struct _ConnectionData, 1);
+ connect_data->host = host;
+ connect_data->username = username;
+ connect_data->password = password;
+ connect_data->ssh_privatekey = ssh_privatekey;
+ connect_data->ssh_passphrase = ssh_passphrase;
+
+ GError *session_error = NULL;
+ gchar* resume_session_id = rmplugin_x2go_ask_session(gp, &session_error,
+ connect_data);
+
+ if (!resume_session_id || session_error || strlen(resume_session_id) <= 0) {
+ // If no error is set but session_id is NULL
+ // then something is not right at all.
+ // Most likely the developer forgot to add an error message. Crash.
+ g_assert(session_error != NULL);
+
+ REMMINA_PLUGIN_WARNING("%s", g_strdup_printf(
+ _("A non-critical error happened: %s"),
+ session_error->message
+ ));
+ } else {
+ REMMINA_PLUGIN_INFO("%s", g_strdup_printf(
+ _("User chose to resume session with ID: '%s'"),
+ resume_session_id
+ ));
+ }
+
+ argc = 0;
+ argv[argc++] = g_strdup("pyhoca-cli");
+
+ argv[argc++] = g_strdup("--server"); // Not listed as feature.
+ argv[argc++] = g_strdup_printf ("%s", host);
+
+ if (FEATURE_AVAILABLE(gpdata, "REMOTE_SSH_PORT")) {
+ argv[argc++] = g_strdup("-p");
+ argv[argc++] = g_strdup_printf ("%d", sshport);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("REMOTE_SSH_PORT"));
+ }
+
+ if (resume_session_id && strlen(resume_session_id) > 0) {
+ REMMINA_PLUGIN_INFO("%s", g_strdup_printf(
+ // TRANSLATORS: Please stick to X2GoClient's way of translating.
+ _("Resuming session '%s'…"),
+ resume_session_id
+ ));
+
+ if (FEATURE_AVAILABLE(gpdata, "RESUME")) {
+ argv[argc++] = g_strdup("--resume");
+ argv[argc++] = g_strdup_printf("%s", resume_session_id);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("RESUME"));
+ }
+ }
+
+ // Deprecated. The user either wants to continue a
+ // session or just not. No inbetween.
+ // if (!resume_session_id) {
+ // if (FEATURE_AVAILABLE(gpdata, "TRY_RESUME")) {
+ // argv[argc++] = g_strdup("--try-resume");
+ // } else {
+ // REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("TRY_RESUME"));
+ // }
+ // }
+
+ if (FEATURE_AVAILABLE(gpdata, "USERNAME")) {
+ argv[argc++] = g_strdup("-u");
+ if (username){
+ argv[argc++] = g_strdup_printf ("%s", username);
+ } else {
+ argv[argc++] = g_strdup_printf ("%s", g_get_user_name());
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("USERNAME"));
+ }
+
+ if (password && FEATURE_AVAILABLE(gpdata, "PASSWORD")) {
+ if (strlen(password) > 0) {
+ argv[argc++] = g_strdup("--force-password");
+ argv[argc++] = g_strdup("--password");
+ argv[argc++] = g_strdup_printf ("%s", password);
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("PASSWORD"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "AUTH_ATTEMPTS")) {
+ argv[argc++] = g_strdup("--auth-attempts");
+ argv[argc++] = g_strdup_printf ("%i", 0);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("AUTH_ATTEMPTS"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "NON_INTERACTIVE")) {
+ argv[argc++] = g_strdup("--non-interactive");
+ } else {
+ REMMINA_PLUGIN_WARNING("%s", FEATURE_NOT_AVAIL_STR("NON_INTERACTIVE"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "COMMAND")) {
+ argv[argc++] = g_strdup("-c");
+ // FIXME: pyhoca-cli is picky about multiple quotes around
+ // the command string...
+ // argv[argc++] = g_strdup_printf ("%s", g_shell_quote(command));
+ argv[argc++] = g_strdup(command);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("COMMAND"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "KBD_LAYOUT")) {
+ if (kbdlayout) {
+ argv[argc++] = g_strdup("--kbd-layout");
+ argv[argc++] = g_strdup_printf ("%s", kbdlayout);
+ } else {
+ argv[argc++] = g_strdup("--kbd-layout");
+ argv[argc++] = g_strdup("auto");
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("KBD_LAYOUT"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "KBD_TYPE")) {
+ if (kbdtype) {
+ argv[argc++] = g_strdup("--kbd-type");
+ argv[argc++] = g_strdup_printf ("%s", kbdtype);
+ } else {
+ argv[argc++] = g_strdup("--kbd-type");
+ argv[argc++] = g_strdup("auto");
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("KBD_TYPE"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "GEOMETRY")) {
+ if (!resolution)
+ resolution = "800x600";
+ argv[argc++] = g_strdup("-g");
+ argv[argc++] = g_strdup_printf ("%s", resolution);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("GEOMETRY"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "TERMINATE_ON_CTRL_C")) {
+ argv[argc++] = g_strdup("--terminate-on-ctrl-c");
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("TERMINATE_ON_CTRL_C"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "SOUND")) {
+ if (audio) {
+ argv[argc++] = g_strdup("--sound");
+ argv[argc++] = g_strdup_printf ("%s", audio);
+ } else {
+ argv[argc++] = g_strdup("--sound");
+ argv[argc++] = g_strdup("none");
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SOUND"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "CLIPBOARD_MODE")) {
+ if (clipboard) {
+ argv[argc++] = g_strdup("--clipboard-mode");
+ argv[argc++] = g_strdup_printf("%s", clipboard);
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("CLIPBOARD_MODE"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "DPI")) {
+ // Even though we validate the users input in the Remmina Editor,
+ // manipulating profile files is still very possible…
+ // Values are extracted from pyhoca-cli.
+ if (dpi < 20 || dpi > 400) {
+ g_strlcpy(errmsg, _("DPI setting is out of bounds. Please adjust "
+ "it in profile settings."), 512);
+ // No need, start_session() will handle output.
+ //REMMINA_PLUGIN_CRITICAL("%s", errmsg);
+ return FALSE;
+ }
+ argv[argc++] = g_strdup("--dpi");
+ argv[argc++] = g_strdup_printf ("%i", dpi);
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("DPI"));
+ }
+
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PRIVKEY")) {
+ if (ssh_privatekey && !g_str_equal(ssh_privatekey, "")) {
+ argv[argc++] = g_strdup("--ssh-privkey");
+ argv[argc++] = g_strdup_printf("%s", ssh_privatekey);
+
+ if (ssh_passphrase && !g_str_equal(ssh_passphrase, "")) {
+ if (FEATURE_AVAILABLE(gpdata, "SSH_PASSPHRASE")) {
+ argv[argc++] = g_strdup("--ssh-passphrase");
+ argv[argc++] = g_strdup_printf("%s", ssh_passphrase);
+ } else {
+ REMMINA_PLUGIN_MESSAGE("%s", FEATURE_NOT_AVAIL_STR("SSH_PASSPHRASE"));
+ }
+ }
+ }
+ } else {
+ REMMINA_PLUGIN_DEBUG("%s", FEATURE_NOT_AVAIL_STR("SSH_PRIVKEY"));
+ }
+
+ argv[argc++] = NULL;
+
+ GError *error = NULL;
+ gchar **envp = g_get_environ();
+ gboolean success = g_spawn_async_with_pipes (NULL, argv, envp,
+ (G_SPAWN_DO_NOT_REAP_CHILD |
+ G_SPAWN_SEARCH_PATH), NULL,
+ NULL, &gpdata->pidx2go,
+ NULL, NULL, NULL, &error);
+
+ REMMINA_PLUGIN_INFO("%s", _("Started PyHoca-CLI with the following arguments:"));
+ // Print every argument except passwords. Free all arg strings.
+ for (gint i = 0; i < argc - 1; i++) {
+ gchar* curr_arg = argv[i];
+
+ if (g_str_equal(curr_arg, "--password") ||
+ g_str_equal(curr_arg, "--ssh-passphrase")) {
+ g_printf("%s ", curr_arg);
+ g_printf("XXXXXX ");
+ g_free(curr_arg);
+ g_free(argv[++i]);
+ continue;
+ } else {
+ g_printf("%s ", curr_arg);
+ g_free(curr_arg);
+ }
+ }
+ g_printf("\n");
+
+ if (!success || error) {
+ // TRANSLATORS: Meta-error. Shouldn't be visible.
+ if (!error) error = g_error_new(0, 0, _("Internal error."));
+
+ gchar *error_title = _("Could not start X2Go session…");
+
+ struct _DialogData* ddata = g_new0(struct _DialogData, 1);
+ ddata->parent = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp)));
+ ddata->flags = GTK_DIALOG_MODAL;
+ ddata->type = GTK_MESSAGE_ERROR;
+ ddata->buttons = GTK_BUTTONS_OK;
+ ddata->title = _("Could not start X2Go session.");
+ ddata->message = g_strdup_printf(_("Could not start PyHoca-CLI (%i): '%s'"),
+ error->code,
+ error->message);
+ // We don't need the response.
+ ddata->callbackfunc = NULL;
+ // We don't need a custom dialog either.
+ ddata->dialog_factory_func = NULL;
+ ddata->dialog_factory_data = NULL;
+
+ /* Prepare X2GoCustomUserData *custom_data
+ * gp -> gp (RemminaProtocolWidget*)
+ * dialog_data -> dialog data (struct _DialogData*)
+ */
+ X2GoCustomUserData *custom_data = g_new0(X2GoCustomUserData, 1);
+ g_assert(custom_data && "Could not initialise Custom_data.");
+
+ custom_data->gp = gp;
+ custom_data->dialog_data = ddata;
+ custom_data->connect_data = NULL;
+ custom_data->opt1 = NULL;
+
+ IDLE_ADD((GSourceFunc) rmplugin_x2go_open_dialog, custom_data);
+
+ g_strlcpy(errmsg, error_title, 512);
+
+ // No need to output here. rmplugin_x2go_start_session will do this.
+
+ g_error_free(error);
+
+ return FALSE;
+ }
+
+ // Prevent a race condition where pyhoca-cli is not
+ // started yet (pidx2go == 0) but a watcher is added.
+
+ struct timespec ts;
+ // 0.001 seconds.
+ ts.tv_nsec = 1 * 1000 * 1000;
+ ts.tv_sec = 0;
+ while (gpdata->pidx2go == 0) {
+ nanosleep(&ts, NULL);
+ REMMINA_PLUGIN_DEBUG("Waiting for PyHoca-CLI to start…");
+ };
+
+ REMMINA_PLUGIN_DEBUG("Watching child 'pyhoca-cli' process now…");
+ g_child_watch_add(gpdata->pidx2go,
+ (GChildWatchFunc) rmplugin_x2go_pyhoca_cli_exited,
+ gp);
+
+ return TRUE;
+}
+
+/**
+ * @returns a GList* with all features which pyhoca-cli had before the feature system.
+ */
+static GList* rmplugin_x2go_old_pyhoca_features()
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ #define AMOUNT_FEATURES 43
+ gchar* features[AMOUNT_FEATURES] = {
+ "ADD_TO_KNOWN_HOSTS", "AUTH_ATTEMPTS", "BROKER_PASSWORD", "BROKER_URL",
+ "CLEAN_SESSIONS", "COMMAND", "DEBUG", "FORCE_PASSWORD", "FORWARD_SSHAGENT",
+ "GEOMETRY", "KBD_LAYOUT", "KBD_TYPE", "LIBDEBUG", "LIBDEBUG_SFTPXFER", "LINK",
+ "LIST_CLIENT_FEATURES", "LIST_DESKTOPS", "LIST_SESSIONS", "NEW", "PACK",
+ "PASSWORD", "PDFVIEW_CMD", "PRINTER", "PRINTING", "PRINT_ACTION", "PRINT_CMD",
+ "QUIET", "REMOTE_SSH_PORT", "RESUME", "SAVE_TO_FOLDER", "SESSION_PROFILE",
+ "SESSION_TYPE", "SHARE_DESKTOP", "SHARE_LOCAL_FOLDERS", "SHARE_MODE", "SOUND",
+ "SSH_PRIVKEY", "SUSPEND", "TERMINATE", "TERMINATE_ON_CTRL_C", "TRY_RESUME",
+ "USERNAME", "XINERAMA"
+ };
+
+ GList *features_list = NULL;
+ for (int i = 0; i < AMOUNT_FEATURES; i++) {
+ features_list = g_list_append(features_list, features[i]);
+ }
+
+ return features_list;
+}
+
+/**
+ * @returns a GList* which includes all pyhoca-cli command line features we can use.
+ */
+static GList* rmplugin_x2go_populate_available_features_list()
+{
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ GList* returning_glist = NULL;
+
+ // We will now start pyhoca-cli with only the '--list-cmdline-features' option
+ // and depending on the exit code and standard output we will determine if some
+ // features are available or not.
+
+ gchar* argv[50];
+ gint argc = 0;
+
+ argv[argc++] = g_strdup("pyhoca-cli");
+ argv[argc++] = g_strdup("--list-cmdline-features");
+ argv[argc++] = NULL;
+
+ GError* error = NULL; // Won't be actually used.
+
+ // Querying pyhoca-cli's command line features.
+ gchar** envp = g_get_environ();
+ gchar* features_string = rmplugin_x2go_spawn_pyhoca_process(argc, argv,
+ &error, envp);
+ g_strfreev(envp);
+
+ if (!features_string || error) {
+ // We added the '--list-cmdline-features' on commit 17d1be1319ba6 of
+ // pyhoca-cli. In order to protect setups which don't have the newest
+ // version of pyhoca-cli available yet we artificially create a list
+ // of an old limited set of features.
+
+ REMMINA_PLUGIN_WARNING("%s",
+ _("Could not get PyHoca-CLI's command-line features. This "
+ "indicates it is either too old, or not installed. "
+ "An old limited set of features will be used for now."));
+
+ return rmplugin_x2go_old_pyhoca_features();
+ } else {
+ gchar **features_list = g_strsplit(features_string, "\n", 0);
+
+ if (features_list == NULL) {
+ gchar *error_msg = _("Could not parse PyHoca-CLI's command-line "
+ "features. Using a limited feature-set for now.");
+ REMMINA_PLUGIN_WARNING("%s", error_msg);
+ return rmplugin_x2go_old_pyhoca_features();
+ }
+
+ REMMINA_PLUGIN_INFO("%s", _("Retrieved the following PyHoca-CLI "
+ "command-line features:"));
+
+ for(int k = 0; features_list[k] != NULL; k++) {
+ // Filter out empty strings
+ if (strlen(features_list[k]) <= 0) continue;
+
+ REMMINA_PLUGIN_INFO("%s",
+ g_strdup_printf(_("Available feature[%i]: '%s'"),
+ k+1, features_list[k]));
+ returning_glist = g_list_append(returning_glist, features_list[k]);
+ }
+ return returning_glist;
+ }
+}
+
+static void rmplugin_x2go_on_plug_added(GtkSocket *socket, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ gchar *server;
+ gint port;
+
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+ REMMINA_PLUGIN_DEBUG("Socket %d", gpdata->socket_id);
+ rm_plugin_service->protocol_plugin_signal_connection_opened(gp);
+
+ RemminaFile *remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
+ rm_plugin_service->get_server_port(rm_plugin_service->file_get_string(remminafile, "server"),
+ 22,
+ &server,
+ &port);
+
+ REMMINA_PLUGIN_AUDIT(_("Connected to %s:%d via X2Go"), server, port);
+ g_free(server), server = NULL;
+
+ return;
+}
+
+static gboolean rmplugin_x2go_on_plug_removed(GtkSocket *socket, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+ rmplugin_x2go_close_connection(gp);
+ return G_SOURCE_CONTINUE;
+}
+
+static void rmplugin_x2go_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Function entry.", PLUGIN_NAME);
+ RemminaPluginX2GoData *gpdata;
+
+ gpdata = g_new0(RemminaPluginX2GoData, 1);
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
+
+ if (!rm_plugin_service->gtksocket_available()) {
+ /* report this in open_connection, not reportable here... */
+ return;
+ }
+
+ GList* available_features = rmplugin_x2go_populate_available_features_list();
+
+ // available_features can't be NULL cause if it fails, it gets populated with an
+ // old standard feature set.
+ gpdata->available_features = available_features;
+
+ gpdata->socket_id = 0;
+ gpdata->thread = 0;
+
+ gpdata->display = NULL;
+ gpdata->window_id = 0;
+ gpdata->pidx2go = 0;
+ gpdata->orig_handler = NULL;
+
+ gpdata->socket = gtk_socket_new();
+ rm_plugin_service->protocol_plugin_register_hostkey(gp, gpdata->socket);
+ gtk_widget_show(gpdata->socket);
+
+ g_signal_connect(G_OBJECT(gpdata->socket), "plug-added",
+ G_CALLBACK(rmplugin_x2go_on_plug_added), gp);
+ g_signal_connect(G_OBJECT(gpdata->socket), "plug-removed",
+ G_CALLBACK(rmplugin_x2go_on_plug_removed), gp);
+ gtk_container_add(GTK_CONTAINER(gp), gpdata->socket);
+}
+
+static gboolean rmplugin_x2go_try_window_id(Window window_id)
+{
+ TRACE_CALL(__func__);
+ gint i;
+ gboolean already_seen = FALSE;
+
+ REMMINA_PLUGIN_DEBUG("Check if the window of X2Go Agent with ID [0x%lx] is already known or if "
+ "it needs registration", window_id);
+
+ pthread_mutex_lock(&remmina_x2go_init_mutex);
+ for (i = 0; i < remmina_x2go_window_id_array->len; i++) {
+ if (g_array_index(remmina_x2go_window_id_array, Window, i) == window_id) {
+ already_seen = TRUE;
+ REMMINA_PLUGIN_DEBUG("Window of X2Go Agent with ID [0x%lx] "
+ "already seen.", window_id);
+ break;
+ }
+ }
+ if (!already_seen) {
+ g_array_append_val(remmina_x2go_window_id_array, window_id);
+ REMMINA_PLUGIN_DEBUG("Registered new window for X2Go Agent with "
+ "ID [0x%lx].", window_id);
+ }
+ pthread_mutex_unlock(&remmina_x2go_init_mutex);
+
+ return (!already_seen);
+}
+
+static int rmplugin_x2go_dummy_handler(Display *dsp, XErrorEvent *err)
+{
+ TRACE_CALL(__func__);
+ return 0;
+}
+
+static gboolean rmplugin_x2go_start_create_notify(RemminaProtocolWidget *gp, gchar *errmsg)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+
+ gpdata->display = XOpenDisplay(gdk_display_get_name(gdk_display_get_default()));
+ if (gpdata->display == NULL) {
+ g_strlcpy(errmsg, _("Could not open X11 DISPLAY."), 512);
+ return FALSE;
+ }
+
+ gpdata->orig_handler = XSetErrorHandler(rmplugin_x2go_dummy_handler);
+
+ XSelectInput(gpdata->display,
+ XDefaultRootWindow(gpdata->display),
+ SubstructureNotifyMask);
+
+ REMMINA_PLUGIN_DEBUG("X11 event-watcher created.");
+
+ return TRUE;
+}
+
+static gboolean rmplugin_x2go_monitor_create_notify(RemminaProtocolWidget *gp,
+ const gchar *cmd,
+ gchar *errmsg)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata;
+
+ gboolean agent_window_found = FALSE;
+ Atom atom;
+ XEvent xev;
+ Window w;
+ Atom type;
+ int format;
+ unsigned long nitems, rest;
+ unsigned char *data = NULL;
+
+ guint16 non_createnotify_count = 0;
+
+ struct timespec ts;
+ // wait_amount * ts.tv_nsec = 20s
+ // 100 * 0.2s = 20s
+ int wait_amount = 100;
+
+ CANCEL_DEFER
+
+ REMMINA_PLUGIN_DEBUG("%s", _("Waiting for window of X2Go Agent to appear…"));
+
+ gpdata = GET_PLUGIN_DATA(gp);
+ atom = XInternAtom(gpdata->display, "WM_COMMAND", True);
+ if (atom == None) {
+ CANCEL_ASYNC
+ return FALSE;
+ }
+
+ ts.tv_sec = 0;
+ // 0.2s = 200000000ns
+ ts.tv_nsec = 200000000;
+
+ while (wait_amount > 0) {
+ pthread_testcancel();
+ if (!(gpdata->pidx2go > 0)) {
+ nanosleep(&ts, NULL);
+ REMMINA_PLUGIN_DEBUG("Waiting for X2Go session to start…");
+ continue;
+ }
+
+ while (!XPending(gpdata->display)) {
+ nanosleep(&ts, NULL);
+ wait_amount--;
+ // Don't spam the console. Print every second though.
+ if (wait_amount % 5 == 0) {
+ REMMINA_PLUGIN_INFO("%s", _("Waiting for PyHoca-CLI to "
+ "show the session's window…"));
+ }
+ continue;
+ }
+
+ XNextEvent(gpdata->display, &xev);
+ // Just ignore non CreatNotify events.
+ if (xev.type != CreateNotify) {
+ non_createnotify_count++;
+ if (non_createnotify_count % 5 == 0) {
+ REMMINA_PLUGIN_DEBUG("Saw '%i' X11 events, which weren't "
+ "CreateNotify.", non_createnotify_count);
+ }
+ continue;
+ }
+
+ w = xev.xcreatewindow.window;
+ if (XGetWindowProperty(gpdata->display, w, atom, 0, 255, False,
+ AnyPropertyType, &type, &format, &nitems, &rest,
+ &data) != Success) {
+ REMMINA_PLUGIN_DEBUG("Could not get WM_COMMAND property from X11 "
+ "window ID [0x%lx].", w);
+ continue;
+ }
+
+ if (data) {
+ REMMINA_PLUGIN_DEBUG("Saw '%i' X11 events, which weren't "
+ "CreateNotify.", non_createnotify_count);
+ REMMINA_PLUGIN_DEBUG("Found X11 window with WM_COMMAND set "
+ "to '%s', the window ID is [0x%lx].",
+ (char*)data, w);
+ }
+ if (data && g_strrstr((gchar*)data, cmd) &&
+ rmplugin_x2go_try_window_id(w)) {
+ gpdata->window_id = w;
+ agent_window_found = TRUE;
+ XFree(data);
+ break;
+ }
+ if (data)
+ XFree(data);
+ }
+
+ XSetErrorHandler(gpdata->orig_handler);
+ XCloseDisplay(gpdata->display);
+ gpdata->display = NULL;
+
+ CANCEL_ASYNC
+
+ if (!agent_window_found) {
+ g_strlcpy(errmsg, _("No X2Go session window appeared. "
+ "Something went wrong…"), 512);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean rmplugin_x2go_start_session(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ REMMINA_PLUGIN_DEBUG("Function entry.");
+
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);;
+ RemminaFile *remminafile;
+ const gchar errmsg[512] = {0};
+ gboolean ret = TRUE;
+
+ gchar *servstr, *host, *username, *password, *command, *kbdlayout, *kbdtype,
+ *audio, *clipboard, *res, *ssh_privatekey;
+ gint sshport, dpi;
+ GdkDisplay *default_dsp;
+ gint width, height;
+
+ // We save the X Display name (:0) as we will need to synchronize the clipboards
+ default_dsp = gdk_display_get_default();
+ const gchar *default_dsp_name = gdk_display_get_name(default_dsp);
+ REMMINA_PLUGIN_DEBUG("Default display is '%s'.", default_dsp_name);
+
+ remminafile = rm_plugin_service->protocol_plugin_get_file(gp);
+
+ servstr = GET_PLUGIN_STRING("server");
+ if (servstr) {
+ rm_plugin_service->get_server_port(servstr, 22, &host, &sshport);
+ } else {
+ return FALSE;
+ }
+
+ if (!sshport) sshport=22;
+
+ username = GET_PLUGIN_STRING("username");
+ password = GET_PLUGIN_PASSWORD("password");
+
+ command = GET_PLUGIN_STRING("command");
+ if (!command) command = "TERMINAL";
+
+ kbdlayout = GET_PLUGIN_STRING("kbdlayout");
+ kbdtype = GET_PLUGIN_STRING("kbdtype");
+
+ audio = GET_PLUGIN_STRING("audio");
+
+ clipboard = GET_PLUGIN_STRING("clipboard");
+
+ dpi = GET_PLUGIN_INT("dpi", 80);
+
+ ssh_privatekey = GET_PLUGIN_STRING("ssh_privatekey");
+
+ // If empty set to NULL
+ if(ssh_privatekey && g_str_equal(ssh_privatekey, "")) {
+ ssh_privatekey = NULL;
+ }
+
+ width = rm_plugin_service->get_profile_remote_width(gp);
+ height = rm_plugin_service->get_profile_remote_height(gp);
+ /* multiple of 4 */
+ width = (width + 3) & ~0x3;
+ height = (height + 3) & ~0x3;
+ if ((width > 0) && (height > 0)) {
+ res = g_strdup_printf ("%dx%d", width, height);
+ } else {
+ res = "800x600";
+ }
+ REMMINA_PLUGIN_DEBUG("Resolution set by user: '%s'.", res);
+
+ REMMINA_PLUGIN_DEBUG("Attached window to socket '%d'.", gpdata->socket_id);
+
+ /* register for notifications of window creation events */
+ if (ret) ret = rmplugin_x2go_start_create_notify(gp, (gchar*)&errmsg);
+
+ /* trigger the session start, session window should appear soon after this */
+ if (ret) ret = rmplugin_x2go_exec_x2go(host, sshport, username, password, command,
+ kbdlayout, kbdtype, audio, clipboard, dpi,
+ res, ssh_privatekey, gp,
+ (gchar*)&errmsg);
+
+ /* get the window ID of the remote x2goagent */
+ if (ret) ret = rmplugin_x2go_monitor_create_notify(gp, "x2goagent",
+ (gchar*)&errmsg);
+
+ if (!ret) {
+ REMMINA_PLUGIN_CRITICAL("%s", errmsg);
+ rm_plugin_service->protocol_plugin_set_error(gp, "%s", &errmsg);
+ return FALSE;
+ }
+
+ /* embed it */
+ onMainThread_gtk_socket_add_id(GTK_SOCKET(gpdata->socket), gpdata->window_id);
+
+ return TRUE;
+}
+
+static gboolean rmplugin_x2go_main(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+ gboolean ret = FALSE;
+
+ ret = rmplugin_x2go_start_session(gp);
+
+ gpdata->thread = 0;
+ return ret;
+}
+
+static gpointer rmplugin_x2go_main_thread(RemminaProtocolWidget* gp)
+{
+ TRACE_CALL(__func__);
+ if (!gp) {
+ REMMINA_PLUGIN_CRITICAL("%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("RemminaProtocolWidget* gp is 'NULL'!")
+ ));
+ return NULL;
+ }
+
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+
+ CANCEL_ASYNC
+ if (!rmplugin_x2go_main(gp)) {
+ IDLE_ADD((GSourceFunc) rmplugin_x2go_cleanup, gp);
+ }
+
+ return NULL;
+}
+
+static gboolean rmplugin_x2go_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginX2GoData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (!rm_plugin_service->gtksocket_available()) {
+ rm_plugin_service->protocol_plugin_set_error(gp, _("The %s protocol is "
+ "unavailable because GtkSocket only works under X.org"),
+ PLUGIN_NAME);
+ return FALSE;
+ }
+
+ gpdata->socket_id = gtk_socket_get_id(GTK_SOCKET(gpdata->socket));
+ // casting to void* is allowed since return type 'gpointer' is actually void*.
+ if (pthread_create(&gpdata->thread, NULL, (void*) rmplugin_x2go_main_thread, gp)) {
+ rm_plugin_service->protocol_plugin_set_error(gp, _("Could not initialize "
+ "pthread. Falling back to non-threaded mode…"));
+ gpdata->thread = 0;
+ return FALSE;
+ } else {
+ return TRUE;
+ }
+}
+
+static gboolean rmplugin_x2go_query_feature(RemminaProtocolWidget* gp,
+ const RemminaProtocolFeature* feature)
+{
+ TRACE_CALL(__func__);
+ return TRUE;
+}
+
+static const RemminaProtocolFeature rmplugin_x2go_features[] = {
+ {REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET, RMPLUGIN_X2GO_FEATURE_GTKSOCKET, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, NULL}
+};
+
+
+/**
+ * @brief This function builds a string like: "'value1', 'value2' and 'value3'" \n
+ * To be used in a loop. \n
+ * See rmplugin_x2go_string_setting_validator() for an example.
+ *
+ * @param max_elements Number of maximum elements.
+ * @param element_to_add Next element to add to the string
+ * @param current_element Which element is element_to_add?
+ * @param string The string to which `element_to_add` will be added.
+ */
+static gchar* rmplugin_x2go_enumeration_prettifier(const guint max_elements,
+ const guint current_element,
+ gchar* element_to_add,
+ gchar* string)
+{
+ if (max_elements > 2) {
+ if (current_element == max_elements - 1) {
+ // TRANSLATORS: Presumably you just want to translate 'and' into
+ // your language.
+ // (Except your listing-grammar differs from english.)
+ // 'value1', 'value2', 'valueN-1' and 'valueN'
+ return g_strdup_printf(_("%sand '%s'"), string, element_to_add);
+ } else if (current_element == max_elements - 2) {
+ // TRANSLATORS: Presumably you just want to leave it english.
+ // (Except your listing-grammar differs from english.)
+ // 'value1', 'value2', 'valueN-1' and 'valueN'
+ return g_strdup_printf(_("%s'%s' "), string, element_to_add);
+ } else {
+ // TRANSLATORS: Presumably you just want to leave it english.
+ // (Except your listing-grammar differs from english.)
+ // 'value1', 'value2', 'valueN-1' and 'valueN'
+ return g_strdup_printf(_("%s'%s', "), string, element_to_add);
+ }
+ } else if (max_elements == 2) {
+ if (current_element == max_elements - 1) {
+ // TRANSLATORS: Presumably you just want to translate 'and' into
+ // your language.
+ // (Except your listing-grammar differs from english.)
+ // 'value1' and 'value2'
+ return g_strdup_printf(_("%sand '%s'"), string, element_to_add);
+ } else {
+ // TRANSLATORS: Presumably you just want to leave it english.
+ // (Except your listing-grammar differs from english.)
+ // 'value1' and 'value2'
+ return g_strdup_printf(_("%s'%s' "), string, element_to_add);
+ }
+ } else {
+ return g_strdup(element_to_add);
+ }
+}
+
+/**
+ * @brief Validator-functions are getting executed when the user wants to save profile
+ * settings. It uses the given data (See RemminaProtocolSetting array) to determine
+ * which strings are allowed and returns a end-user friendly error message.
+ *
+ * @param key Key is the setting's name.
+ * @param value Value to validate.
+ * @param data Data needed for validation process. See RemminaProtocolSetting array.
+ *
+ * @returns End-user friendly and translated error message, explaining why the given
+ * value is invalid. If the given value is error-free then NULL gets returned.
+ *
+ */
+static GError* rmplugin_x2go_string_setting_validator(gchar* key, gchar* value,
+ gchar* data)
+{
+ GError *error = NULL;
+
+ if (!data) {
+ gchar *error_msg = _("Invalid validation data in ProtocolSettings array!");
+ REMMINA_PLUGIN_CRITICAL("%s", error_msg);
+ g_set_error(&error, 1, 1, "%s", error_msg);
+ return error;
+ }
+
+ gchar **elements_list = g_strsplit(data, ",", 0);
+
+ guint elements_amount = 0;
+ elements_amount = g_strv_length(elements_list);
+
+ if (elements_list == NULL ||
+ elements_list[0] == NULL ||
+ strlen(elements_list[0]) <= 0)
+ {
+ gchar *error_msg = _("Validation data in ProtocolSettings array is invalid!");
+ REMMINA_PLUGIN_CRITICAL("%s", error_msg);
+ g_set_error(&error, 1, 1, "%s", error_msg);
+ return error;
+ }
+
+ gchar *data_str = "";
+
+ if (!key || !value) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Parameters 'key' or 'value' are 'NULL'!"));
+ g_set_error(&error, 1, 1, "%s", _("Internal error."));
+ return error;
+ }
+
+ for (guint i = 0; elements_list[i] != NULL; i++) {
+ // Don't wanna crash if elements_list[i] is NULL.
+ gchar* element = elements_list[i] ? elements_list[i] : "";
+ if (g_strcmp0(value, element) == 0) {
+ // We found value in elements_list. Value passed validation.
+ return NULL;
+ }
+
+ data_str = rmplugin_x2go_enumeration_prettifier(elements_amount, i,
+ element, data_str);
+ }
+
+ if (elements_amount > 1) {
+ g_set_error(&error, 1, 1, _("Allowed values are %s."), data_str);
+ } else {
+ g_set_error(&error, 1, 1, _("The only allowed value is '%s'."), data_str);
+ }
+
+ g_free(data_str);
+ g_strfreev(elements_list);
+
+ return error;
+}
+
+/**
+ * @brief Validator-functions are getting executed when the user wants to save profile
+ * settings. It uses the given data (See RemminaProtocolSetting array) to determine
+ * if the given value is a valid integer is in range and returns a end-user
+ * friendly error message.
+ *
+ * @param key Key is the setting's name.
+ * @param value Value to validate.
+ * @param data Data needed for validation process. See RemminaProtocolSetting array.
+ *
+ * @returns End-user friendly and translated error message, explaining why the given
+ * value is invalid. If the given value is error-free then NULL gets returned.
+ *
+ */
+static GError* rmplugin_x2go_int_setting_validator(gchar* key, gpointer value,
+ gchar* data)
+{
+ GError *error = NULL;
+
+ gchar **integer_list = g_strsplit(data, ";", 0);
+
+ if (integer_list == NULL ||
+ integer_list[0] == NULL ||
+ integer_list[1] == NULL ||
+ strlen(integer_list[0]) <= 0 ||
+ strlen(integer_list[1]) <= 0)
+ {
+ gchar *error_msg = _("Validation data in ProtocolSettings array is invalid!");
+ REMMINA_PLUGIN_CRITICAL("%s", error_msg);
+ g_set_error(&error, 1, 1, "%s", error_msg);
+ return error;
+ }
+
+ gint minimum;
+ str2int_errno err = str2int(&minimum, integer_list[0], 10);
+ if (err == STR2INT_INCONVERTIBLE) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The lower limit is not a valid integer!")
+ ));
+ } else if (err == STR2INT_OVERFLOW) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The lower limit is too high!")
+ ));
+ } else if (err == STR2INT_UNDERFLOW) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The lower limit is too low!")
+ ));
+ } else if (err == STR2INT_INVALID_DATA) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Something unknown went wrong.")
+ ));
+ }
+
+ if (error) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Please check the RemminaProtocolSetting "
+ "array for possible errors."));
+ return error;
+ }
+
+ gint maximum;
+ err = str2int(&maximum, integer_list[1], 10);
+ if (err == STR2INT_INCONVERTIBLE) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The upper limit is not a valid integer!")
+ ));
+ } else if (err == STR2INT_OVERFLOW) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The upper limit is too high!")
+ ));
+ } else if (err == STR2INT_UNDERFLOW) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("The upper limit is too low!")
+ ));
+ } else if (err == STR2INT_INVALID_DATA) {
+ g_set_error(&error, 1, 1, "%s", g_strdup_printf(
+ _("Internal error: %s"),
+ _("Something unknown went wrong.")
+ ));
+ }
+
+ if (error) {
+ REMMINA_PLUGIN_CRITICAL("%s", _("Please check the RemminaProtocolSetting "
+ "array for possible errors."));
+ return error;
+ }
+
+ gint int_value;
+ err = str2int(&int_value, value, 10);
+ if (err == STR2INT_INCONVERTIBLE) {
+ // non-numerical characters are can't be entered but, the user can
+ // input an empty string.
+ g_set_error(&error, 1, 1, "%s", _("The input is not a valid integer!"));
+ } else if (err == STR2INT_OVERFLOW || err == STR2INT_UNDERFLOW) {
+ g_set_error(&error, 1, 1, _("Input must be a number between %i and %i."),
+ minimum, maximum);
+ } else if (err == STR2INT_INVALID_DATA) {
+ g_set_error(&error, 1, 1, "%s", _("Something unknown went wrong."));
+ }
+
+ if (error) {
+ return error;
+ }
+
+ /*REMMINA_PLUGIN_DEBUG("Key: \t%s", (gchar*) key);
+ REMMINA_PLUGIN_DEBUG("Value:\t%s", (gchar*) value);
+ REMMINA_PLUGIN_DEBUG("Data: \t%s", data);
+ REMMINA_PLUGIN_DEBUG("Min: %i, Max: %i", minimum, maximum);
+ REMMINA_PLUGIN_DEBUG("Value converted:\t%i", int_value);*/
+
+ if (err == STR2INT_SUCCESS && (minimum > int_value || int_value > maximum)) {
+ g_set_error(&error, 1, 1, _("Input must be a number between %i and %i."),
+ minimum, maximum);
+ }
+
+ // Should be NULL.
+ return error;
+}
+
+/* Array of RemminaProtocolSetting for basic settings.
+ * Each item is composed by:
+ * a) RemminaProtocolSettingType for setting type
+ * b) Setting name
+ * c) Setting description
+ * d) Compact disposition
+ * e) Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO
+ * f) Setting tooltip
+ * g) Validation data pointer, will be passed to the validation callback method.
+ * h) Validation callback method (Can be NULL. Every entry will be valid then.)
+ * use following prototype:
+ * gboolean mysetting_validator_method(gpointer key, gpointer value,
+ * gpointer validator_data);
+ * gpointer key is a gchar* containing the setting's name,
+ * gpointer value contains the value which should be validated,
+ * gpointer validator_data contains your passed data.
+ */
+static const RemminaProtocolSetting rmplugin_x2go_basic_settings[] = {
+ {REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "username", N_("Username"), FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "password", N_("Password"), FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "command", N_("Startup program"), FALSE,
+ /* SELECT & COMBO Values */ "MATE,KDE,XFCE,LXDE,TERMINAL",
+ /* Tooltip */ N_("Which command should be executed after creating the X2Go session?"), NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION, "resolution", NULL, FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "kbdlayout", N_("Keyboard Layout (auto)"), FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "kbdtype", N_("Keyboard type (auto)"), FALSE, NULL, NULL, NULL, NULL},
+ {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "audio", N_("Audio support"), FALSE,
+ /* SELECT & COMBO Values */ "pulse,esd,none",
+ /* Tooltip */ N_("The sound system of the X2Go server (default: 'pulse')."),
+ /* Validation data */ "pulse,esd,none",
+ /* Validation method */ G_CALLBACK(rmplugin_x2go_string_setting_validator)},
+ {REMMINA_PROTOCOL_SETTING_TYPE_COMBO, "clipboard", N_("Clipboard direction"), FALSE,
+ /* SELECT & COMBO Values */ "none,server,client,both",
+ /* Tooltip */ N_("Which direction should clipboard content be copied? "
+ "(default: 'both')."),
+ /* Validation data */ "none,server,client,both",
+ /* Validation method */ G_CALLBACK(rmplugin_x2go_string_setting_validator)},
+ {REMMINA_PROTOCOL_SETTING_TYPE_INT, "dpi", N_("DPI resolution"), FALSE, NULL,
+ /* Tooltip */ N_("Launch session with a specific resolution (in dots per inch). "
+ "Must be between 20 and 400."),
+ /* Validation data */ "20;400", // "<min>;<max>;"
+ /* Validation method */ G_CALLBACK(rmplugin_x2go_int_setting_validator)},
+ {REMMINA_PROTOCOL_SETTING_TYPE_FILE, "ssh_privatekey", N_("SSH identity file"), FALSE, NULL, N_("Your private key"), NULL, NULL },
+ {REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL}};
+
+/* Protocol plugin definition and features */
+static RemminaProtocolPlugin rmplugin_x2go = {
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ PLUGIN_NAME, // Name
+ PLUGIN_DESCRIPTION, // Description
+ GETTEXT_PACKAGE, // Translation domain
+ PLUGIN_VERSION, // Version number
+ PLUGIN_APPICON, // Icon for normal connection
+ PLUGIN_SSH_APPICON, // Icon for SSH connection
+ rmplugin_x2go_basic_settings, // Array for basic settings
+ NULL, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_NONE, // SSH settings type
+ rmplugin_x2go_features, // Array for available features
+ rmplugin_x2go_init, // Plugin initialization method
+ rmplugin_x2go_open_connection, // Plugin open connection method
+ rmplugin_x2go_close_connection, // Plugin connection-method closure
+ rmplugin_x2go_query_feature, // Query for available features
+ NULL, // Call a feature
+ NULL, // Send a keystroke
+ NULL, // Screenshot
+};
+
+G_MODULE_EXPORT gboolean remmina_plugin_entry(RemminaPluginService *service)
+{
+ TRACE_CALL("remmina_plugin_entry");
+ rm_plugin_service = service;
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+
+ if (!service->register_plugin((RemminaPlugin *) &rmplugin_x2go)) {
+ return FALSE;
+ }
+
+ pthread_mutex_init(&remmina_x2go_init_mutex, NULL);
+ remmina_x2go_window_id_array = g_array_new(FALSE, TRUE, sizeof(Window));
+
+ REMMINA_PLUGIN_MESSAGE("%s", _("X2Go plugin loaded."));
+
+ return TRUE;
+}
diff --git a/plugins/x2go/x2go_plugin.h b/plugins/x2go/x2go_plugin.h
new file mode 100644
index 0000000..5fc53a6
--- /dev/null
+++ b/plugins/x2go/x2go_plugin.h
@@ -0,0 +1,46 @@
+/*
+ * Project: Remmina Plugin X2Go
+ * Description: Remmina protocol plugin to connect via X2Go using PyHoca
+ * Based on Fabio Castelli Team Viewer Plugin
+ * Copyright: 2013-2014 Fabio Castelli (Muflone)
+ * Author: Antenore Gatta <antenore@simbiosi.org>
+ * Copyright: 2015 Antenore Gatta
+ * License: GPL-2+
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * In addition, as a special exception, the copyright holders give
+ * permission to link the code of portions of this program with the
+ * OpenSSL library under certain conditions as described in each
+ * individual source file, and distribute linked combinations
+ * including the two.
+ * You must obey the GNU General Public License in all respects
+ * for all of the code used other than OpenSSL. * If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. * If you
+ * do not wish to do so, delete this exception statement from your
+ * version. * If you delete this exception statement from all source
+ * files in the program, then also delete it here.
+ *
+ */
+
+#pragma once
+
+#define PLUGIN_NAME "X2GO"
+#define PLUGIN_DESCRIPTION N_("X2Go - Launch an X2Go session")
+#define PLUGIN_VERSION "2.0.0"
+#define PLUGIN_APPICON "org.remmina.Remmina-x2go-symbolic"
+#define PLUGIN_SSH_APPICON "org.remmina.Remmina-x2go-ssh-symbolic"