summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt282
-rw-r--r--src/external_tools/CMakeLists.txt44
-rwxr-xr-xsrc/external_tools/functions.sh16
-rwxr-xr-xsrc/external_tools/launcher.sh28
-rwxr-xr-xsrc/external_tools/remmina_filezilla_sftp.sh7
-rwxr-xr-xsrc/external_tools/remmina_filezilla_sftp_pki.sh7
-rwxr-xr-xsrc/external_tools/remmina_nslookup.sh8
-rwxr-xr-xsrc/external_tools/remmina_ping.sh8
-rwxr-xr-xsrc/external_tools/remmina_traceroute.sh8
-rw-r--r--src/include/remmina/plugin.h326
-rw-r--r--src/include/remmina/remmina_trace_calls.h51
-rw-r--r--src/include/remmina/types.h164
-rw-r--r--src/pygobject.h669
-rw-r--r--src/rcw.c4812
-rw-r--r--src/rcw.h93
-rw-r--r--src/remmina.1229
-rw-r--r--src/remmina.c489
-rw-r--r--src/remmina.h43
-rw-r--r--src/remmina.scd172
-rw-r--r--src/remmina_about.c64
-rw-r--r--src/remmina_about.h44
-rw-r--r--src/remmina_applet_menu.c283
-rw-r--r--src/remmina_applet_menu.h80
-rw-r--r--src/remmina_applet_menu_item.c199
-rw-r--r--src/remmina_applet_menu_item.h75
-rw-r--r--src/remmina_avahi.c300
-rw-r--r--src/remmina_avahi.h56
-rw-r--r--src/remmina_bug_report.c411
-rw-r--r--src/remmina_bug_report.h61
-rw-r--r--src/remmina_chat_window.c255
-rw-r--r--src/remmina_chat_window.h68
-rw-r--r--src/remmina_crypt.c198
-rw-r--r--src/remmina_crypt.h45
-rw-r--r--src/remmina_curl_connector.c275
-rw-r--r--src/remmina_curl_connector.h54
-rw-r--r--src/remmina_exec.c544
-rw-r--r--src/remmina_exec.h71
-rw-r--r--src/remmina_ext_exec.c133
-rw-r--r--src/remmina_ext_exec.h52
-rw-r--r--src/remmina_external_tools.c157
-rw-r--r--src/remmina_external_tools.h46
-rw-r--r--src/remmina_file.c1134
-rw-r--r--src/remmina_file.h126
-rw-r--r--src/remmina_file_editor.c2204
-rw-r--r--src/remmina_file_editor.h80
-rw-r--r--src/remmina_file_manager.c372
-rw-r--r--src/remmina_file_manager.h61
-rw-r--r--src/remmina_ftp_client.c1276
-rw-r--r--src/remmina_ftp_client.h158
-rw-r--r--src/remmina_icon.c491
-rw-r--r--src/remmina_icon.h49
-rw-r--r--src/remmina_info.c1328
-rw-r--r--src/remmina_info.h71
-rw-r--r--src/remmina_key_chooser.c146
-rw-r--r--src/remmina_key_chooser.h64
-rw-r--r--src/remmina_log.c505
-rw-r--r--src/remmina_log.h66
-rw-r--r--src/remmina_main.c1844
-rw-r--r--src/remmina_main.h146
-rw-r--r--src/remmina_marshals.c161
-rw-r--r--src/remmina_marshals.h50
-rw-r--r--src/remmina_marshals.list6
-rw-r--r--src/remmina_masterthread_exec.c151
-rw-r--r--src/remmina_masterthread_exec.h136
-rw-r--r--src/remmina_message_panel.c801
-rw-r--r--src/remmina_message_panel.h85
-rw-r--r--src/remmina_monitor.c229
-rw-r--r--src/remmina_monitor.h57
-rw-r--r--src/remmina_mpchange.c518
-rw-r--r--src/remmina_mpchange.h45
-rw-r--r--src/remmina_passwd.c163
-rw-r--r--src/remmina_passwd.h58
-rw-r--r--src/remmina_plugin_manager.c1555
-rw-r--r--src/remmina_plugin_manager.h103
-rw-r--r--src/remmina_plugin_native.c95
-rw-r--r--src/remmina_plugin_native.h45
-rw-r--r--src/remmina_pref.c1252
-rw-r--r--src/remmina_pref.h295
-rw-r--r--src/remmina_pref_dialog.c883
-rw-r--r--src/remmina_pref_dialog.h180
-rw-r--r--src/remmina_protocol_widget.c2226
-rw-r--r--src/remmina_protocol_widget.h187
-rw-r--r--src/remmina_public.c735
-rw-r--r--src/remmina_public.h126
-rw-r--r--src/remmina_scheduler.c90
-rw-r--r--src/remmina_scheduler.h51
-rw-r--r--src/remmina_scrolled_viewport.c218
-rw-r--r--src/remmina_scrolled_viewport.h74
-rw-r--r--src/remmina_sftp_client.c1057
-rw-r--r--src/remmina_sftp_client.h80
-rw-r--r--src/remmina_sftp_plugin.c401
-rw-r--r--src/remmina_sftp_plugin.h47
-rw-r--r--src/remmina_sodium.c192
-rw-r--r--src/remmina_sodium.h52
-rw-r--r--src/remmina_ssh.c3238
-rw-r--r--src/remmina_ssh.h284
-rw-r--r--src/remmina_ssh_plugin.c1738
-rw-r--r--src/remmina_ssh_plugin.h67
-rw-r--r--src/remmina_string_array.c176
-rw-r--r--src/remmina_string_array.h56
-rw-r--r--src/remmina_string_list.c312
-rw-r--r--src/remmina_string_list.h84
-rw-r--r--src/remmina_sysinfo.c155
-rw-r--r--src/remmina_sysinfo.h45
-rw-r--r--src/remmina_unlock.c238
-rw-r--r--src/remmina_unlock.h63
-rw-r--r--src/remmina_utils.c1121
-rw-r--r--src/remmina_utils.h86
-rw-r--r--src/remmina_widget_pool.c144
-rw-r--r--src/remmina_widget_pool.h51
110 files changed, 41310 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..9a0627c
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,282 @@
+# src/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.
+
+list(
+ APPEND
+ REMMINA_SRCS
+ "remmina_about.c"
+ "remmina_about.h"
+ "remmina_applet_menu.c"
+ "remmina_applet_menu.h"
+ "remmina_applet_menu_item.c"
+ "remmina_applet_menu_item.h"
+ "remmina_avahi.c"
+ "remmina_avahi.h"
+ "remmina.c"
+ "remmina.h"
+ "remmina_bug_report.c"
+ "remmina_bug_report.h"
+ "remmina_chat_window.c"
+ "remmina_chat_window.h"
+ "remmina_crypt.c"
+ "remmina_crypt.h"
+ "remmina_exec.c"
+ "remmina_exec.h"
+ "remmina_file.c"
+ "remmina_file_editor.c"
+ "remmina_file_editor.h"
+ "remmina_file.h"
+ "remmina_file_manager.c"
+ "remmina_file_manager.h"
+ "remmina_ftp_client.c"
+ "remmina_ftp_client.h"
+ "remmina_icon.c"
+ "remmina_icon.h"
+ "remmina_key_chooser.c"
+ "remmina_key_chooser.h"
+ "remmina_log.c"
+ "remmina_log.h"
+ "remmina_main.c"
+ "remmina_main.h"
+ "remmina_monitor.c"
+ "remmina_monitor.h"
+ "remmina_marshals.c"
+ "remmina_marshals.h"
+ "remmina_marshals.list"
+ "remmina_masterthread_exec.c"
+ "remmina_masterthread_exec.h"
+ "remmina_message_panel.c"
+ "remmina_message_panel.h"
+ "remmina_plugin_manager.c"
+ "remmina_plugin_manager.h"
+ "remmina_plugin_native.c"
+ "remmina_plugin_native.h"
+ "remmina_ext_exec.c"
+ "remmina_ext_exec.h"
+ "remmina_passwd.c"
+ "remmina_passwd.h"
+ "remmina_pref.c"
+ "remmina_pref_dialog.c"
+ "remmina_pref_dialog.h"
+ "remmina_pref.h"
+ "remmina_protocol_widget.c"
+ "remmina_protocol_widget.h"
+ "remmina_public.c"
+ "remmina_public.h"
+ "remmina_scrolled_viewport.c"
+ "remmina_scrolled_viewport.h"
+ "remmina_sftp_client.c"
+ "remmina_sftp_client.h"
+ "remmina_sftp_plugin.c"
+ "remmina_sftp_plugin.h"
+ "remmina_sodium.c"
+ "remmina_sodium.h"
+ "remmina_curl_connector.c"
+ "remmina_curl_connector.h"
+ "remmina_ssh.c"
+ "remmina_ssh.h"
+ "remmina_ssh_plugin.c"
+ "remmina_ssh_plugin.h"
+ "remmina_string_array.c"
+ "remmina_string_array.h"
+ "remmina_string_list.c"
+ "remmina_string_list.h"
+ "remmina_unlock.c"
+ "remmina_unlock.h"
+ "remmina_utils.c"
+ "remmina_utils.h"
+ "remmina_widget_pool.c"
+ "remmina_widget_pool.h"
+ "remmina_external_tools.c"
+ "remmina_external_tools.h"
+ "remmina_sysinfo.h"
+ "remmina_sysinfo.c"
+ "rcw.c"
+ "rcw.h"
+ "remmina_mpchange.c"
+ "remmina_mpchange.h"
+ "remmina_scheduler.c"
+ "remmina_scheduler.h"
+ "remmina_info.c"
+ "remmina_info.h"
+ "resources.c")
+
+set(RESOURCE_LIST
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_about.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_bug_report.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_key_chooser.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_main.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_mpc.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_info.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_passwd.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_preferences.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_search.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_search_popover.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_snap_info_dialog.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_spinner.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_string_list.glade
+ ${CMAKE_CURRENT_SOURCE_DIR}/../data/ui/remmina_unlock.glade)
+message(DEBUG "Source list set to: " ${REMMINA_SRCS})
+
+compile_gresources(
+ RESOURCE_FILE
+ XML_OUT
+ TYPE
+ EMBED_C
+ PREFIX
+ /org/remmina/Remmina/src
+ SOURCE_DIR
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ RESOURCES
+ ${RESOURCE_LIST})
+
+add_custom_target(resource DEPENDS ${RESOURCE_FILE})
+add_executable(remmina ${REMMINA_SRCS} ${RESOURCE_FILE})
+add_dependencies(remmina resource)
+target_link_libraries(remmina ${GTK_LIBRARIES} -rdynamic)
+
+if(WITH_PYTHONLIBS)
+ if(PythonLibs_FOUND)
+ include_directories(${PYTHON_INCLUDE_DIRS})
+ target_link_libraries(remmina ${PYTHON_LIBRARIES})
+ endif()
+endif()
+
+if(WITH_MANPAGES)
+ install(FILES remmina.1 DESTINATION ${CMAKE_INSTALL_FULL_MANDIR}/man1)
+endif()
+
+target_link_libraries(remmina ${CMAKE_THREAD_LIBS_INIT})
+
+if(Intl_FOUND)
+ message(STATUS "${CMAKE_SYSTEM_NAME} detected, building with Intl")
+ include_directories(${Intl_INCLUDE_DIRS})
+ target_link_libraries(remmina ${Intl_LIBRARIES})
+endif()
+
+find_suggested_package(LIBSSH)
+if(LIBSSH_FOUND)
+ add_definitions(-DHAVE_LIBSSH)
+ include_directories(${LIBSSH_INCLUDE_DIRS})
+ target_link_libraries(remmina ${LIBSSH_LIBRARIES})
+endif()
+
+if(GCRYPT_FOUND)
+ include_directories(${GCRYPT_INCLUDE_DIRS})
+ target_link_libraries(remmina ${GCRYPT_LIBRARIES})
+endif()
+
+if(AVAHI_FOUND)
+ include_directories(${AVAHI_INCLUDE_DIRS})
+ target_link_libraries(remmina ${AVAHI_LIBRARIES})
+endif()
+
+if(OPENSSL_FOUND)
+ include_directories(${OPENSSL_INCLUDE_DIRS})
+ target_link_libraries(remmina ${OPENSSL_LIBRARIES})
+endif()
+
+option(WITH_VTE "Build with support for VTE" ON)
+if(GTK3_FOUND AND WITH_VTE)
+ set(_VTE_VERSION_NUMS 2.91 2.90)
+ foreach(__VTE_VERSION ${_VTE_VERSION_NUMS})
+ set(_VTE_VERSION_NUM ${__VTE_VERSION})
+ find_suggested_package(VTE)
+ if(VTE_FOUND)
+ break()
+ endif()
+ message(STATUS "VTE ${__VTE_VERSION} not found")
+ endforeach()
+elseif(WITH_VTE)
+ set(_VTE_VERSION_NUM)
+ find_package(VTE)
+endif()
+
+if(VTE_FOUND)
+ add_definitions(-DHAVE_LIBVTE)
+ include_directories(${VTE_INCLUDE_DIRS})
+ target_link_libraries(remmina ${VTE_LIBRARIES})
+endif()
+
+if(sodium_FOUND)
+ message(STATUS "Sodium found")
+ message(STATUS "Sodium include dirs ${sodium_INCLUDE_DIR}")
+ include_directories(${sodium_INCLUDE_DIR})
+ target_link_libraries(remmina sodium)
+endif()
+
+option(HAVE_LIBAPPINDICATOR "Build with support for status icon. Appindicator is required" ON)
+if(GTK3_FOUND)
+ if(HAVE_LIBAPPINDICATOR)
+ find_required_package(APPINDICATOR)
+ if(APPINDICATOR_FOUND)
+ add_definitions(-DHAVE_LIBAPPINDICATOR)
+ include_directories(${APPINDICATOR_INCLUDE_DIRS})
+ target_link_libraries(remmina ${APPINDICATOR_LIBRARIES})
+ message(STATUS "AppIndicator library dirs ${APPINDICATOR_LIBRARIES}")
+ message(STATUS "AppIndicator include dirs ${APPINDICATOR_INCLUDE_DIRS}")
+ else()
+ message(FATAL_ERROR "AppIndicator not found")
+ endif()
+ endif()
+ find_required_package(JSONGLIB)
+ if(JSONGLIB_FOUND)
+ include_directories(${JSONGLIB_INCLUDE_DIRS})
+ target_link_libraries(remmina ${JSONGLIB_LIBRARIES})
+ else()
+ message(FATAL_ERROR "json-glib library not found")
+ endif()
+endif()
+
+find_required_package(CURL)
+if(NOT CURL_FOUND)
+ message(FATAL_ERROR "libcurl not found")
+else()
+ include_directories(${CURL_INCLUDE_DIRS})
+ target_link_libraries(remmina ${CURL_LIBRARIES})
+endif()
+
+find_required_package(PCRE2)
+if(NOT PCRE2_FOUND)
+ message(FATAL_ERROR "libpcre2 library not found")
+endif()
+include_directories(${PCRE2_INCLUDE_DIRS})
+
+add_subdirectory(external_tools)
+
+install(TARGETS remmina DESTINATION ${CMAKE_INSTALL_BINDIR})
+install(
+ DIRECTORY include/remmina/
+ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/remmina
+ FILES_MATCHING
+ PATTERN "*.h")
+
+add_definitions(-DG_LOG_DOMAIN="remmina")
diff --git a/src/external_tools/CMakeLists.txt b/src/external_tools/CMakeLists.txt
new file mode 100644
index 0000000..f7b17cf
--- /dev/null
+++ b/src/external_tools/CMakeLists.txt
@@ -0,0 +1,44 @@
+# desktop/remmina - 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.
+
+INSTALL(PROGRAMS
+ launcher.sh
+ functions.sh
+ remmina_filezilla_sftp.sh
+ remmina_filezilla_sftp_pki.sh
+ remmina_nslookup.sh
+ remmina_ping.sh
+ remmina_traceroute.sh
+ DESTINATION ${REMMINA_EXTERNAL_TOOLS_DIR})
+
+
diff --git a/src/external_tools/functions.sh b/src/external_tools/functions.sh
new file mode 100755
index 0000000..2f70eaf
--- /dev/null
+++ b/src/external_tools/functions.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+# Awaits user key press.
+pause ()
+{
+ echo "Press any key to continue…"
+ OLDCONFIG=$(stty -g)
+ stty -icanon -echo min 1 time 0
+ dd count=1 2>/dev/null
+ stty $OLDCONFIG
+}
+
+# Set terminal title for gnome-terminal and many others
+settitle() {
+ echo -n -e "\033]0;${remmina_term_title}\007"
+}
diff --git a/src/external_tools/launcher.sh b/src/external_tools/launcher.sh
new file mode 100755
index 0000000..ff056cc
--- /dev/null
+++ b/src/external_tools/launcher.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+####################
+# Main Script
+####################
+
+TERMINALS="xfce4-terminal gnome-terminal x-terminal-emulator"
+
+for t in $TERMINALS; do
+ TERMBIN="$(command -v "$t")"
+ if [ "$?" -eq 0 ]; then
+ case "$t" in
+ xfce4-terminal)
+ TERMBIN="$TERMBIN --disable-server"
+ break
+ ;;
+ gnome-terminal)
+ break
+ ;;
+ x-terminal-emulator)
+ break
+ ;;
+ esac
+ fi
+done
+
+$TERMBIN -e "$1" &
+
diff --git a/src/external_tools/remmina_filezilla_sftp.sh b/src/external_tools/remmina_filezilla_sftp.sh
new file mode 100755
index 0000000..63c1b11
--- /dev/null
+++ b/src/external_tools/remmina_filezilla_sftp.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+. $(dirname $0)/functions.sh
+settitle
+
+filezilla sftp://$username:"$password"@$server
+
diff --git a/src/external_tools/remmina_filezilla_sftp_pki.sh b/src/external_tools/remmina_filezilla_sftp_pki.sh
new file mode 100755
index 0000000..5b1ac50
--- /dev/null
+++ b/src/external_tools/remmina_filezilla_sftp_pki.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+. $(dirname $0)/functions.sh
+settitle
+
+filezilla sftp://$ssh_username@$server
+
diff --git a/src/external_tools/remmina_nslookup.sh b/src/external_tools/remmina_nslookup.sh
new file mode 100755
index 0000000..9f48953
--- /dev/null
+++ b/src/external_tools/remmina_nslookup.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+. $(dirname $0)/functions.sh
+settitle
+
+nslookup $server
+
+pause
diff --git a/src/external_tools/remmina_ping.sh b/src/external_tools/remmina_ping.sh
new file mode 100755
index 0000000..f69fe5e
--- /dev/null
+++ b/src/external_tools/remmina_ping.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+. $(dirname $0)/functions.sh
+settitle
+
+ping -c3 $server
+
+pause
diff --git a/src/external_tools/remmina_traceroute.sh b/src/external_tools/remmina_traceroute.sh
new file mode 100755
index 0000000..3b5d8cc
--- /dev/null
+++ b/src/external_tools/remmina_traceroute.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+. $(dirname $0)/functions.sh
+settitle
+
+traceroute $server
+
+pause
diff --git a/src/include/remmina/plugin.h b/src/include/remmina/plugin.h
new file mode 100644
index 0000000..64e367a
--- /dev/null
+++ b/src/include/remmina/plugin.h
@@ -0,0 +1,326 @@
+/*
+ * 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
+ * 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.
+ *
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <stdarg.h>
+#include <remmina/types.h>
+#include "remmina/remmina_trace_calls.h"
+
+G_BEGIN_DECLS
+
+typedef enum {
+ REMMINA_PLUGIN_TYPE_PROTOCOL = 0,
+ REMMINA_PLUGIN_TYPE_ENTRY = 1,
+ REMMINA_PLUGIN_TYPE_FILE = 2,
+ REMMINA_PLUGIN_TYPE_TOOL = 3,
+ REMMINA_PLUGIN_TYPE_PREF = 4,
+ REMMINA_PLUGIN_TYPE_SECRET = 5,
+ REMMINA_PLUGIN_TYPE_LANGUAGE_WRAPPER = 6
+} RemminaPluginType;
+
+typedef struct _RemminaPlugin {
+ RemminaPluginType type;
+ const gchar * name;
+ const gchar * description;
+ const gchar * domain;
+ const gchar * version;
+} RemminaPlugin;
+
+typedef struct _RemminaServerPluginResponse {
+ const gchar * name;
+ const gchar * version;
+ const gchar * file_name;
+ /*
+ * This is the signature received directly from the server. It should be base64 encoded.
+ */
+ const guchar * signature;
+ /*
+ * This is the data received directly from the server. It should be
+ * first compressed with gzip and then base64 encoded.
+ */
+ guchar * data;
+} RemminaServerPluginResponse;
+
+typedef struct _RemminaProtocolPlugin _RemminaProtocolPlugin;
+typedef struct _RemminaProtocolPlugin {
+ RemminaPluginType type;
+ const gchar * name;
+ const gchar * description;
+ const gchar * domain;
+ const gchar * version;
+
+ const gchar * icon_name;
+ const gchar * icon_name_ssh;
+ const RemminaProtocolSetting * basic_settings;
+ const RemminaProtocolSetting * advanced_settings;
+ RemminaProtocolSSHSetting ssh_setting;
+ const RemminaProtocolFeature * features;
+
+ void (*init)(RemminaProtocolWidget *gp);
+ gboolean (*open_connection)(RemminaProtocolWidget *gp);
+ gboolean (*close_connection)(RemminaProtocolWidget *gp);
+ gboolean (*query_feature)(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature);
+ void (*call_feature)(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature);
+ void (*send_keystrokes)(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen);
+ gboolean (*get_plugin_screenshot)(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd);
+ gboolean (*map_event)(RemminaProtocolWidget *gp);
+ gboolean (*unmap_event)(RemminaProtocolWidget *gp);
+} RemminaProtocolPlugin;
+
+typedef struct _RemminaEntryPlugin {
+ RemminaPluginType type;
+ const gchar * name;
+ const gchar * description;
+ const gchar * domain;
+ const gchar * version;
+
+ void (*entry_func)(struct _RemminaEntryPlugin* instance);
+} RemminaEntryPlugin;
+
+typedef struct _RemminaFilePlugin {
+ RemminaPluginType type;
+ const gchar * name;
+ const gchar * description;
+ const gchar * domain;
+ const gchar * version;
+
+ gboolean (*import_test_func)(struct _RemminaFilePlugin* instance, const gchar *from_file);
+ RemminaFile * (*import_func)(struct _RemminaFilePlugin* instance, const gchar * from_file);
+ gboolean (*export_test_func)(struct _RemminaFilePlugin* instance, RemminaFile *file);
+ gboolean (*export_func)(struct _RemminaFilePlugin* instance, RemminaFile *file, const gchar *to_file);
+ const gchar * export_hints;
+} RemminaFilePlugin;
+
+typedef struct _RemminaToolPlugin {
+ RemminaPluginType type;
+ const gchar * name;
+ const gchar * description;
+ const gchar * domain;
+ const gchar * version;
+
+ void (*exec_func)(GtkMenuItem* item, struct _RemminaToolPlugin* instance);
+} RemminaToolPlugin;
+
+typedef struct _RemminaPrefPlugin {
+ RemminaPluginType type;
+ const gchar * name;
+ const gchar * description;
+ const gchar * domain;
+ const gchar * version;
+
+ const gchar * pref_label;
+ GtkWidget * (*get_pref_body)(struct _RemminaPrefPlugin* instance);
+} RemminaPrefPlugin;
+
+typedef struct _RemminaSecretPlugin {
+ RemminaPluginType type;
+ const gchar * name;
+ const gchar * description;
+ const gchar * domain;
+ const gchar * version;
+ int init_order;
+
+ gboolean (*init)(struct _RemminaSecretPlugin* instance);
+ gboolean (*is_service_available)(struct _RemminaSecretPlugin* instance);
+ void (*store_password)(struct _RemminaSecretPlugin* instance, RemminaFile *remminafile, const gchar *key, const gchar *password);
+ gchar * (*get_password)(struct _RemminaSecretPlugin* instance, RemminaFile * remminafile, const gchar *key);
+ void (*delete_password)(struct _RemminaSecretPlugin* instance, RemminaFile *remminafile, const gchar *key);
+} RemminaSecretPlugin;
+
+typedef struct _RemminaLanguageWrapperPlugin {
+ RemminaPluginType type;
+ const gchar * name;
+ const gchar * description;
+ const gchar * domain;
+ const gchar * version;
+ const gchar ** supported_extentions;
+
+ gboolean (*init)(struct _RemminaLanguageWrapperPlugin* instance);
+ gboolean (*load)(struct _RemminaLanguageWrapperPlugin* instance, const gchar* plugin_file);
+} RemminaLanguageWrapperPlugin;
+
+/* Plugin Service is a struct containing a list of function pointers,
+ * which is passed from Remmina main program to the plugin module
+ * through the plugin entry function remmina_plugin_entry() */
+typedef struct _RemminaPluginService {
+ gboolean (*register_plugin)(RemminaPlugin *plugin);
+
+ gint (*protocol_plugin_get_width)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_set_width)(RemminaProtocolWidget *gp, gint width);
+ gint (*protocol_plugin_get_height)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_set_height)(RemminaProtocolWidget *gp, gint height);
+ RemminaScaleMode (*remmina_protocol_widget_get_current_scale_mode)(RemminaProtocolWidget *gp);
+ gboolean (*protocol_plugin_get_expand)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_set_expand)(RemminaProtocolWidget *gp, gboolean expand);
+ gboolean (*protocol_plugin_has_error)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_set_error)(RemminaProtocolWidget *gp, const gchar *fmt, ...);
+ gboolean (*protocol_plugin_is_closed)(RemminaProtocolWidget *gp);
+ RemminaFile * (*protocol_plugin_get_file)(RemminaProtocolWidget * gp);
+ void (*protocol_plugin_emit_signal)(RemminaProtocolWidget *gp, const gchar *signal_name);
+ void (*protocol_plugin_register_hostkey)(RemminaProtocolWidget *gp, GtkWidget *widget);
+ gchar * (*protocol_plugin_start_direct_tunnel)(RemminaProtocolWidget * gp, gint default_port, gboolean port_plus);
+ gboolean (*protocol_plugin_start_reverse_tunnel)(RemminaProtocolWidget *gp, gint local_port);
+ gboolean (*protocol_plugin_start_xport_tunnel)(RemminaProtocolWidget *gp, RemminaXPortTunnelInitFunc init_func);
+ void (*protocol_plugin_set_display)(RemminaProtocolWidget *gp, gint display);
+ void (*protocol_plugin_signal_connection_closed)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_signal_connection_opened)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_update_align)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_lock_dynres)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_unlock_dynres)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_desktop_resize)(RemminaProtocolWidget *gp);
+ gint (*protocol_plugin_init_auth)(RemminaProtocolWidget *gp, RemminaMessagePanelFlags pflags, const gchar *title, const gchar *default_username, const gchar *default_password, const gchar *default_domain, const gchar *password_prompt);
+ gint (*protocol_plugin_init_certificate)(RemminaProtocolWidget *gp, const gchar *subject, const gchar *issuer, const gchar *fingerprint);
+ gint (*protocol_plugin_changed_certificate)(RemminaProtocolWidget *gp, const gchar *subject, const gchar *issuer, const gchar *new_fingerprint, const gchar *old_fingerprint);
+ gchar * (*protocol_plugin_init_get_username)(RemminaProtocolWidget * gp);
+ gchar * (*protocol_plugin_init_get_password)(RemminaProtocolWidget * gp);
+ gchar * (*protocol_plugin_init_get_domain)(RemminaProtocolWidget * gp);
+ gboolean (*protocol_plugin_init_get_savepassword)(RemminaProtocolWidget *gp);
+ gint (*protocol_plugin_init_authx509)(RemminaProtocolWidget *gp);
+ gchar * (*protocol_plugin_init_get_cacert)(RemminaProtocolWidget * gp);
+ gchar * (*protocol_plugin_init_get_cacrl)(RemminaProtocolWidget * gp);
+ gchar * (*protocol_plugin_init_get_clientcert)(RemminaProtocolWidget * gp);
+ gchar * (*protocol_plugin_init_get_clientkey)(RemminaProtocolWidget * gp);
+ void (*protocol_plugin_init_save_cred)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_init_show_listen)(RemminaProtocolWidget *gp, gint port);
+ void (*protocol_plugin_init_show_retry)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_init_show)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_init_hide)(RemminaProtocolWidget *gp);
+ gboolean (*protocol_plugin_ssh_exec)(RemminaProtocolWidget *gp, gboolean wait, const gchar *fmt, ...);
+ void (*protocol_plugin_chat_open)(RemminaProtocolWidget *gp, const gchar *name, void (*on_send)(RemminaProtocolWidget *gp, const gchar *text), void (*on_destroy)(RemminaProtocolWidget *gp));
+ void (*protocol_plugin_chat_close)(RemminaProtocolWidget *gp);
+ void (*protocol_plugin_chat_receive)(RemminaProtocolWidget *gp, const gchar *text);
+ void (*protocol_plugin_send_keys_signals)(GtkWidget *widget, const guint *keyvals, int length, GdkEventType action);
+
+ gchar * (*file_get_user_datadir)(void);
+
+ RemminaFile * (*file_new)(void);
+ const gchar * (*file_get_path)(RemminaFile * remminafile);
+ void (*file_set_string)(RemminaFile *remminafile, const gchar *setting, const gchar *value);
+ const gchar * (*file_get_string)(RemminaFile * remminafile, const gchar *setting);
+ gchar * (*file_get_secret)(RemminaFile * remminafile, const gchar *setting);
+ void (*file_set_int)(RemminaFile *remminafile, const gchar *setting, gint value);
+ gint (*file_get_int)(RemminaFile *remminafile, const gchar *setting, gint default_value);
+ gdouble (*file_get_double)(RemminaFile *remminafile, const gchar *setting, gdouble default_value);
+ void (*file_unsave_passwords)(RemminaFile *remminafile);
+
+ void (*pref_set_value)(const gchar *key, const gchar *value);
+ gchar * (*pref_get_value)(const gchar * key);
+ gint (*pref_get_scale_quality)(void);
+ gint (*pref_get_sshtunnel_port)(void);
+ gint (*pref_get_ssh_loglevel)(void);
+ gboolean (*pref_get_ssh_parseconfig)(void);
+ guint *(*pref_keymap_get_table)(const gchar *keymap);
+ guint (*pref_keymap_get_keyval)(const gchar *keymap, guint keyval);
+
+ void (*_remmina_info)(const gchar *fmt, ...);
+ void (*_remmina_message)(const gchar *fmt, ...);
+ void (*_remmina_debug)(const gchar *func, const gchar *fmt, ...);
+ void (*_remmina_warning)(const gchar *func, const gchar *fmt, ...);
+ void (*_remmina_audit)(const gchar *func, const gchar *fmt, ...);
+ void (*_remmina_error)(const gchar *func, const gchar *fmt, ...);
+ void (*_remmina_critical)(const gchar *func, const gchar *fmt, ...);
+ void (*log_print)(const gchar *text);
+ void (*log_printf)(const gchar *fmt, ...);
+
+ void (*ui_register)(GtkWidget *widget);
+
+ GtkWidget * (*open_connection)(RemminaFile * remminafile, GCallback disconnect_cb, gpointer data, guint *handler);
+ gint (*open_unix_sock)(const char *unixsock);
+ void (*get_server_port)(const gchar *server, gint defaultport, gchar **host, gint *port);
+ gboolean (*is_main_thread)(void);
+ gboolean (*gtksocket_available)(void);
+ gint (*get_profile_remote_width)(RemminaProtocolWidget *gp);
+ gint (*get_profile_remote_height)(RemminaProtocolWidget *gp);
+ const gchar*(*protocol_widget_get_name)(RemminaProtocolWidget *gp);
+ gint(*protocol_widget_get_width)(RemminaProtocolWidget *gp);
+ gint(*protocol_widget_get_height)(RemminaProtocolWidget *gp);
+ void(*protocol_widget_set_width)(RemminaProtocolWidget *gp, gint width);
+ void(*protocol_widget_set_height)(RemminaProtocolWidget *gp, gint height);
+ RemminaScaleMode(*protocol_widget_get_current_scale_mode)(RemminaProtocolWidget *gp);
+ gboolean (*protocol_widget_get_expand)(RemminaProtocolWidget *gp);
+ void (*protocol_widget_set_expand)(RemminaProtocolWidget *gp, gboolean expand);
+ void (*protocol_widget_set_error)(RemminaProtocolWidget *gp, const gchar *fmt, ...);
+ gboolean (*protocol_widget_has_error)(RemminaProtocolWidget *gp);
+ GtkWidget *(*protocol_widget_gtkviewport)(RemminaProtocolWidget *gp);
+ gboolean (*protocol_widget_is_closed)(RemminaProtocolWidget *gp);
+ RemminaFile *(*protocol_widget_get_file)(RemminaProtocolWidget *gp);
+ gint (*protocol_widget_panel_auth)(RemminaProtocolWidget *gp, RemminaMessagePanelFlags pflags,
+ const gchar *title, const gchar *default_username, const gchar *default_password, const gchar *default_domain, const gchar *password_prompt);
+ void (*protocol_widget_register_hostkey)(RemminaProtocolWidget *gp, GtkWidget *widget);
+ gchar *(*protocol_widget_start_direct_tunnel)(RemminaProtocolWidget *gp, gint default_port, gboolean port_plus);
+ gboolean (*protocol_widget_start_reverse_tunnel)(RemminaProtocolWidget *gp, gint local_port);
+ void (*protocol_widget_send_keys_signals)(GtkWidget *widget, const guint *keyvals, int keyvals_length, GdkEventType action);
+ void (*protocol_widget_chat_receive)(RemminaProtocolWidget *gp, const gchar *text);
+ void (*protocol_widget_panel_hide)(RemminaProtocolWidget *gp);
+ void (*protocol_widget_chat_open)(RemminaProtocolWidget *gp, const gchar *name,
+ void (*on_send)(RemminaProtocolWidget *gp, const gchar *text), void (*on_destroy)(RemminaProtocolWidget *gp));
+ gboolean (*protocol_widget_ssh_exec)(RemminaProtocolWidget *gp, gboolean wait, const gchar *fmt, ...);
+ void (*protocol_widget_panel_show)(RemminaProtocolWidget *gp);
+ void (*protocol_widget_panel_show_retry)(RemminaProtocolWidget *gp);
+ gboolean (*protocol_widget_start_xport_tunnel)(RemminaProtocolWidget *gp, RemminaXPortTunnelInitFunc init_func);
+ void (*protocol_widget_set_display)(RemminaProtocolWidget *gp, gint display);
+ void (*protocol_widget_signal_connection_closed)(RemminaProtocolWidget *gp);
+ void (*protocol_widget_signal_connection_opened)(RemminaProtocolWidget *gp);
+ void (*protocol_widget_update_align)(RemminaProtocolWidget *gp);
+ void (*protocol_widget_unlock_dynres)(RemminaProtocolWidget *gp);
+ void (*protocol_widget_desktop_resize)(RemminaProtocolWidget *gp);
+ gint (*protocol_widget_panel_new_certificate)(RemminaProtocolWidget *gp, const gchar *subject, const gchar *issuer, const gchar *fingerprint);
+ gint (*protocol_widget_panel_changed_certificate)(RemminaProtocolWidget *gp, const gchar *subject, const gchar *issuer, const gchar *new_fingerprint, const gchar *old_fingerprint);
+ gchar *(*protocol_widget_get_username)(RemminaProtocolWidget *gp);
+ gchar *(*protocol_widget_get_password)(RemminaProtocolWidget *gp);
+ gchar *(*protocol_widget_get_domain)(RemminaProtocolWidget *gp);
+ gboolean (*protocol_widget_get_savepassword)(RemminaProtocolWidget *gp);
+ gint (*protocol_widget_panel_authx509)(RemminaProtocolWidget *gp);
+ gchar *(*protocol_widget_get_cacert)(RemminaProtocolWidget *gp);
+ gchar *(*protocol_widget_get_cacrl)(RemminaProtocolWidget *gp);
+ gchar *(*protocol_widget_get_clientcert)(RemminaProtocolWidget *gp);
+ gchar *(*protocol_widget_get_clientkey)(RemminaProtocolWidget *gp);
+ void (*protocol_widget_save_cred)(RemminaProtocolWidget *gp);
+ void (*protocol_widget_panel_show_listen)(RemminaProtocolWidget *gp, gint port);
+ void (*widget_pool_register)(GtkWidget *widget);
+ GtkWidget *(*rcw_open_from_file_full)(RemminaFile *remminafile, GCallback disconnect_cb, gpointer data, guint *handler);
+ void (*show_dialog)(GtkMessageType msg, GtkButtonsType buttons, const gchar* message);
+ GtkWindow *(*get_window)(void);
+ gint (*plugin_unlock_new)(GtkWindow* parent);
+} RemminaPluginService;
+
+/* "Prototype" of the plugin entry function */
+typedef gboolean (*RemminaPluginEntryFunc) (RemminaPluginService *service);
+
+G_END_DECLS
diff --git a/src/include/remmina/remmina_trace_calls.h b/src/include/remmina/remmina_trace_calls.h
new file mode 100644
index 0000000..1539927
--- /dev/null
+++ b/src/include/remmina/remmina_trace_calls.h
@@ -0,0 +1,51 @@
+/*
+ * Remmina - 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.
+ *
+ */
+
+#pragma once
+
+#ifdef WITH_TRACE_CALLS
+#include <gtk/gtk.h>
+
+#define TRACE_CALL(text) \
+ { \
+ GDateTime *datetime = g_date_time_new_now_local(); \
+ gchar *sfmtdate = g_date_time_format(datetime, "%x %X"); \
+ g_print("%s Trace calls: %s\n", sfmtdate, text); \
+ g_free(sfmtdate); \
+ g_date_time_unref(datetime); \
+ }
+#else
+#define TRACE_CALL(text)
+#endif /* _WITH_TRACE_CALLS_ */
diff --git a/src/include/remmina/types.h b/src/include/remmina/types.h
new file mode 100644
index 0000000..43d7ec2
--- /dev/null
+++ b/src/include/remmina/types.h
@@ -0,0 +1,164 @@
+/*
+ * 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 <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef struct _RemminaFile RemminaFile;
+
+typedef enum {
+ REMMINA_PROTOCOL_FEATURE_TYPE_END,
+ REMMINA_PROTOCOL_FEATURE_TYPE_PREF,
+ REMMINA_PROTOCOL_FEATURE_TYPE_TOOL,
+ REMMINA_PROTOCOL_FEATURE_TYPE_UNFOCUS,
+ REMMINA_PROTOCOL_FEATURE_TYPE_SCALE,
+ REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE,
+ REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON,
+ REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET
+} RemminaProtocolFeatureType;
+
+#define REMMINA_PROTOCOL_FEATURE_PREF_RADIO 1
+#define REMMINA_PROTOCOL_FEATURE_PREF_CHECK 2
+
+typedef enum
+{
+ REMMINA_TYPEHINT_STRING,
+ REMMINA_TYPEHINT_SIGNED,
+ REMMINA_TYPEHINT_UNSIGNED,
+ REMMINA_TYPEHINT_BOOLEAN,
+ REMMINA_TYPEHINT_CPOINTER,
+ REMMINA_TYPEHINT_RAW,
+ REMMINA_TYPEHINT_TUPLE,
+ REMMINA_TYPEHINT_UNDEFINED,
+} RemminaTypeHint;
+
+typedef struct _RemminaProtocolFeature {
+ RemminaProtocolFeatureType type;
+ gint id;
+ gpointer opt1;
+ gpointer opt2;
+ gpointer opt3;
+ RemminaTypeHint opt1_type_hint;
+ RemminaTypeHint opt2_type_hint;
+ RemminaTypeHint opt3_type_hint;
+} RemminaProtocolFeature;
+
+typedef struct _RemminaPluginScreenshotData {
+ unsigned char * buffer;
+ int bitsPerPixel;
+ int bytesPerPixel;
+ int width;
+ int height;
+} RemminaPluginScreenshotData;
+
+
+typedef struct _RemminaProtocolWidgetClass RemminaProtocolWidgetClass;
+typedef struct _RemminaProtocolWidget RemminaProtocolWidget;
+typedef gpointer RemminaTunnelInitFunc;
+typedef gboolean (*RemminaXPortTunnelInitFunc) (RemminaProtocolWidget *gp, gint remotedisplay, const gchar *server, gint port);
+
+typedef enum {
+ REMMINA_PROTOCOL_SETTING_TYPE_END,
+
+ REMMINA_PROTOCOL_SETTING_TYPE_SERVER,
+ REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD,
+ REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION,
+ REMMINA_PROTOCOL_SETTING_TYPE_ASSISTANCE,
+ REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP,
+
+ REMMINA_PROTOCOL_SETTING_TYPE_TEXT,
+ REMMINA_PROTOCOL_SETTING_TYPE_TEXTAREA,
+ REMMINA_PROTOCOL_SETTING_TYPE_SELECT,
+ REMMINA_PROTOCOL_SETTING_TYPE_COMBO,
+ REMMINA_PROTOCOL_SETTING_TYPE_CHECK,
+ REMMINA_PROTOCOL_SETTING_TYPE_FILE,
+ REMMINA_PROTOCOL_SETTING_TYPE_FOLDER,
+ REMMINA_PROTOCOL_SETTING_TYPE_INT,
+ REMMINA_PROTOCOL_SETTING_TYPE_DOUBLE
+} RemminaProtocolSettingType;
+
+typedef struct _RemminaProtocolSetting {
+ RemminaProtocolSettingType type;
+ const gchar * name;
+ const gchar * label;
+ gboolean compact;
+ gpointer opt1;
+ gpointer opt2;
+ gpointer validator_data;
+ GCallback validator;
+} RemminaProtocolSetting;
+
+typedef enum {
+ REMMINA_PROTOCOL_SSH_SETTING_NONE,
+ REMMINA_PROTOCOL_SSH_SETTING_TUNNEL,
+ REMMINA_PROTOCOL_SSH_SETTING_SSH,
+ REMMINA_PROTOCOL_SSH_SETTING_REVERSE_TUNNEL,
+ REMMINA_PROTOCOL_SSH_SETTING_SFTP
+} RemminaProtocolSSHSetting;
+
+typedef enum {
+ REMMINA_AUTHPWD_TYPE_PROTOCOL,
+ REMMINA_AUTHPWD_TYPE_SSH_PWD,
+ REMMINA_AUTHPWD_TYPE_SSH_PRIVKEY
+} RemminaAuthpwdType;
+
+typedef enum {
+ REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE = 0,
+ REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED = 1,
+ REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES = 2
+} RemminaScaleMode;
+
+typedef enum {
+ RES_INVALID = -1,
+ RES_USE_CUSTOM = 0,
+ RES_USE_CLIENT = 1,
+ RES_USE_INITIAL_WINDOW_SIZE = 2
+} RemminaProtocolWidgetResolutionMode;
+
+/* pflags field for remmina_protocol_widget_panel_auth() */
+typedef enum {
+ REMMINA_MESSAGE_PANEL_FLAG_USERNAME = 1, /* require username in auth panel */
+ REMMINA_MESSAGE_PANEL_FLAG_USERNAME_READONLY = 2, /* Username, if required, is readonly */
+ REMMINA_MESSAGE_PANEL_FLAG_DOMAIN = 4, /* require domain in auth panel */
+ REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD = 8 /* require savepassword switch in auth panel */
+
+} RemminaMessagePanelFlags;
+
+G_END_DECLS
diff --git a/src/pygobject.h b/src/pygobject.h
new file mode 100644
index 0000000..145d1c6
--- /dev/null
+++ b/src/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/src/rcw.c b/src/rcw.c
new file mode 100644
index 0000000..8d00965
--- /dev/null
+++ b/src/rcw.c
@@ -0,0 +1,4812 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 "config.h"
+
+#ifdef GDK_WINDOWING_X11
+#include <cairo/cairo-xlib.h>
+#else
+#include <cairo/cairo.h>
+#endif
+#include <gdk/gdk.h>
+#include <gdk/gdkkeysyms.h>
+#include <glib/gi18n.h>
+#include <stdlib.h>
+
+#include "remmina.h"
+#include "remmina_main.h"
+#include "rcw.h"
+#include "remmina_applet_menu_item.h"
+#include "remmina_applet_menu.h"
+#include "remmina_file.h"
+#include "remmina_file_manager.h"
+#include "remmina_log.h"
+#include "remmina_message_panel.h"
+#include "remmina_ext_exec.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_pref.h"
+#include "remmina_protocol_widget.h"
+#include "remmina_public.h"
+#include "remmina_scrolled_viewport.h"
+#include "remmina_unlock.h"
+#include "remmina_utils.h"
+#include "remmina_widget_pool.h"
+#include "remmina/remmina_trace_calls.h"
+
+#ifdef GDK_WINDOWING_WAYLAND
+#include <gdk/gdkwayland.h>
+#endif
+
+
+#define DEBUG_KB_GRABBING 0
+#include "remmina_exec.h"
+
+gchar *remmina_pref_file;
+RemminaPref remmina_pref;
+
+G_DEFINE_TYPE(RemminaConnectionWindow, rcw, GTK_TYPE_WINDOW)
+
+#define MOTION_TIME 100
+
+/* default timeout used to hide the floating toolbar when switching profile */
+#define TB_HIDE_TIME_TIME 1500
+
+#define FULL_SCREEN_TARGET_MONITOR_UNDEFINED -1
+
+struct _RemminaConnectionWindowPriv {
+ GtkNotebook * notebook;
+ GtkWidget * floating_toolbar_widget;
+ GtkWidget * overlay;
+ GtkWidget * revealer;
+ GtkWidget * overlay_ftb_overlay;
+ GtkWidget * overlay_ftb_fr;
+
+ GtkWidget * floating_toolbar_label;
+ gdouble floating_toolbar_opacity;
+
+ /* Various delayed and timer event source ids */
+ guint acs_eventsourceid; // timeout
+ guint spf_eventsourceid; // idle
+ guint grab_retry_eventsourceid; // timeout
+ guint delayed_grab_eventsourceid;
+ guint ftb_hide_eventsource; // timeout
+ guint tar_eventsource; // timeout
+ guint hidetb_eventsource; // timeout
+ guint dwp_eventsourceid; // timeout
+
+ GtkWidget * toolbar;
+ GtkWidget * grid;
+
+ /* Toolitems that need to be handled */
+ GtkToolItem * toolitem_menu;
+ GtkToolItem * toolitem_autofit;
+ GtkToolItem * toolitem_fullscreen;
+ GtkToolItem * toolitem_switch_page;
+ GtkToolItem * toolitem_dynres;
+ GtkToolItem * toolitem_scale;
+ GtkToolItem * toolitem_grab;
+ GtkToolItem * toolitem_multimon;
+ GtkToolItem * toolitem_preferences;
+ GtkToolItem * toolitem_tools;
+ GtkToolItem * toolitem_new;
+ GtkToolItem * toolitem_duplicate;
+ GtkToolItem * toolitem_screenshot;
+ GtkWidget * fullscreen_option_button;
+ GtkWidget * fullscreen_scaler_button;
+ GtkWidget * scaler_option_button;
+
+ GtkWidget * pin_button;
+ gboolean pin_down;
+
+ gboolean sticky;
+
+ /* Flag to turn off toolbar signal handling when toolbar is
+ * reconfiguring, usually due to a tab switch */
+ gboolean toolbar_is_reconfiguring;
+
+ /* This is the current view mode, i.e. VIEWPORT_FULLSCREEN_MODE,
+ * as saved on the "viwemode" profile preference file */
+ gint view_mode;
+
+ /* Status variables used when in fullscreen mode. Needed
+ * to restore a fullscreen mode after coming from scrolled */
+ gint fss_view_mode;
+ /* Status variables used when in scrolled window mode. Needed
+ * to restore a scrolled window mode after coming from fullscreen */
+ gint ss_width, ss_height;
+ gboolean ss_maximized;
+
+ gboolean kbcaptured;
+ gboolean pointer_captured;
+ gboolean hostkey_activated;
+ gboolean hostkey_used;
+
+ gboolean pointer_entered;
+
+ RemminaConnectionWindowOnDeleteConfirmMode on_delete_confirm_mode;
+};
+
+typedef struct _RemminaConnectionObject {
+ RemminaConnectionWindow * cnnwin;
+ RemminaFile * remmina_file;
+
+ GtkWidget * proto;
+ GtkWidget * aspectframe;
+ GtkWidget * viewport;
+
+ GtkWidget * scrolled_container;
+
+ gboolean plugin_can_scale;
+
+ gboolean connected;
+ gboolean dynres_unlocked;
+
+ gulong deferred_open_size_allocate_handler;
+} RemminaConnectionObject;
+
+enum {
+ TOOLBARPLACE_SIGNAL,
+ LAST_SIGNAL
+};
+
+static guint rcw_signals[LAST_SIGNAL] =
+{ 0 };
+
+static RemminaConnectionWindow *rcw_create_scrolled(gint width, gint height, gboolean maximize);
+static RemminaConnectionWindow *rcw_create_fullscreen(GtkWindow *old, gint view_mode);
+static gboolean rcw_hostkey_func(RemminaProtocolWidget *gp, guint keyval, gboolean release);
+static GtkWidget *rco_create_tab_page(RemminaConnectionObject *cnnobj);
+static GtkWidget *rco_create_tab_label(RemminaConnectionObject *cnnobj);
+
+void rcw_grab_focus(RemminaConnectionWindow *cnnwin);
+static GtkWidget *rcw_create_toolbar(RemminaConnectionWindow *cnnwin, gint mode);
+static void rcw_place_toolbar(GtkToolbar *toolbar, GtkGrid *grid, GtkWidget *sibling, int toolbar_placement);
+static void rco_update_toolbar(RemminaConnectionObject *cnnobj);
+static void rcw_keyboard_grab(RemminaConnectionWindow *cnnwin);
+static GtkWidget *rcw_append_new_page(RemminaConnectionWindow *cnnwin, RemminaConnectionObject *cnnobj);
+
+
+static void rcw_ftb_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data);
+
+static const GtkTargetEntry dnd_targets_ftb[] =
+{
+ {
+ (char *)"text/x-remmina-ftb",
+ GTK_TARGET_SAME_APP | GTK_TARGET_OTHER_WIDGET,
+ 0
+ },
+};
+
+static const GtkTargetEntry dnd_targets_tb[] =
+{
+ {
+ (char *)"text/x-remmina-tb",
+ GTK_TARGET_SAME_APP,
+ 0
+ },
+};
+
+static void rcw_class_init(RemminaConnectionWindowClass *klass)
+{
+ TRACE_CALL(__func__);
+ GtkCssProvider *provider;
+
+ provider = gtk_css_provider_new();
+
+ /* It’s important to remove padding, border and shadow from GtkViewport or
+ * we will never know its internal area size, because GtkViweport::viewport_get_view_allocation,
+ * which returns the internal size of the GtkViewport, is private and we cannot access it */
+
+#if GTK_CHECK_VERSION(3, 14, 0)
+ gtk_css_provider_load_from_data(provider,
+ "#remmina-cw-viewport, #remmina-cw-aspectframe {\n"
+ " padding:0;\n"
+ " border:0;\n"
+ " background-color: black;\n"
+ "}\n"
+ "GtkDrawingArea {\n"
+ "}\n"
+ "GtkToolbar {\n"
+ " -GtkWidget-window-dragging: 0;\n"
+ "}\n"
+ "#remmina-connection-window-fullscreen {\n"
+ " border-color: black;\n"
+ "}\n"
+ "#remmina-small-button {\n"
+ " outline-offset: 0;\n"
+ " outline-width: 0;\n"
+ " padding: 0;\n"
+ " border: 0;\n"
+ "}\n"
+ "#remmina-pin-button {\n"
+ " outline-offset: 0;\n"
+ " outline-width: 0;\n"
+ " padding: 2px;\n"
+ " border: 0;\n"
+ "}\n"
+ "#remmina-tab-page {\n"
+ " background-color: black;\n"
+ "}\n"
+ "#remmina-scrolled-container {\n"
+ "}\n"
+ "#remmina-scrolled-container.undershoot {\n"
+ " background: none;\n"
+ "}\n"
+ "#remmina-tab-page {\n"
+ "}\n"
+ "#ftbbox-upper {\n"
+ " background-color: white;\n"
+ " color: black;\n"
+ " border-style: none solid solid solid;\n"
+ " border-width: 1px;\n"
+ " border-radius: 4px;\n"
+ " padding: 0px;\n"
+ "}\n"
+ "#ftbbox-lower {\n"
+ " background-color: white;\n"
+ " color: black;\n"
+ " border-style: solid solid none solid;\n"
+ " border-width: 1px;\n"
+ " border-radius: 4px;\n"
+ " padding: 0px;\n"
+ "}\n"
+ "#ftb-handle {\n"
+ "}\n"
+ ".message_panel {\n"
+ " border: 0px solid;\n"
+ " padding: 20px 20px 20px 20px;\n"
+ "}\n"
+ ".message_panel entry {\n"
+ " background-image: none;\n"
+ " border-width: 4px;\n"
+ " border-radius: 8px;\n"
+ "}\n"
+ ".message_panel .title_label {\n"
+ " font-size: 2em; \n"
+ "}\n"
+ , -1, NULL);
+
+#else
+ gtk_css_provider_load_from_data(provider,
+ "#remmina-cw-viewport, #remmina-cw-aspectframe {\n"
+ " padding:0;\n"
+ " border:0;\n"
+ " background-color: black;\n"
+ "}\n"
+ "#remmina-cw-message-panel {\n"
+ "}\n"
+ "GtkDrawingArea {\n"
+ "}\n"
+ "GtkToolbar {\n"
+ " -GtkWidget-window-dragging: 0;\n"
+ "}\n"
+ "#remmina-connection-window-fullscreen {\n"
+ " border-color: black;\n"
+ "}\n"
+ "#remmina-small-button {\n"
+ " -GtkWidget-focus-padding: 0;\n"
+ " -GtkWidget-focus-line-width: 0;\n"
+ " padding: 0;\n"
+ " border: 0;\n"
+ "}\n"
+ "#remmina-pin-button {\n"
+ " -GtkWidget-focus-padding: 0;\n"
+ " -GtkWidget-focus-line-width: 0;\n"
+ " padding: 2px;\n"
+ " border: 0;\n"
+ "}\n"
+ "#remmina-scrolled-container {\n"
+ "}\n"
+ "#remmina-scrolled-container.undershoot {\n"
+ " background: none\n"
+ "}\n"
+ "#remmina-tab-page {\n"
+ "}\n"
+ "#ftbbox-upper {\n"
+ " border-style: none solid solid solid;\n"
+ " border-width: 1px;\n"
+ " border-radius: 4px;\n"
+ " padding: 0px;\n"
+ "}\n"
+ "#ftbbox-lower {\n"
+ " border-style: solid solid none solid;\n"
+ " border-width: 1px;\n"
+ " border-radius: 4px;\n"
+ " padding: 0px;\n"
+ "}\n"
+ "#ftb-handle {\n"
+ "}\n"
+
+ , -1, NULL);
+#endif
+
+ gtk_style_context_add_provider_for_screen(gdk_screen_get_default(),
+ GTK_STYLE_PROVIDER(provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ g_object_unref(provider);
+
+ /* Define a signal used to notify all rcws of toolbar move */
+ rcw_signals[TOOLBARPLACE_SIGNAL] = g_signal_new("toolbar-place", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaConnectionWindowClass, toolbar_place), NULL, NULL,
+ g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+}
+
+static RemminaConnectionObject *rcw_get_cnnobj_at_page(RemminaConnectionWindow *cnnwin, gint npage)
+{
+ GtkWidget *po;
+
+ if (!cnnwin->priv->notebook)
+ return NULL;
+ po = gtk_notebook_get_nth_page(GTK_NOTEBOOK(cnnwin->priv->notebook), npage);
+ return g_object_get_data(G_OBJECT(po), "cnnobj");
+}
+
+static RemminaConnectionObject *rcw_get_visible_cnnobj(RemminaConnectionWindow *cnnwin)
+{
+ gint np;
+
+ if (cnnwin != NULL && cnnwin->priv != NULL && cnnwin->priv->notebook != NULL) {
+ np = gtk_notebook_get_current_page(GTK_NOTEBOOK(cnnwin->priv->notebook));
+ if (np < 0)
+ return NULL;
+ return rcw_get_cnnobj_at_page(cnnwin, np);
+ } else {
+ return NULL;
+ }
+}
+
+static RemminaScaleMode get_current_allowed_scale_mode(RemminaConnectionObject *cnnobj, gboolean *dynres_avail, gboolean *scale_avail)
+{
+ TRACE_CALL(__func__);
+ RemminaScaleMode scalemode;
+ gboolean plugin_has_dynres, plugin_can_scale;
+
+ scalemode = remmina_protocol_widget_get_current_scale_mode(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+
+ plugin_has_dynres = remmina_protocol_widget_query_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto),
+ REMMINA_PROTOCOL_FEATURE_TYPE_DYNRESUPDATE);
+
+ plugin_can_scale = remmina_protocol_widget_query_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto),
+ REMMINA_PROTOCOL_FEATURE_TYPE_SCALE);
+
+ /* Forbid scalemode REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES when not possible */
+ if ((!plugin_has_dynres) && scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES)
+ scalemode = REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE;
+
+ /* Forbid scalemode REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED when not possible */
+ if (!plugin_can_scale && scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED)
+ scalemode = REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE;
+
+ if (scale_avail)
+ *scale_avail = plugin_can_scale;
+ if (dynres_avail)
+ *dynres_avail = (plugin_has_dynres && cnnobj->dynres_unlocked);
+
+ return scalemode;
+}
+
+static void rco_disconnect_current_page(RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+
+ /* Disconnects the connection which is currently in view in the notebook */
+ remmina_protocol_widget_close_connection(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+}
+
+static void rcw_kp_ungrab(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ GdkDisplay *display;
+
+#if GTK_CHECK_VERSION(3, 20, 0)
+ GdkSeat *seat;
+#else
+ GdkDeviceManager *manager;
+ GdkDevice *keyboard = NULL;
+#endif
+
+ if (cnnwin->priv->grab_retry_eventsourceid) {
+ g_source_remove(cnnwin->priv->grab_retry_eventsourceid);
+ cnnwin->priv->grab_retry_eventsourceid = 0;
+ }
+ if (cnnwin->priv->delayed_grab_eventsourceid) {
+ g_source_remove(cnnwin->priv->delayed_grab_eventsourceid);
+ cnnwin->priv->delayed_grab_eventsourceid = 0;
+ }
+
+ display = gtk_widget_get_display(GTK_WIDGET(cnnwin));
+#if GTK_CHECK_VERSION(3, 20, 0)
+ seat = gdk_display_get_default_seat(display);
+ // keyboard = gdk_seat_get_pointer(seat);
+#else
+ manager = gdk_display_get_device_manager(display);
+ keyboard = gdk_device_manager_get_client_pointer(manager);
+#endif
+
+ if (!cnnwin->priv->kbcaptured && !cnnwin->priv->pointer_captured)
+ return;
+
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: --- ungrabbing\n");
+#endif
+
+
+
+#if GTK_CHECK_VERSION(3, 20, 0)
+ /* We can use gtk_seat_grab()/_ungrab() only after GTK 3.24 */
+ gdk_seat_ungrab(seat);
+#else
+ if (keyboard != NULL) {
+ if (gdk_device_get_source(keyboard) != GDK_SOURCE_KEYBOARD)
+ keyboard = gdk_device_get_associated_device(keyboard);
+ G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+ gdk_device_ungrab(keyboard, GDK_CURRENT_TIME);
+ G_GNUC_END_IGNORE_DEPRECATIONS
+ }
+#endif
+ cnnwin->priv->kbcaptured = FALSE;
+ cnnwin->priv->pointer_captured = FALSE;
+}
+
+static gboolean rcw_keyboard_grab_retry(gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)user_data;
+
+#if DEBUG_KB_GRABBING
+ printf("%s retry grab\n", __func__);
+#endif
+ rcw_keyboard_grab(cnnwin);
+ cnnwin->priv->grab_retry_eventsourceid = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void rcw_pointer_ungrab(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+#if GTK_CHECK_VERSION(3, 20, 0)
+ GdkSeat *seat;
+ GdkDisplay *display;
+ if (!cnnwin->priv->pointer_captured)
+ return;
+
+ display = gtk_widget_get_display(GTK_WIDGET(cnnwin));
+ seat = gdk_display_get_default_seat(display);
+ gdk_seat_ungrab(seat);
+#endif
+}
+
+static void rcw_pointer_grab(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ /* This function in Wayland is useless and generates a spurious leave-notify event.
+ * Should we remove it ? https://gitlab.gnome.org/GNOME/mutter/-/issues/2450#note_1588081 */
+#if GTK_CHECK_VERSION(3, 20, 0)
+ GdkSeat *seat;
+ GdkDisplay *display;
+ GdkGrabStatus ggs;
+
+
+ if (cnnwin->priv->pointer_captured) {
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: pointer_captured is true, it should not\n");
+#endif
+ return;
+ }
+
+ display = gtk_widget_get_display(GTK_WIDGET(cnnwin));
+ seat = gdk_display_get_default_seat(display);
+ ggs = gdk_seat_grab(seat, gtk_widget_get_window(GTK_WIDGET(cnnwin)),
+ GDK_SEAT_CAPABILITY_ALL_POINTING, TRUE, NULL, NULL, NULL, NULL);
+ if (ggs != GDK_GRAB_SUCCESS) {
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: GRAB of POINTER failed. GdkGrabStatus: %d\n", (int)ggs);
+#endif
+ } else {
+ cnnwin->priv->pointer_captured = TRUE;
+ }
+
+#endif
+}
+
+static void rcw_keyboard_grab(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ GdkDisplay *display;
+
+#if GTK_CHECK_VERSION(3, 20, 0)
+ GdkSeat *seat;
+#else
+ GdkDeviceManager *manager;
+#endif
+ GdkGrabStatus ggs;
+ GdkDevice *keyboard = NULL;
+
+ if (cnnwin->priv->kbcaptured) {
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: %s not grabbing because already grabbed.\n", __func__);
+#endif
+ return;
+ }
+
+ display = gtk_widget_get_display(GTK_WIDGET(cnnwin));
+#if GTK_CHECK_VERSION(3, 20, 0)
+ seat = gdk_display_get_default_seat(display);
+ keyboard = gdk_seat_get_pointer(seat);
+#else
+ manager = gdk_display_get_device_manager(display);
+ keyboard = gdk_device_manager_get_client_pointer(manager);
+#endif
+
+ if (keyboard != NULL) {
+ if (gdk_device_get_source(keyboard) != GDK_SOURCE_KEYBOARD)
+ keyboard = gdk_device_get_associated_device(keyboard);
+
+
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: profile asks for grabbing, let’s try.\n");
+#endif
+ /* Up to GTK version 3.20 we can grab the keyboard with gdk_device_grab().
+ * in GTK 3.20 gdk_seat_grab() should be used instead of gdk_device_grab().
+ * There is a bug in GTK up to 3.22: When gdk_device_grab() fails
+ * the widget is hidden:
+ * https://gitlab.gnome.org/GNOME/gtk/commit/726ad5a5ae7c4f167e8dd454cd7c250821c400ab
+ * The bugfix will be released with GTK 3.24.
+ * Also please note that the newer gdk_seat_grab() is still calling gdk_device_grab().
+ *
+ * Warning: gdk_seat_grab() will call XGrabKeyboard() or XIGrabDevice()
+ * which in turn will generate a core X input event FocusOut and FocusIn
+ * but not Xinput2 events.
+ * In some cases, GTK is unable to neutralize FocusIn and FocusOut core
+ * events (ie: i3wm+Plasma with GDK_CORE_DEVICE_EVENTS=1 because detail=NotifyNonlinear
+ * instead of detail=NotifyAncestor/detail=NotifyInferior)
+ * Receiving a FocusOut event for Remmina at this time will cause an infinite loop.
+ * Therefore is important for GTK to use Xinput2 instead of core X events
+ * by unsetting GDK_CORE_DEVICE_EVENTS
+ */
+#if GTK_CHECK_VERSION(3, 20, 0)
+ ggs = gdk_seat_grab(seat, gtk_widget_get_window(GTK_WIDGET(cnnwin)),
+ GDK_SEAT_CAPABILITY_KEYBOARD, TRUE, NULL, NULL, NULL, NULL);
+#else
+ ggs = gdk_device_grab(keyboard, gtk_widget_get_window(GTK_WIDGET(cnnwin)), GDK_OWNERSHIP_WINDOW,
+ TRUE, GDK_KEY_PRESS | GDK_KEY_RELEASE, NULL, GDK_CURRENT_TIME);
+#endif
+ if (ggs != GDK_GRAB_SUCCESS) {
+#if DEBUG_KB_GRABBING
+ printf("GRAB of keyboard failed.\n");
+#endif
+ /* Reschedule grabbing in half a second if not already done */
+ if (cnnwin->priv->grab_retry_eventsourceid == 0)
+ cnnwin->priv->grab_retry_eventsourceid = g_timeout_add(500, (GSourceFunc)rcw_keyboard_grab_retry, cnnwin);
+ } else {
+#if DEBUG_KB_GRABBING
+ printf("Keyboard grabbed\n");
+#endif
+ if (cnnwin->priv->grab_retry_eventsourceid != 0) {
+ g_source_remove(cnnwin->priv->grab_retry_eventsourceid);
+ cnnwin->priv->grab_retry_eventsourceid = 0;
+ }
+ cnnwin->priv->kbcaptured = TRUE;
+ }
+ } else {
+ rcw_kp_ungrab(cnnwin);
+ }
+}
+
+static void rcw_close_all_connections(RemminaConnectionWindow *cnnwin)
+{
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+ GtkNotebook *notebook = GTK_NOTEBOOK(priv->notebook);
+ GtkWidget *w;
+ RemminaConnectionObject *cnnobj;
+ gint i, n;
+
+ if (GTK_IS_WIDGET(notebook)) {
+ n = gtk_notebook_get_n_pages(notebook);
+ for (i = n - 1; i >= 0; i--) {
+ w = gtk_notebook_get_nth_page(notebook, i);
+ cnnobj = (RemminaConnectionObject *)g_object_get_data(G_OBJECT(w), "cnnobj");
+ /* Do close the connection on this tab */
+ remmina_protocol_widget_close_connection(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+ }
+ }
+}
+
+gboolean rcw_delete(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+ GtkNotebook *notebook = GTK_NOTEBOOK(priv->notebook);
+ GtkWidget *dialog;
+ gint i, n, nopen;
+
+ if (!REMMINA_IS_CONNECTION_WINDOW(cnnwin))
+ return TRUE;
+
+ if (cnnwin->priv->on_delete_confirm_mode != RCW_ONDELETE_NOCONFIRM) {
+ n = gtk_notebook_get_n_pages(notebook);
+ nopen = 0;
+ /* count all non-closed connections */
+ for(i = 0; i < n; i ++) {
+ RemminaConnectionObject *cnnobj = rcw_get_cnnobj_at_page(cnnwin, i);
+ if (!remmina_protocol_widget_is_closed((RemminaProtocolWidget *)cnnobj->proto))
+ nopen ++;
+ }
+ if (nopen > 1) {
+ dialog = gtk_message_dialog_new(GTK_WINDOW(cnnwin), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_YES_NO,
+ _("Are you sure you want to close %i active connections in the current window?"), nopen);
+ i = gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ if (i != GTK_RESPONSE_YES)
+ return FALSE;
+ }
+ else if (nopen == 1) {
+ if (remmina_pref.confirm_close) {
+ dialog = gtk_message_dialog_new(GTK_WINDOW(cnnwin), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_YES_NO,
+ _("Are you sure you want to close this last active connection?"));
+ i = gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ if (i != GTK_RESPONSE_YES)
+ return FALSE;
+ }
+ }
+ }
+ rcw_close_all_connections(cnnwin);
+
+ return TRUE;
+}
+
+static gboolean rcw_delete_event(GtkWidget *widget, GdkEvent *event, gpointer data)
+{
+ TRACE_CALL(__func__);
+ rcw_delete(RCW(widget));
+ return TRUE;
+}
+
+static void rcw_destroy(GtkWidget *widget, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv;
+ RemminaConnectionWindow *cnnwin;
+
+ if (!REMMINA_IS_CONNECTION_WINDOW(widget))
+ return;
+
+ cnnwin = (RemminaConnectionWindow *)widget;
+ priv = cnnwin->priv;
+
+ if (priv->kbcaptured)
+ rcw_kp_ungrab(cnnwin);
+
+ if (priv->acs_eventsourceid) {
+ g_source_remove(priv->acs_eventsourceid);
+ priv->acs_eventsourceid = 0;
+ }
+ if (priv->spf_eventsourceid) {
+ g_source_remove(priv->spf_eventsourceid);
+ priv->spf_eventsourceid = 0;
+ }
+ if (priv->grab_retry_eventsourceid) {
+ g_source_remove(priv->grab_retry_eventsourceid);
+ priv->grab_retry_eventsourceid = 0;
+ }
+ if (cnnwin->priv->delayed_grab_eventsourceid) {
+ g_source_remove(cnnwin->priv->delayed_grab_eventsourceid);
+ cnnwin->priv->delayed_grab_eventsourceid = 0;
+ }
+ if (priv->ftb_hide_eventsource) {
+ g_source_remove(priv->ftb_hide_eventsource);
+ priv->ftb_hide_eventsource = 0;
+ }
+ if (priv->tar_eventsource) {
+ g_source_remove(priv->tar_eventsource);
+ priv->tar_eventsource = 0;
+ }
+ if (priv->hidetb_eventsource) {
+ g_source_remove(priv->hidetb_eventsource);
+ priv->hidetb_eventsource = 0;
+ }
+ if (priv->dwp_eventsourceid) {
+ g_source_remove(priv->dwp_eventsourceid);
+ priv->dwp_eventsourceid = 0;
+ }
+
+ /* There is no need to destroy priv->floating_toolbar_widget,
+ * because it’s our child and will be destroyed automatically */
+
+ cnnwin->priv = NULL;
+ g_free(priv);
+}
+
+gboolean rcw_notify_widget_toolbar_placement(GtkWidget *widget, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GType rcwtype;
+
+ rcwtype = rcw_get_type();
+ if (G_TYPE_CHECK_INSTANCE_TYPE(widget, rcwtype)) {
+ g_signal_emit_by_name(G_OBJECT(widget), "toolbar-place");
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gboolean rcw_tb_drag_failed(GtkWidget *widget, GdkDragContext *context,
+ GtkDragResult result, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv;
+ RemminaConnectionWindow *cnnwin;
+
+
+ cnnwin = (RemminaConnectionWindow *)user_data;
+ priv = cnnwin->priv;
+
+ if (priv->toolbar)
+ gtk_widget_show(GTK_WIDGET(priv->toolbar));
+
+ return TRUE;
+}
+
+static gboolean rcw_tb_drag_drop(GtkWidget *widget, GdkDragContext *context,
+ gint x, gint y, guint time, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkAllocation wa;
+ gint new_toolbar_placement;
+ RemminaConnectionWindowPriv *priv;
+ RemminaConnectionWindow *cnnwin;
+
+ cnnwin = (RemminaConnectionWindow *)user_data;
+ priv = cnnwin->priv;
+
+ gtk_widget_get_allocation(widget, &wa);
+
+ if (wa.width * y >= wa.height * x) {
+ if (wa.width * y > wa.height * (wa.width - x))
+ new_toolbar_placement = TOOLBAR_PLACEMENT_BOTTOM;
+ else
+ new_toolbar_placement = TOOLBAR_PLACEMENT_LEFT;
+ } else {
+ if (wa.width * y > wa.height * (wa.width - x))
+ new_toolbar_placement = TOOLBAR_PLACEMENT_RIGHT;
+ else
+ new_toolbar_placement = TOOLBAR_PLACEMENT_TOP;
+ }
+
+ gtk_drag_finish(context, TRUE, TRUE, time);
+
+ if (new_toolbar_placement != remmina_pref.toolbar_placement) {
+ /* Save new position */
+ remmina_pref.toolbar_placement = new_toolbar_placement;
+ remmina_pref_save();
+
+ /* Signal all windows that the toolbar must be moved */
+ remmina_widget_pool_foreach(rcw_notify_widget_toolbar_placement, NULL);
+ }
+ if (priv->toolbar)
+ gtk_widget_show(GTK_WIDGET(priv->toolbar));
+
+ return TRUE;
+}
+
+static void rcw_tb_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ cairo_surface_t *surface;
+ cairo_t *cr;
+ GtkAllocation wa;
+ double dashes[] = { 10 };
+
+ gtk_widget_get_allocation(widget, &wa);
+
+ surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 16, 16);
+ cr = cairo_create(surface);
+ cairo_set_source_rgb(cr, 0.6, 0.6, 0.6);
+ cairo_set_line_width(cr, 4);
+ cairo_set_dash(cr, dashes, 1, 0);
+ cairo_rectangle(cr, 0, 0, 16, 16);
+ cairo_stroke(cr);
+ cairo_destroy(cr);
+
+ gtk_widget_hide(widget);
+
+ gtk_drag_set_icon_surface(context, surface);
+}
+
+void rcw_update_toolbar_opacity(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+ RemminaConnectionObject *cnnobj;
+
+ cnnobj = rcw_get_visible_cnnobj(cnnwin);
+ if (!cnnobj) return;
+
+ priv->floating_toolbar_opacity = (1.0 - TOOLBAR_OPACITY_MIN) / ((gdouble)TOOLBAR_OPACITY_LEVEL)
+ * ((gdouble)(TOOLBAR_OPACITY_LEVEL - remmina_file_get_int(cnnobj->remmina_file, "toolbar_opacity", 0)))
+ + TOOLBAR_OPACITY_MIN;
+ if (priv->floating_toolbar_widget)
+ gtk_widget_set_opacity(GTK_WIDGET(priv->overlay_ftb_overlay), priv->floating_toolbar_opacity);
+}
+
+static gboolean rcw_floating_toolbar_make_invisible(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = (RemminaConnectionWindowPriv *)data;
+
+ gtk_widget_set_opacity(GTK_WIDGET(priv->overlay_ftb_overlay), 0.0);
+ priv->ftb_hide_eventsource = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void rcw_floating_toolbar_show(RemminaConnectionWindow *cnnwin, gboolean show)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+
+ if (priv->floating_toolbar_widget == NULL)
+ return;
+
+ if (show || priv->pin_down) {
+ /* Make the FTB no longer transparent, in case we have an hidden toolbar */
+ rcw_update_toolbar_opacity(cnnwin);
+ /* Remove outstanding hide events, if not yet active */
+ if (priv->ftb_hide_eventsource) {
+ g_source_remove(priv->ftb_hide_eventsource);
+ priv->ftb_hide_eventsource = 0;
+ }
+ } else {
+ /* If we are hiding and the toolbar must be made invisible, schedule
+ * a later toolbar hide */
+ if (remmina_pref.fullscreen_toolbar_visibility == FLOATING_TOOLBAR_VISIBILITY_INVISIBLE)
+ if (priv->ftb_hide_eventsource == 0)
+ priv->ftb_hide_eventsource = g_timeout_add(1000, rcw_floating_toolbar_make_invisible, priv);
+ }
+
+ gtk_revealer_set_reveal_child(GTK_REVEALER(priv->revealer), show || priv->pin_down);
+}
+
+static void rco_get_desktop_size(RemminaConnectionObject *cnnobj, gint *width, gint *height)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto);
+
+
+ *width = remmina_protocol_widget_get_width(gp);
+ *height = remmina_protocol_widget_get_height(gp);
+ if (*width == 0) {
+ /* Before connecting we do not have real remote width/height,
+ * so we ask profile values */
+ *width = remmina_protocol_widget_get_profile_remote_width(gp);
+ *height = remmina_protocol_widget_get_profile_remote_height(gp);
+ }
+}
+
+void rco_set_scrolled_policy(RemminaScaleMode scalemode, GtkScrolledWindow *scrolled_window)
+{
+ TRACE_CALL(__func__);
+
+ gtk_scrolled_window_set_policy(scrolled_window,
+ scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED ? GTK_POLICY_NEVER : GTK_POLICY_AUTOMATIC,
+ scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED ? GTK_POLICY_NEVER : GTK_POLICY_AUTOMATIC);
+}
+
+static GtkWidget *rco_create_scrolled_container(RemminaScaleMode scalemode, int view_mode)
+{
+ GtkWidget *scrolled_container;
+
+ if (view_mode == VIEWPORT_FULLSCREEN_MODE) {
+ scrolled_container = remmina_scrolled_viewport_new();
+ } else {
+ scrolled_container = gtk_scrolled_window_new(NULL, NULL);
+ rco_set_scrolled_policy(scalemode, GTK_SCROLLED_WINDOW(scrolled_container));
+ gtk_container_set_border_width(GTK_CONTAINER(scrolled_container), 0);
+ gtk_widget_set_can_focus(scrolled_container, FALSE);
+ }
+
+ gtk_widget_set_name(scrolled_container, "remmina-scrolled-container");
+ gtk_widget_show(scrolled_container);
+
+ return scrolled_container;
+}
+
+gboolean rcw_toolbar_autofit_restore(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+ RemminaConnectionObject *cnnobj;
+ gint dwidth, dheight;
+ GtkAllocation nba, ca, ta;
+
+ cnnwin->priv->tar_eventsource = 0;
+
+ if (priv->toolbar_is_reconfiguring)
+ return G_SOURCE_REMOVE;
+
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return FALSE;
+
+ if (cnnobj->connected && GTK_IS_SCROLLED_WINDOW(cnnobj->scrolled_container)) {
+ rco_get_desktop_size(cnnobj, &dwidth, &dheight);
+ gtk_widget_get_allocation(GTK_WIDGET(priv->notebook), &nba);
+ gtk_widget_get_allocation(cnnobj->scrolled_container, &ca);
+ gtk_widget_get_allocation(priv->toolbar, &ta);
+ if (remmina_pref.toolbar_placement == TOOLBAR_PLACEMENT_LEFT ||
+ remmina_pref.toolbar_placement == TOOLBAR_PLACEMENT_RIGHT)
+ gtk_window_resize(GTK_WINDOW(cnnobj->cnnwin), MAX(1, dwidth + ta.width + nba.width - ca.width),
+ MAX(1, dheight + nba.height - ca.height));
+ else
+ gtk_window_resize(GTK_WINDOW(cnnobj->cnnwin), MAX(1, dwidth + nba.width - ca.width),
+ MAX(1, dheight + ta.height + nba.height - ca.height));
+ gtk_container_check_resize(GTK_CONTAINER(cnnobj->cnnwin));
+ }
+ if (GTK_IS_SCROLLED_WINDOW(cnnobj->scrolled_container)) {
+ RemminaScaleMode scalemode = get_current_allowed_scale_mode(cnnobj, NULL, NULL);
+ rco_set_scrolled_policy(scalemode, GTK_SCROLLED_WINDOW(cnnobj->scrolled_container));
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+static void rcw_toolbar_autofit(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ if (GTK_IS_SCROLLED_WINDOW(cnnobj->scrolled_container)) {
+ if ((gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(cnnwin))) & GDK_WINDOW_STATE_MAXIMIZED) != 0)
+ gtk_window_unmaximize(GTK_WINDOW(cnnwin));
+
+ /* It’s tricky to make the toolbars disappear automatically, while keeping scrollable.
+ * Please tell me if you know a better way to do this */
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container), GTK_POLICY_NEVER, GTK_POLICY_NEVER);
+
+ cnnwin->priv->tar_eventsource = g_timeout_add(200, (GSourceFunc)rcw_toolbar_autofit_restore, cnnwin);
+ }
+}
+
+void rco_get_monitor_geometry(RemminaConnectionObject *cnnobj, GdkRectangle *sz)
+{
+ TRACE_CALL(__func__);
+
+ /* Fill sz with the monitor (or workarea) size and position
+ * of the monitor (or workarea) where cnnobj->cnnwin is located */
+
+ GdkRectangle monitor_geometry;
+
+ sz->x = sz->y = sz->width = sz->height = 0;
+
+ if (!cnnobj)
+ return;
+ if (!cnnobj->cnnwin)
+ return;
+ if (!gtk_widget_is_visible(GTK_WIDGET(cnnobj->cnnwin)))
+ return;
+
+#if GTK_CHECK_VERSION(3, 22, 0)
+ GdkDisplay *display;
+ GdkMonitor *monitor;
+ display = gtk_widget_get_display(GTK_WIDGET(cnnobj->cnnwin));
+ monitor = gdk_display_get_monitor_at_window(display, gtk_widget_get_window(GTK_WIDGET(cnnobj->cnnwin)));
+#else
+ GdkScreen *screen;
+ gint monitor;
+ screen = gtk_window_get_screen(GTK_WINDOW(cnnobj->cnnwin));
+ monitor = gdk_screen_get_monitor_at_window(screen, gtk_widget_get_window(GTK_WIDGET(cnnobj->cnnwin)));
+#endif
+
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gdk_monitor_get_workarea(monitor, &monitor_geometry);
+ /* Under Wayland, GTK 3.22, all values returned by gdk_monitor_get_geometry()
+ * and gdk_monitor_get_workarea() seem to have been divided by the
+ * gdk scale factor, so we need to adjust the returned rect
+ * undoing the division */
+#ifdef GDK_WINDOWING_WAYLAND
+ if (GDK_IS_WAYLAND_DISPLAY(display)) {
+ int monitor_scale_factor = gdk_monitor_get_scale_factor(monitor);
+ monitor_geometry.width *= monitor_scale_factor;
+ monitor_geometry.height *= monitor_scale_factor;
+ }
+#endif
+#elif gdk_screen_get_monitor_workarea
+ gdk_screen_get_monitor_workarea(screen, monitor, &monitor_geometry);
+#else
+ gdk_screen_get_monitor_geometry(screen, monitor, &monitor_geometry);
+#endif
+ *sz = monitor_geometry;
+}
+
+static void rco_check_resize(RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ gboolean scroll_required = FALSE;
+
+ GdkRectangle monitor_geometry;
+ gint rd_width, rd_height;
+ gint bordersz;
+ gint scalemode;
+
+ scalemode = remmina_protocol_widget_get_current_scale_mode(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+
+ /* Get remote destkop size */
+ rco_get_desktop_size(cnnobj, &rd_width, &rd_height);
+
+ /* Get our monitor size */
+ rco_get_monitor_geometry(cnnobj, &monitor_geometry);
+
+ if (!remmina_protocol_widget_get_expand(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)) &&
+ (monitor_geometry.width < rd_width || monitor_geometry.height < rd_height) &&
+ scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE)
+ scroll_required = TRUE;
+
+ switch (cnnobj->cnnwin->priv->view_mode) {
+ case SCROLLED_FULLSCREEN_MODE:
+ gtk_window_resize(GTK_WINDOW(cnnobj->cnnwin), monitor_geometry.width, monitor_geometry.height);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container),
+ (scroll_required ? GTK_POLICY_AUTOMATIC : GTK_POLICY_NEVER),
+ (scroll_required ? GTK_POLICY_AUTOMATIC : GTK_POLICY_NEVER));
+ break;
+
+ case VIEWPORT_FULLSCREEN_MODE:
+ bordersz = scroll_required ? SCROLL_BORDER_SIZE : 0;
+ gtk_window_resize(GTK_WINDOW(cnnobj->cnnwin), monitor_geometry.width, monitor_geometry.height);
+ if (REMMINA_IS_SCROLLED_VIEWPORT(cnnobj->scrolled_container))
+ /* Put a border around Notebook content (RemminaScrolledViewpord), so we can
+ * move the mouse over the border to scroll */
+ gtk_container_set_border_width(GTK_CONTAINER(cnnobj->scrolled_container), bordersz);
+
+ break;
+
+ case SCROLLED_WINDOW_MODE:
+ if (remmina_file_get_int(cnnobj->remmina_file, "viewmode", UNDEFINED_MODE) == UNDEFINED_MODE) {
+ /* ToDo: is this really needed ? When ? */
+ gtk_window_set_default_size(GTK_WINDOW(cnnobj->cnnwin),
+ MIN(rd_width, monitor_geometry.width), MIN(rd_height, monitor_geometry.height));
+ if (rd_width >= monitor_geometry.width || rd_height >= monitor_geometry.height) {
+ gtk_window_maximize(GTK_WINDOW(cnnobj->cnnwin));
+ remmina_file_set_int(cnnobj->remmina_file, "window_maximize", TRUE);
+ } else {
+ rcw_toolbar_autofit(NULL, cnnobj->cnnwin);
+ remmina_file_set_int(cnnobj->remmina_file, "window_maximize", FALSE);
+ }
+ } else {
+ if (remmina_file_get_int(cnnobj->remmina_file, "window_maximize", FALSE))
+ gtk_window_maximize(GTK_WINDOW(cnnobj->cnnwin));
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+static void rcw_set_tooltip(GtkWidget *item, const gchar *tip, guint key1, guint key2)
+{
+ TRACE_CALL(__func__);
+ gchar *s1;
+ gchar *s2;
+
+ if (remmina_pref.hostkey && key1) {
+ if (key2)
+ s1 = g_strdup_printf(" (%s + %s,%s)", gdk_keyval_name(remmina_pref.hostkey),
+ gdk_keyval_name(gdk_keyval_to_upper(key1)), gdk_keyval_name(gdk_keyval_to_upper(key2)));
+ else if (key1 == remmina_pref.hostkey)
+ s1 = g_strdup_printf(" (%s)", gdk_keyval_name(remmina_pref.hostkey));
+ else
+ s1 = g_strdup_printf(" (%s + %s)", gdk_keyval_name(remmina_pref.hostkey),
+ gdk_keyval_name(gdk_keyval_to_upper(key1)));
+ } else {
+ s1 = NULL;
+ }
+ s2 = g_strdup_printf("%s%s", tip, s1 ? s1 : "");
+ gtk_widget_set_tooltip_text(item, s2);
+ g_free(s2);
+ g_free(s1);
+}
+
+static void remmina_protocol_widget_update_alignment(RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ RemminaScaleMode scalemode;
+ gboolean scaledexpandedmode;
+ int rdwidth, rdheight;
+ gfloat aratio;
+
+ if (!cnnobj->plugin_can_scale) {
+ /* If we have a plugin that cannot scale,
+ * (i.e. SFTP plugin), then we expand proto */
+ gtk_widget_set_halign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL);
+ gtk_widget_set_valign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL);
+ } else {
+ /* Plugin can scale */
+
+ scalemode = get_current_allowed_scale_mode(cnnobj, NULL, NULL);
+ scaledexpandedmode = remmina_protocol_widget_get_expand(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+
+ /* Check if we need aspectframe and create/destroy it accordingly */
+ if (scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED && !scaledexpandedmode) {
+ /* We need an aspectframe as a parent of proto */
+ rdwidth = remmina_protocol_widget_get_width(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+ rdheight = remmina_protocol_widget_get_height(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+ aratio = (gfloat)rdwidth / (gfloat)rdheight;
+ if (!cnnobj->aspectframe) {
+ /* We need a new aspectframe */
+ cnnobj->aspectframe = gtk_aspect_frame_new(NULL, 0.5, 0.5, aratio, FALSE);
+ gtk_widget_set_name(cnnobj->aspectframe, "remmina-cw-aspectframe");
+ gtk_frame_set_shadow_type(GTK_FRAME(cnnobj->aspectframe), GTK_SHADOW_NONE);
+ g_object_ref(cnnobj->proto);
+ gtk_container_remove(GTK_CONTAINER(cnnobj->viewport), cnnobj->proto);
+ gtk_container_add(GTK_CONTAINER(cnnobj->viewport), cnnobj->aspectframe);
+ gtk_container_add(GTK_CONTAINER(cnnobj->aspectframe), cnnobj->proto);
+ g_object_unref(cnnobj->proto);
+ gtk_widget_show(cnnobj->aspectframe);
+ if (cnnobj != NULL && cnnobj->cnnwin != NULL && cnnobj->cnnwin->priv->notebook != NULL)
+ rcw_grab_focus(cnnobj->cnnwin);
+ } else {
+ gtk_aspect_frame_set(GTK_ASPECT_FRAME(cnnobj->aspectframe), 0.5, 0.5, aratio, FALSE);
+ }
+ } else {
+ /* We do not need an aspectframe as a parent of proto */
+ if (cnnobj->aspectframe) {
+ /* We must remove the old aspectframe reparenting proto to viewport */
+ g_object_ref(cnnobj->aspectframe);
+ g_object_ref(cnnobj->proto);
+ gtk_container_remove(GTK_CONTAINER(cnnobj->aspectframe), cnnobj->proto);
+ gtk_container_remove(GTK_CONTAINER(cnnobj->viewport), cnnobj->aspectframe);
+ g_object_unref(cnnobj->aspectframe);
+ cnnobj->aspectframe = NULL;
+ gtk_container_add(GTK_CONTAINER(cnnobj->viewport), cnnobj->proto);
+ g_object_unref(cnnobj->proto);
+ if (cnnobj != NULL && cnnobj->cnnwin != NULL && cnnobj->cnnwin->priv->notebook != NULL)
+ rcw_grab_focus(cnnobj->cnnwin);
+ }
+ }
+
+ if (scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED || scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES) {
+ /* We have a plugin that can be scaled, and the scale button
+ * has been pressed. Give it the correct WxH maintaining aspect
+ * ratio of remote destkop size */
+ gtk_widget_set_halign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL);
+ gtk_widget_set_valign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL);
+ } else {
+ /* Plugin can scale, but no scaling is active. Ensure that we have
+ * aspectframe with a ratio of 1 */
+ gtk_widget_set_halign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_CENTER);
+ }
+ }
+}
+
+static void nb_set_current_page(GtkNotebook *notebook, GtkWidget *page)
+{
+ gint np, i;
+
+ np = gtk_notebook_get_n_pages(notebook);
+ for (i = 0; i < np; i++) {
+ if (gtk_notebook_get_nth_page(notebook, i) == page) {
+ gtk_notebook_set_current_page(notebook, i);
+ break;
+ }
+ }
+}
+
+static void nb_migrate_message_panels(GtkWidget *frompage, GtkWidget *topage)
+{
+ /* Migrate a single connection tab from a notebook to another one */
+ GList *lst, *l;
+
+ /* Reparent message panels */
+ lst = gtk_container_get_children(GTK_CONTAINER(frompage));
+ for (l = lst; l != NULL; l = l->next) {
+ if (REMMINA_IS_MESSAGE_PANEL(l->data)) {
+ g_object_ref(l->data);
+ gtk_container_remove(GTK_CONTAINER(frompage), GTK_WIDGET(l->data));
+ gtk_container_add(GTK_CONTAINER(topage), GTK_WIDGET(l->data));
+ g_object_unref(l->data);
+ gtk_box_reorder_child(GTK_BOX(topage), GTK_WIDGET(l->data), 0);
+ }
+ }
+ g_list_free(lst);
+
+}
+
+static void rcw_migrate(RemminaConnectionWindow *from, RemminaConnectionWindow *to)
+{
+ /* Migrate a complete notebook from a window to another */
+
+ gchar *tag;
+ gint cp, np, i;
+ GtkNotebook *from_notebook;
+ GtkWidget *frompage, *newpage, *old_scrolled_container;
+ RemminaConnectionObject *cnnobj;
+ RemminaScaleMode scalemode;
+
+ /* Migrate TAG */
+ tag = g_strdup((gchar *)g_object_get_data(G_OBJECT(from), "tag"));
+ g_object_set_data_full(G_OBJECT(to), "tag", tag, (GDestroyNotify)g_free);
+
+ /* Migrate notebook content */
+ from_notebook = from->priv->notebook;
+ if (from_notebook && GTK_IS_NOTEBOOK(from_notebook)) {
+
+ cp = gtk_notebook_get_current_page(from_notebook);
+ np = gtk_notebook_get_n_pages(from_notebook);
+ /* Create pages on dest notebook and migrate
+ * page content */
+ for (i = 0; i < np; i++) {
+ frompage = gtk_notebook_get_nth_page(from_notebook, i);
+ cnnobj = g_object_get_data(G_OBJECT(frompage), "cnnobj");
+
+ /* A scrolled container must be recreated, because it can be different on the new window/page
+ depending on view_mode */
+ scalemode = get_current_allowed_scale_mode(cnnobj, NULL, NULL);
+ old_scrolled_container = cnnobj->scrolled_container;
+ cnnobj->scrolled_container = rco_create_scrolled_container(scalemode, to->priv->view_mode);
+
+ newpage = rcw_append_new_page(to, cnnobj);
+
+ nb_migrate_message_panels(frompage, newpage);
+
+ /* Reparent the viewport (which is inside scrolled_container) to the new page */
+ g_object_ref(cnnobj->viewport);
+ gtk_container_remove(GTK_CONTAINER(old_scrolled_container), cnnobj->viewport);
+ gtk_container_add(GTK_CONTAINER(cnnobj->scrolled_container), cnnobj->viewport);
+ g_object_unref(cnnobj->viewport);
+
+ /* Destroy old scrolled_container. Not really needed, it will be destroyed
+ * when removing the page from the notepad */
+ gtk_widget_destroy(old_scrolled_container);
+
+ }
+
+ /* Remove all the pages from source notebook */
+ for (i = np - 1; i >= 0; i--)
+ gtk_notebook_remove_page(from_notebook, i);
+ gtk_notebook_set_current_page(to->priv->notebook, cp);
+
+ }
+}
+
+static void rcw_switch_viewmode(RemminaConnectionWindow *cnnwin, int newmode)
+{
+ GdkWindowState s;
+ RemminaConnectionWindow *newwin;
+ gint old_width, old_height;
+ int old_mode;
+
+ old_mode = cnnwin->priv->view_mode;
+ if (old_mode == newmode)
+ return;
+
+ if (newmode == VIEWPORT_FULLSCREEN_MODE || newmode == SCROLLED_FULLSCREEN_MODE) {
+ if (old_mode == SCROLLED_WINDOW_MODE) {
+ /* We are leaving SCROLLED_WINDOW_MODE, save W,H, and maximized
+ * status before self destruction of cnnwin */
+ gtk_window_get_size(GTK_WINDOW(cnnwin), &old_width, &old_height);
+ s = gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(cnnwin)));
+ }
+ newwin = rcw_create_fullscreen(GTK_WINDOW(cnnwin), cnnwin->priv->fss_view_mode);
+ rcw_migrate(cnnwin, newwin);
+ if (old_mode == SCROLLED_WINDOW_MODE) {
+ newwin->priv->ss_maximized = (s & GDK_WINDOW_STATE_MAXIMIZED) ? TRUE : FALSE;
+ newwin->priv->ss_width = old_width;
+ newwin->priv->ss_height = old_height;
+ }
+ } else {
+ newwin = rcw_create_scrolled(cnnwin->priv->ss_width, cnnwin->priv->ss_height,
+ cnnwin->priv->ss_maximized);
+ rcw_migrate(cnnwin, newwin);
+ if (old_mode == VIEWPORT_FULLSCREEN_MODE || old_mode == SCROLLED_FULLSCREEN_MODE)
+ /* We are leaving a FULLSCREEN mode, save some parameters
+ * status before self destruction of cnnwin */
+ newwin->priv->fss_view_mode = old_mode;
+ }
+
+ /* Prevent unreleased hostkey from old window to be released here */
+ newwin->priv->hostkey_used = TRUE;
+}
+
+
+static void rcw_toolbar_fullscreen(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+
+ RemminaConnectionObject *cnnobj;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto);
+
+ if (remmina_protocol_widget_get_multimon(gp) >= 1) {
+ REMMINA_DEBUG("Fullscreen on all monitor");
+ gdk_window_set_fullscreen_mode(gtk_widget_get_window(GTK_WIDGET(toggle)), GDK_FULLSCREEN_ON_ALL_MONITORS);
+ } else {
+ REMMINA_DEBUG("Fullscreen on one monitor");
+ }
+
+ if ((toggle != NULL && toggle == cnnwin->priv->toolitem_fullscreen)) {
+ if (gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle))) {
+ if (remmina_protocol_widget_get_multimon(gp) >= 1)
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(cnnwin->priv->toolitem_multimon), TRUE);
+ rcw_switch_viewmode(cnnwin, cnnwin->priv->fss_view_mode);
+ } else {
+ rcw_switch_viewmode(cnnwin, SCROLLED_WINDOW_MODE);
+ }
+ } else
+ if (gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(cnnwin->priv->toolitem_multimon))) {
+ rcw_switch_viewmode(cnnwin, cnnwin->priv->fss_view_mode);
+ } else {
+ rcw_switch_viewmode(cnnwin, SCROLLED_WINDOW_MODE);
+ }
+}
+
+static void rco_viewport_fullscreen_mode(GtkWidget *widget, RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindow *newwin;
+
+ if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget)))
+ return;
+ cnnobj->cnnwin->priv->fss_view_mode = VIEWPORT_FULLSCREEN_MODE;
+ newwin = rcw_create_fullscreen(GTK_WINDOW(cnnobj->cnnwin), VIEWPORT_FULLSCREEN_MODE);
+ rcw_migrate(cnnobj->cnnwin, newwin);
+}
+
+static void rco_scrolled_fullscreen_mode(GtkWidget *widget, RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindow *newwin;
+
+ if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget)))
+ return;
+ cnnobj->cnnwin->priv->fss_view_mode = SCROLLED_FULLSCREEN_MODE;
+ newwin = rcw_create_fullscreen(GTK_WINDOW(cnnobj->cnnwin), SCROLLED_FULLSCREEN_MODE);
+ rcw_migrate(cnnobj->cnnwin, newwin);
+}
+
+static void rcw_fullscreen_option_popdown(GtkWidget *widget, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+
+ priv->sticky = FALSE;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->fullscreen_option_button), FALSE);
+ rcw_floating_toolbar_show(cnnwin, FALSE);
+}
+
+void rcw_toolbar_fullscreen_option(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+ GtkWidget *menu;
+ GtkWidget *menuitem;
+ GSList *group;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)))
+ return;
+
+ cnnwin->priv->sticky = TRUE;
+
+ menu = gtk_menu_new();
+
+ menuitem = gtk_radio_menu_item_new_with_label(NULL, _("Viewport fullscreen mode"));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
+ if (cnnwin->priv->view_mode == VIEWPORT_FULLSCREEN_MODE)
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE);
+ g_signal_connect(G_OBJECT(menuitem), "toggled", G_CALLBACK(rco_viewport_fullscreen_mode), cnnobj);
+
+ menuitem = gtk_radio_menu_item_new_with_label(group, _("Scrolled fullscreen"));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ if (cnnwin->priv->view_mode == SCROLLED_FULLSCREEN_MODE)
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE);
+ g_signal_connect(G_OBJECT(menuitem), "toggled", G_CALLBACK(rco_scrolled_fullscreen_mode), cnnobj);
+
+ g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_fullscreen_option_popdown), cnnwin);
+
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle),
+ GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
+#else
+ gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, cnnwin->priv->toolitem_fullscreen, 0,
+ gtk_get_current_event_time());
+#endif
+}
+
+
+static void rcw_scaler_option_popdown(GtkWidget *widget, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+
+ if (priv->toolbar_is_reconfiguring)
+ return;
+ priv->sticky = FALSE;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->scaler_option_button), FALSE);
+ rcw_floating_toolbar_show(cnnwin, FALSE);
+}
+
+static void rcw_scaler_expand(GtkWidget *widget, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget)))
+ return;
+ cnnobj = rcw_get_visible_cnnobj(cnnwin);
+ if (!cnnobj)
+ return;
+ remmina_protocol_widget_set_expand(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), TRUE);
+ remmina_file_set_int(cnnobj->remmina_file, "scaler_expand", TRUE);
+ remmina_protocol_widget_update_alignment(cnnobj);
+}
+static void rcw_scaler_keep_aspect(GtkWidget *widget, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ if (!gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget)))
+ return;
+ cnnobj = rcw_get_visible_cnnobj(cnnwin);
+ if (!cnnobj)
+ return;
+
+ remmina_protocol_widget_set_expand(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), FALSE);
+ remmina_file_set_int(cnnobj->remmina_file, "scaler_expand", FALSE);
+ remmina_protocol_widget_update_alignment(cnnobj);
+}
+
+static void rcw_toolbar_scaler_option(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv;
+ RemminaConnectionObject *cnnobj;
+ GtkWidget *menu;
+ GtkWidget *menuitem;
+ GSList *group;
+ gboolean scaler_expand;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+ priv = cnnwin->priv;
+
+ if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)))
+ return;
+
+ scaler_expand = remmina_protocol_widget_get_expand(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+
+ priv->sticky = TRUE;
+
+ menu = gtk_menu_new();
+
+ menuitem = gtk_radio_menu_item_new_with_label(NULL, _("Keep aspect ratio when scaled"));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
+ if (!scaler_expand)
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE);
+ g_signal_connect(G_OBJECT(menuitem), "toggled", G_CALLBACK(rcw_scaler_keep_aspect), cnnwin);
+
+ menuitem = gtk_radio_menu_item_new_with_label(group, _("Fill client window when scaled"));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ if (scaler_expand)
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE);
+ g_signal_connect(G_OBJECT(menuitem), "toggled", G_CALLBACK(rcw_scaler_expand), cnnwin);
+
+ g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_scaler_option_popdown), cnnwin);
+
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle),
+ GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
+#else
+ gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, priv->toolitem_scale, 0,
+ gtk_get_current_event_time());
+#endif
+}
+
+void rco_switch_page_activate(GtkMenuItem *menuitem, RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv;
+ gint page_num;
+
+ page_num = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menuitem), "new-page-num"));
+ gtk_notebook_set_current_page(GTK_NOTEBOOK(priv->notebook), page_num);
+}
+
+void rcw_toolbar_switch_page_popdown(GtkWidget *widget, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+
+ priv->sticky = FALSE;
+
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_switch_page), FALSE);
+ rcw_floating_toolbar_show(cnnwin, FALSE);
+}
+
+static void rcw_toolbar_switch_page(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+ RemminaConnectionObject *cnnobj;
+
+ GtkWidget *menu;
+ GtkWidget *menuitem;
+ GtkWidget *image;
+ gint i, n;
+
+ if (priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ if (!gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle)))
+ return;
+
+ priv->sticky = TRUE;
+
+ menu = gtk_menu_new();
+
+ n = gtk_notebook_get_n_pages(GTK_NOTEBOOK(priv->notebook));
+ for (i = 0; i < n; i++) {
+ cnnobj = rcw_get_cnnobj_at_page(cnnobj->cnnwin, i);
+
+ menuitem = gtk_menu_item_new_with_label(remmina_file_get_string(cnnobj->remmina_file, "name"));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+
+ image = gtk_image_new_from_icon_name(remmina_file_get_icon_name(cnnobj->remmina_file), GTK_ICON_SIZE_MENU);
+ gtk_widget_show(image);
+
+ g_object_set_data(G_OBJECT(menuitem), "new-page-num", GINT_TO_POINTER(i));
+ g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(rco_switch_page_activate), cnnobj);
+ if (i == gtk_notebook_get_current_page(GTK_NOTEBOOK(priv->notebook)))
+ gtk_widget_set_sensitive(menuitem, FALSE);
+ }
+
+ g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_toolbar_switch_page_popdown),
+ cnnwin);
+
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle),
+ GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
+#else
+ gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, widget, 0, gtk_get_current_event_time());
+#endif
+}
+
+void rco_update_toolbar_autofit_button(RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv;
+ GtkToolItem *toolitem;
+ RemminaScaleMode sc;
+
+ toolitem = priv->toolitem_autofit;
+ if (toolitem) {
+ if (priv->view_mode != SCROLLED_WINDOW_MODE) {
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE);
+ } else {
+ sc = get_current_allowed_scale_mode(cnnobj, NULL, NULL);
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), sc == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE);
+ }
+ }
+}
+
+static void rco_change_scalemode(RemminaConnectionObject *cnnobj, gboolean bdyn, gboolean bscale)
+{
+ RemminaScaleMode scalemode;
+ RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv;
+
+ if (bdyn)
+ scalemode = REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES;
+ else if (bscale)
+ scalemode = REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED;
+ else
+ scalemode = REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE;
+
+ remmina_protocol_widget_set_current_scale_mode(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), scalemode);
+ remmina_file_set_int(cnnobj->remmina_file, "scale", scalemode);
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->scaler_option_button), scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED);
+ rco_update_toolbar_autofit_button(cnnobj);
+
+ remmina_protocol_widget_call_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto),
+ REMMINA_PROTOCOL_FEATURE_TYPE_SCALE, 0);
+
+ if (cnnobj->cnnwin->priv->view_mode != SCROLLED_WINDOW_MODE)
+ rco_check_resize(cnnobj);
+ if (GTK_IS_SCROLLED_WINDOW(cnnobj->scrolled_container)) {
+ rco_set_scrolled_policy(scalemode, GTK_SCROLLED_WINDOW(cnnobj->scrolled_container));
+ }
+}
+
+static void rcw_toolbar_dynres(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ gboolean bdyn, bscale;
+ RemminaConnectionObject *cnnobj;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ if (cnnobj->connected) {
+ bdyn = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle));
+ bscale = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(cnnobj->cnnwin->priv->toolitem_scale));
+
+ if (bdyn && bscale) {
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(cnnobj->cnnwin->priv->toolitem_scale), FALSE);
+ bscale = FALSE;
+ }
+
+ rco_change_scalemode(cnnobj, bdyn, bscale);
+ }
+}
+
+static void rcw_toolbar_scaled_mode(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ gboolean bdyn, bscale;
+ RemminaConnectionObject *cnnobj;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ bdyn = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(cnnobj->cnnwin->priv->toolitem_dynres));
+ bscale = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle));
+
+ if (bdyn && bscale) {
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(cnnobj->cnnwin->priv->toolitem_dynres), FALSE);
+ bdyn = FALSE;
+ }
+
+ rco_change_scalemode(cnnobj, bdyn, bscale);
+}
+
+static void rcw_toolbar_multi_monitor_mode(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ if (gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle))) {
+ REMMINA_DEBUG("Saving multimon as 1");
+ remmina_file_set_int(cnnobj->remmina_file, "multimon", 1);
+ remmina_file_save(cnnobj->remmina_file);
+ remmina_protocol_widget_call_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto),
+ REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON, 0);
+ if (!gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(cnnwin->priv->toolitem_fullscreen)))
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(cnnwin->priv->toolitem_fullscreen), TRUE);
+ } else {
+ REMMINA_DEBUG("Saving multimon as 0");
+ remmina_file_set_int(cnnobj->remmina_file, "multimon", 0);
+ remmina_file_save(cnnobj->remmina_file);
+ rcw_toolbar_fullscreen(NULL, cnnwin);
+ }
+}
+
+static void rcw_toolbar_open_main(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+
+ remmina_exec_command(REMMINA_COMMAND_MAIN, NULL);
+}
+
+static void rcw_toolbar_preferences_popdown(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ cnnobj->cnnwin->priv->sticky = FALSE;
+
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(cnnobj->cnnwin->priv->toolitem_preferences), FALSE);
+ rcw_floating_toolbar_show(cnnwin, FALSE);
+}
+
+void rcw_toolbar_menu_popdown(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+
+ if (priv->toolbar_is_reconfiguring)
+ return;
+
+ priv->sticky = FALSE;
+
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_menu), FALSE);
+ rcw_floating_toolbar_show(cnnwin, FALSE);
+}
+
+void rcw_toolbar_tools_popdown(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+
+ if (priv->toolbar_is_reconfiguring)
+ return;
+
+ priv->sticky = FALSE;
+
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_tools), FALSE);
+ rcw_floating_toolbar_show(cnnwin, FALSE);
+}
+
+static void rco_call_protocol_feature_radio(GtkMenuItem *menuitem, RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolFeature *feature;
+ gpointer value;
+
+ if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem))) {
+ feature = (RemminaProtocolFeature *)g_object_get_data(G_OBJECT(menuitem), "feature-type");
+ value = g_object_get_data(G_OBJECT(menuitem), "feature-value");
+
+ remmina_file_set_string(cnnobj->remmina_file, (const gchar *)feature->opt2, (const gchar *)value);
+ remmina_protocol_widget_call_feature_by_ref(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), feature);
+ }
+}
+
+static void rco_call_protocol_feature_check(GtkMenuItem *menuitem, RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolFeature *feature;
+ gboolean value;
+
+ feature = (RemminaProtocolFeature *)g_object_get_data(G_OBJECT(menuitem), "feature-type");
+ value = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem));
+ remmina_file_set_int(cnnobj->remmina_file, (const gchar *)feature->opt2, value);
+ remmina_protocol_widget_call_feature_by_ref(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), feature);
+}
+
+static void rco_call_protocol_feature_activate(GtkMenuItem *menuitem, RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolFeature *feature;
+
+ feature = (RemminaProtocolFeature *)g_object_get_data(G_OBJECT(menuitem), "feature-type");
+ remmina_protocol_widget_call_feature_by_ref(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), feature);
+}
+
+void rcw_toolbar_preferences_radio(RemminaConnectionObject *cnnobj, RemminaFile *remminafile,
+ GtkWidget *menu, const RemminaProtocolFeature *feature, const gchar *domain, gboolean enabled)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *menuitem;
+ GSList *group;
+ gint i;
+ const gchar **list;
+ const gchar *value;
+
+ group = NULL;
+ value = remmina_file_get_string(remminafile, (const gchar *)feature->opt2);
+ list = (const gchar **)feature->opt3;
+ for (i = 0; list[i]; i += 2) {
+ menuitem = gtk_radio_menu_item_new_with_label(group, g_dgettext(domain, list[i + 1]));
+ group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+
+ if (enabled) {
+ g_object_set_data(G_OBJECT(menuitem), "feature-type", (gpointer)feature);
+ g_object_set_data(G_OBJECT(menuitem), "feature-value", (gpointer)list[i]);
+
+ if (value && g_strcmp0(list[i], value) == 0)
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE);
+
+ g_signal_connect(G_OBJECT(menuitem), "toggled",
+ G_CALLBACK(rco_call_protocol_feature_radio), cnnobj);
+ } else {
+ gtk_widget_set_sensitive(menuitem, FALSE);
+ }
+ }
+}
+
+void rcw_toolbar_preferences_check(RemminaConnectionObject *cnnobj,
+ GtkWidget *menu, const RemminaProtocolFeature *feature,
+ const gchar *domain, gboolean enabled)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *menuitem;
+
+ menuitem = gtk_check_menu_item_new_with_label(g_dgettext(domain, (const gchar *)feature->opt3));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+
+ if (enabled) {
+ g_object_set_data(G_OBJECT(menuitem), "feature-type", (gpointer)feature);
+
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem),
+ remmina_file_get_int(cnnobj->remmina_file, (const gchar *)feature->opt2, FALSE));
+
+ g_signal_connect(G_OBJECT(menuitem), "toggled",
+ G_CALLBACK(rco_call_protocol_feature_check), cnnobj);
+ } else {
+ gtk_widget_set_sensitive(menuitem, FALSE);
+ }
+}
+
+static void rcw_toolbar_preferences(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv;
+ RemminaConnectionObject *cnnobj;
+ const RemminaProtocolFeature *feature;
+ GtkWidget *menu;
+ GtkWidget *menuitem;
+ gboolean separator;
+ gchar *domain;
+ gboolean enabled;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+ priv = cnnobj->cnnwin->priv;
+
+ if (!gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle)))
+ return;
+
+ priv->sticky = TRUE;
+
+ separator = FALSE;
+
+ domain = remmina_protocol_widget_get_domain(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+ menu = gtk_menu_new();
+ for (feature = remmina_protocol_widget_get_features(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); feature && feature->type;
+ feature++) {
+ if (feature->type != REMMINA_PROTOCOL_FEATURE_TYPE_PREF)
+ continue;
+
+ if (separator) {
+ menuitem = gtk_separator_menu_item_new();
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ separator = FALSE;
+ }
+ enabled = remmina_protocol_widget_query_feature_by_ref(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), feature);
+ switch (GPOINTER_TO_INT(feature->opt1)) {
+ case REMMINA_PROTOCOL_FEATURE_PREF_RADIO:
+ rcw_toolbar_preferences_radio(cnnobj, cnnobj->remmina_file, menu, feature,
+ domain, enabled);
+ separator = TRUE;
+ break;
+ case REMMINA_PROTOCOL_FEATURE_PREF_CHECK:
+ rcw_toolbar_preferences_check(cnnobj, menu, feature,
+ domain, enabled);
+ break;
+ }
+ }
+
+ g_free(domain);
+
+ g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_toolbar_preferences_popdown), cnnwin);
+
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle),
+ GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
+#else
+ gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, widget, 0, gtk_get_current_event_time());
+#endif
+}
+
+static void rcw_toolbar_menu_on_launch_item(RemminaAppletMenu *menu, RemminaAppletMenuItem *menuitem, gpointer data)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+
+ switch (menuitem->item_type) {
+ case REMMINA_APPLET_MENU_ITEM_NEW:
+ remmina_exec_command(REMMINA_COMMAND_NEW, NULL);
+ break;
+ case REMMINA_APPLET_MENU_ITEM_FILE:
+ remmina_exec_command(REMMINA_COMMAND_CONNECT, menuitem->filename);
+ break;
+ case REMMINA_APPLET_MENU_ITEM_DISCOVERED:
+ s = g_strdup_printf("%s,%s", menuitem->protocol, menuitem->name);
+ remmina_exec_command(REMMINA_COMMAND_NEW, s);
+ g_free(s);
+ break;
+ }
+}
+
+static void rcw_toolbar_menu(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv;
+ RemminaConnectionObject *cnnobj;
+ GtkWidget *menu;
+ GtkWidget *menuitem = NULL;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+ priv = cnnobj->cnnwin->priv;
+
+ if (!gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle)))
+ return;
+
+ priv->sticky = TRUE;
+
+ menu = remmina_applet_menu_new();
+ remmina_applet_menu_set_hide_count(REMMINA_APPLET_MENU(menu), remmina_pref.applet_hide_count);
+ remmina_applet_menu_populate(REMMINA_APPLET_MENU(menu));
+
+ g_signal_connect(G_OBJECT(menu), "launch-item", G_CALLBACK(rcw_toolbar_menu_on_launch_item), NULL);
+ //g_signal_connect(G_OBJECT(menu), "edit-item", G_CALLBACK(rcw_toolbar_menu_on_edit_item), NULL);
+ menuitem = gtk_separator_menu_item_new();
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle),
+ GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
+#else
+ gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, widget, 0, gtk_get_current_event_time());
+#endif
+ g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_toolbar_menu_popdown), cnnwin);
+}
+
+static void rcw_toolbar_tools(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv;
+ RemminaConnectionObject *cnnobj;
+ const RemminaProtocolFeature *feature;
+ GtkWidget *menu;
+ GtkWidget *menuitem = NULL;
+ GtkMenu *submenu_keystrokes;
+ const gchar *domain;
+ gboolean enabled;
+ gchar **keystrokes;
+ gchar **keystroke_values;
+ gint i;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+ priv = cnnobj->cnnwin->priv;
+
+ if (!gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle)))
+ return;
+
+ priv->sticky = TRUE;
+
+ domain = remmina_protocol_widget_get_domain(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+ menu = gtk_menu_new();
+ for (feature = remmina_protocol_widget_get_features(REMMINA_PROTOCOL_WIDGET(cnnobj->proto)); feature && feature->type;
+ feature++) {
+ if (feature->type != REMMINA_PROTOCOL_FEATURE_TYPE_TOOL)
+ continue;
+
+ if (feature->opt1)
+ menuitem = gtk_menu_item_new_with_label(g_dgettext(domain, (const gchar *)feature->opt1));
+ if (feature->opt3)
+ rcw_set_tooltip(menuitem, "", GPOINTER_TO_UINT(feature->opt3), 0);
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+
+ enabled = remmina_protocol_widget_query_feature_by_ref(REMMINA_PROTOCOL_WIDGET(cnnobj->proto), feature);
+ if (enabled) {
+ g_object_set_data(G_OBJECT(menuitem), "feature-type", (gpointer)feature);
+
+ g_signal_connect(G_OBJECT(menuitem), "activate",
+ G_CALLBACK(rco_call_protocol_feature_activate), cnnobj);
+ } else {
+ gtk_widget_set_sensitive(menuitem, FALSE);
+ }
+ }
+
+ /* If the plugin accepts keystrokes include the keystrokes menu */
+ if (remmina_protocol_widget_plugin_receives_keystrokes(REMMINA_PROTOCOL_WIDGET(cnnobj->proto))) {
+ /* Get the registered keystrokes list */
+ keystrokes = g_strsplit(remmina_pref.keystrokes, STRING_DELIMITOR, -1);
+ if (g_strv_length(keystrokes)) {
+ /* Add a keystrokes submenu */
+ menuitem = gtk_menu_item_new_with_label(_("Keystrokes"));
+ submenu_keystrokes = GTK_MENU(gtk_menu_new());
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), GTK_WIDGET(submenu_keystrokes));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ /* Add each registered keystroke */
+ for (i = 0; i < g_strv_length(keystrokes); i++) {
+ keystroke_values = g_strsplit(keystrokes[i], STRING_DELIMITOR2, -1);
+ if (g_strv_length(keystroke_values) > 1) {
+ /* Add the keystroke if no description was available */
+ menuitem = gtk_menu_item_new_with_label(
+ g_strdup(keystroke_values[strlen(keystroke_values[0]) ? 0 : 1]));
+ g_object_set_data(G_OBJECT(menuitem), "keystrokes", g_strdup(keystroke_values[1]));
+ g_signal_connect_swapped(G_OBJECT(menuitem), "activate",
+ G_CALLBACK(remmina_protocol_widget_send_keystrokes),
+ REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(submenu_keystrokes), menuitem);
+ }
+ g_strfreev(keystroke_values);
+ }
+ menuitem = gtk_menu_item_new_with_label(_("Send clipboard content as keystrokes"));
+ static gchar k_tooltip[] =
+ N_("CAUTION: Pasted text will be sent as a sequence of key-codes as if typed on your local keyboard.\n"
+ "\n"
+ " • For best results use same keyboard settings for both, client and server.\n"
+ "\n"
+ " • If client-keyboard is different from server-keyboard the received text can contain wrong or erroneous characters.\n"
+ "\n"
+ " • Unicode characters and other special characters that can't be translated to local key-codes won’t be sent to the server.\n"
+ "\n");
+ gtk_widget_set_tooltip_text(menuitem, k_tooltip);
+ gtk_menu_shell_append(GTK_MENU_SHELL(submenu_keystrokes), menuitem);
+ g_signal_connect_swapped(G_OBJECT(menuitem), "activate",
+ G_CALLBACK(remmina_protocol_widget_send_clipboard),
+ REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+ gtk_widget_show(menuitem);
+ }
+ g_strfreev(keystrokes);
+ }
+
+ g_signal_connect(G_OBJECT(menu), "deactivate", G_CALLBACK(rcw_toolbar_tools_popdown), cnnwin);
+
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gtk_menu_popup_at_widget(GTK_MENU(menu), GTK_WIDGET(toggle),
+ GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
+#else
+ gtk_menu_popup(GTK_MENU(menu), NULL, NULL, remmina_public_popup_position, widget, 0, gtk_get_current_event_time());
+#endif
+}
+
+static void rcw_toolbar_duplicate(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+
+ RemminaConnectionObject *cnnobj;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ remmina_file_save(cnnobj->remmina_file);
+
+ remmina_exec_command(REMMINA_COMMAND_CONNECT, cnnobj->remmina_file->filename);
+}
+
+static void rcw_toolbar_screenshot(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+
+ GdkPixbuf *screenshot;
+ GdkWindow *active_window;
+ cairo_t *cr;
+ gint width, height;
+ GString *pngstr;
+ gchar *pngname;
+ GtkWidget *dialog;
+ RemminaProtocolWidget *gp;
+ RemminaPluginScreenshotData rpsd;
+ RemminaConnectionObject *cnnobj;
+ cairo_surface_t *srcsurface;
+ cairo_format_t cairo_format;
+ cairo_surface_t *surface;
+ int stride;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ GDateTime *date = g_date_time_new_now_utc();
+
+ // We will take a screenshot of the currently displayed RemminaProtocolWidget.
+ gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto);
+
+ gchar *denyclip = remmina_pref_get_value("deny_screenshot_clipboard");
+
+ REMMINA_DEBUG("deny_screenshot_clipboard is set to %s", denyclip);
+
+ GtkClipboard *c = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
+
+ // Ask the plugin if it can give us a screenshot
+ if (remmina_protocol_widget_plugin_screenshot(gp, &rpsd)) {
+ // Good, we have a screenshot from the plugin !
+
+ REMMINA_DEBUG("Screenshot from plugin: w=%d h=%d bpp=%d bytespp=%d\n",
+ rpsd.width, rpsd.height, rpsd.bitsPerPixel, rpsd.bytesPerPixel);
+
+ width = rpsd.width;
+ height = rpsd.height;
+
+ if (rpsd.bitsPerPixel == 32)
+ cairo_format = CAIRO_FORMAT_ARGB32;
+ else if (rpsd.bitsPerPixel == 24)
+ cairo_format = CAIRO_FORMAT_RGB24;
+ else
+ cairo_format = CAIRO_FORMAT_RGB16_565;
+
+ stride = cairo_format_stride_for_width(cairo_format, width);
+
+ srcsurface = cairo_image_surface_create_for_data(rpsd.buffer, cairo_format, width, height, stride);
+ // Transfer the PixBuf in the main clipboard selection
+ if (denyclip && (g_strcmp0(denyclip, "true")))
+ gtk_clipboard_set_image(c, gdk_pixbuf_get_from_surface(
+ srcsurface, 0, 0, width, height));
+ surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
+ cr = cairo_create(surface);
+ cairo_set_source_surface(cr, srcsurface, 0, 0);
+ cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
+ cairo_paint(cr);
+ cairo_surface_destroy(srcsurface);
+
+ free(rpsd.buffer);
+ } else {
+ // The plugin is not releasing us a screenshot, just try to catch one via GTK
+
+ /* Warn the user if image is distorted */
+ if (cnnobj->plugin_can_scale &&
+ get_current_allowed_scale_mode(cnnobj, NULL, NULL) == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED) {
+ dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK,
+ _("Turn off scaling to avoid screenshot distortion."));
+ g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_widget_show(dialog);
+ }
+
+ // Get the screenshot.
+ active_window = gtk_widget_get_window(GTK_WIDGET(gp));
+ // width = gdk_window_get_width(gtk_widget_get_window(GTK_WIDGET(cnnobj->cnnwin)));
+ width = gdk_window_get_width(active_window);
+ // height = gdk_window_get_height(gtk_widget_get_window(GTK_WIDGET(cnnobj->cnnwin)));
+ height = gdk_window_get_height(active_window);
+
+ screenshot = gdk_pixbuf_get_from_window(active_window, 0, 0, width, height);
+ if (screenshot == NULL)
+ g_print("gdk_pixbuf_get_from_window failed\n");
+
+ // Transfer the PixBuf in the main clipboard selection
+ if (denyclip && (g_strcmp0(denyclip, "true")))
+ gtk_clipboard_set_image(c, screenshot);
+ // Prepare the destination Cairo surface.
+ surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
+ cr = cairo_create(surface);
+
+ // Copy the source pixbuf to the surface and paint it.
+ gdk_cairo_set_source_pixbuf(cr, screenshot, 0, 0);
+ cairo_paint(cr);
+
+ // Deallocate screenshot pixbuf
+ g_object_unref(screenshot);
+ }
+
+ //home/antenore/Pictures/remmina_%p_%h_%Y %m %d-%H%M%S.png pngname
+ //home/antenore/Pictures/remmina_st_ _2018 9 24-151958.240374.png
+
+ pngstr = g_string_new(g_strdup_printf("%s/%s.png",
+ remmina_pref.screenshot_path,
+ remmina_pref.screenshot_name));
+ remmina_utils_string_replace_all(pngstr, "%p",
+ remmina_file_get_string(cnnobj->remmina_file, "name"));
+ remmina_utils_string_replace_all(pngstr, "%h",
+ remmina_file_get_string(cnnobj->remmina_file, "server"));
+ remmina_utils_string_replace_all(pngstr, "%Y",
+ g_strdup_printf("%d", g_date_time_get_year(date)));
+ remmina_utils_string_replace_all(pngstr, "%m", g_strdup_printf("%02d",
+ g_date_time_get_month(date)));
+ remmina_utils_string_replace_all(pngstr, "%d",
+ g_strdup_printf("%02d", g_date_time_get_day_of_month(date)));
+ remmina_utils_string_replace_all(pngstr, "%H",
+ g_strdup_printf("%02d", g_date_time_get_hour(date)));
+ remmina_utils_string_replace_all(pngstr, "%M",
+ g_strdup_printf("%02d", g_date_time_get_minute(date)));
+ remmina_utils_string_replace_all(pngstr, "%S",
+ g_strdup_printf("%02d", g_date_time_get_second(date)));
+ g_date_time_unref(date);
+ pngname = g_string_free(pngstr, FALSE);
+
+ cairo_surface_write_to_png(surface, pngname);
+
+ /* send a desktop notification */
+ if (g_file_test(pngname, G_FILE_TEST_EXISTS))
+ remmina_public_send_notification("remmina-screenshot-is-ready-id", _("Screenshot taken"), pngname);
+
+ //Clean up and return.
+ cairo_destroy(cr);
+ cairo_surface_destroy(surface);
+}
+
+static void rcw_toolbar_minimize(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+
+ rcw_floating_toolbar_show(cnnwin, FALSE);
+ gtk_window_iconify(GTK_WINDOW(cnnwin));
+}
+
+static void rcw_toolbar_disconnect(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+ rco_disconnect_current_page(cnnobj);
+}
+
+static void rcw_toolbar_grab(GtkToolItem *toggle, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ gboolean capture;
+ RemminaConnectionObject *cnnobj;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring)
+ return;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ capture = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(toggle));
+
+ if (cnnobj->connected){
+ remmina_file_set_int(cnnobj->remmina_file, "keyboard_grab", capture);
+ }
+
+ if (capture && cnnobj->connected) {
+
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: Grabbing for button\n");
+#endif
+ rcw_keyboard_grab(cnnobj->cnnwin);
+ if (cnnobj->cnnwin->priv->pointer_entered)
+ rcw_pointer_grab(cnnobj->cnnwin);
+ } else {
+ rcw_kp_ungrab(cnnobj->cnnwin);
+ }
+
+ rco_update_toolbar(cnnobj);
+}
+
+static GtkWidget *
+rcw_create_toolbar(RemminaConnectionWindow *cnnwin, gint mode)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+ RemminaConnectionObject *cnnobj;
+ GtkWidget *toolbar;
+ GtkToolItem *toolitem;
+ GtkWidget *widget;
+ GtkWidget *arrow;
+
+ GdkDisplay *display;
+ gint n_monitors;
+
+ display = gdk_display_get_default();
+ n_monitors = gdk_display_get_n_monitors(display);
+
+ cnnobj = rcw_get_visible_cnnobj(cnnwin);
+
+ priv->toolbar_is_reconfiguring = TRUE;
+
+ toolbar = gtk_toolbar_new();
+ gtk_widget_show(toolbar);
+ gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_ICONS);
+
+ /* Main actions */
+
+ /* Menu */
+ toolitem = gtk_toggle_tool_button_new();
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "view-more-symbolic");
+ gtk_tool_button_set_label(GTK_TOOL_BUTTON(toolitem), _("_Menu"));
+ gtk_tool_item_set_tooltip_text(toolitem, _("Menu"));
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_menu), cnnwin);
+ priv->toolitem_menu = toolitem;
+
+ /* Open Main window */
+ toolitem = gtk_tool_button_new(NULL, "Open Remmina Main window");
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "go-home-symbolic");
+ gtk_tool_item_set_tooltip_text(toolitem, _("Open the Remmina main window"));
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_open_main), cnnwin);
+
+ priv->toolitem_new = toolitem;
+
+ /* Duplicate session */
+ toolitem = gtk_tool_button_new(NULL, "Duplicate connection");
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-duplicate-symbolic");
+ gtk_tool_item_set_tooltip_text(toolitem, _("Duplicate current connection"));
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_duplicate), cnnwin);
+ if (!cnnobj)
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE);
+
+ priv->toolitem_duplicate = toolitem;
+
+ /* Separator */
+ toolitem = gtk_separator_tool_item_new();
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+
+ /* Auto-Fit */
+ toolitem = gtk_tool_button_new(NULL, NULL);
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-fit-window-symbolic");
+ rcw_set_tooltip(GTK_WIDGET(toolitem), _("Resize the window to fit in remote resolution"),
+ remmina_pref.shortcutkey_autofit, 0);
+ g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_autofit), cnnwin);
+ priv->toolitem_autofit = toolitem;
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+
+
+ /* Fullscreen toggle */
+ toolitem = gtk_toggle_tool_button_new();
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-fullscreen-symbolic");
+ rcw_set_tooltip(GTK_WIDGET(toolitem), _("Toggle fullscreen mode"),
+ remmina_pref.shortcutkey_fullscreen, 0);
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ priv->toolitem_fullscreen = toolitem;
+ if (kioskmode) {
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem), FALSE);
+ } else {
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem), mode != SCROLLED_WINDOW_MODE);
+ g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_fullscreen), cnnwin);
+ }
+
+ /* Fullscreen drop-down options */
+ toolitem = gtk_tool_item_new();
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ widget = gtk_toggle_button_new();
+ gtk_widget_show(widget);
+ gtk_container_set_border_width(GTK_CONTAINER(widget), 0);
+ gtk_button_set_relief(GTK_BUTTON(widget), GTK_RELIEF_NONE);
+#if GTK_CHECK_VERSION(3, 20, 0)
+ gtk_widget_set_focus_on_click(GTK_WIDGET(widget), FALSE);
+ if (remmina_pref.small_toolbutton)
+ gtk_widget_set_name(widget, "remmina-small-button");
+
+#else
+ gtk_button_set_focus_on_click(GTK_BUTTON(widget), FALSE);
+#endif
+ gtk_container_add(GTK_CONTAINER(toolitem), widget);
+
+#if GTK_CHECK_VERSION(3, 14, 0)
+ arrow = gtk_image_new_from_icon_name("org.remmina.Remmina-pan-down-symbolic", GTK_ICON_SIZE_SMALL_TOOLBAR);
+#else
+ arrow = gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_NONE);
+#endif
+ gtk_widget_show(arrow);
+ gtk_container_add(GTK_CONTAINER(widget), arrow);
+ g_signal_connect(G_OBJECT(widget), "toggled", G_CALLBACK(rcw_toolbar_fullscreen_option), cnnwin);
+ priv->fullscreen_option_button = widget;
+ if (mode == SCROLLED_WINDOW_MODE)
+ gtk_widget_set_sensitive(GTK_WIDGET(widget), FALSE);
+
+ /* Multi monitor */
+ if (n_monitors > 1) {
+ toolitem = gtk_toggle_tool_button_new();
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-multi-monitor-symbolic");
+ rcw_set_tooltip(GTK_WIDGET(toolitem), _("Multi monitor"),
+ remmina_pref.shortcutkey_multimon, 0);
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_multi_monitor_mode), cnnwin);
+ priv->toolitem_multimon = toolitem;
+ if (!cnnobj)
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE);
+ else
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem),
+ remmina_file_get_int(cnnobj->remmina_file, "multimon", FALSE));
+ }
+
+ /* Dynamic Resolution Update */
+ toolitem = gtk_toggle_tool_button_new();
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-dynres-symbolic");
+ rcw_set_tooltip(GTK_WIDGET(toolitem), _("Toggle dynamic resolution update"),
+ remmina_pref.shortcutkey_dynres, 0);
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_dynres), cnnwin);
+ priv->toolitem_dynres = toolitem;
+
+ /* Scaler button */
+ toolitem = gtk_toggle_tool_button_new();
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-scale-symbolic");
+ rcw_set_tooltip(GTK_WIDGET(toolitem), _("Toggle scaled mode"), remmina_pref.shortcutkey_scale, 0);
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_scaled_mode), cnnwin);
+ priv->toolitem_scale = toolitem;
+
+ /* Scaler aspect ratio dropdown menu */
+ toolitem = gtk_tool_item_new();
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ widget = gtk_toggle_button_new();
+ gtk_widget_show(widget);
+ gtk_container_set_border_width(GTK_CONTAINER(widget), 0);
+ gtk_button_set_relief(GTK_BUTTON(widget), GTK_RELIEF_NONE);
+#if GTK_CHECK_VERSION(3, 20, 0)
+ gtk_widget_set_focus_on_click(GTK_WIDGET(widget), FALSE);
+#else
+ gtk_button_set_focus_on_click(GTK_BUTTON(widget), FALSE);
+#endif
+ if (remmina_pref.small_toolbutton)
+ gtk_widget_set_name(widget, "remmina-small-button");
+ gtk_container_add(GTK_CONTAINER(toolitem), widget);
+#if GTK_CHECK_VERSION(3, 14, 0)
+ arrow = gtk_image_new_from_icon_name("org.remmina.Remmina-pan-down-symbolic", GTK_ICON_SIZE_SMALL_TOOLBAR);
+#else
+ arrow = gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_NONE);
+#endif
+ gtk_widget_show(arrow);
+ gtk_container_add(GTK_CONTAINER(widget), arrow);
+ g_signal_connect(G_OBJECT(widget), "toggled", G_CALLBACK(rcw_toolbar_scaler_option), cnnwin);
+ priv->scaler_option_button = widget;
+
+ /* Separator */
+ toolitem = gtk_separator_tool_item_new();
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+
+ /* Switch tabs */
+ toolitem = gtk_toggle_tool_button_new();
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-switch-page-symbolic");
+ rcw_set_tooltip(GTK_WIDGET(toolitem), _("Switch tab pages"), remmina_pref.shortcutkey_prevtab,
+ remmina_pref.shortcutkey_nexttab);
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_switch_page), cnnwin);
+ priv->toolitem_switch_page = toolitem;
+
+ /* Grab keyboard button */
+ toolitem = gtk_toggle_tool_button_new();
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-keyboard-symbolic");
+ rcw_set_tooltip(GTK_WIDGET(toolitem), _("Grab all keyboard events"),
+ remmina_pref.shortcutkey_grab, 0);
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_grab), cnnwin);
+ priv->toolitem_grab = toolitem;
+ if (!cnnobj)
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE);
+ else {
+ const gchar *protocol = remmina_file_get_string(cnnobj->remmina_file, "protocol");
+ if (g_strcmp0(protocol, "SFTP") == 0 || g_strcmp0(protocol, "SSH") == 0)
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE);
+ }
+
+ /* Preferences */
+ toolitem = gtk_toggle_tool_button_new();
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-preferences-system-symbolic");
+ gtk_tool_item_set_tooltip_text(toolitem, _("Preferences"));
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_preferences), cnnwin);
+ priv->toolitem_preferences = toolitem;
+
+ /* Tools */
+ toolitem = gtk_toggle_tool_button_new();
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-system-run-symbolic");
+ gtk_tool_button_set_label(GTK_TOOL_BUTTON(toolitem), _("_Tools"));
+ gtk_tool_item_set_tooltip_text(toolitem, _("Tools"));
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "toggled", G_CALLBACK(rcw_toolbar_tools), cnnwin);
+ priv->toolitem_tools = toolitem;
+
+ /* Separator */
+ toolitem = gtk_separator_tool_item_new();
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+
+ toolitem = gtk_tool_button_new(NULL, "_Screenshot");
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-camera-photo-symbolic");
+ rcw_set_tooltip(GTK_WIDGET(toolitem), _("Screenshot"), remmina_pref.shortcutkey_screenshot, 0);
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_screenshot), cnnwin);
+ priv->toolitem_screenshot = toolitem;
+
+ /* Separator */
+ toolitem = gtk_separator_tool_item_new();
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+
+ /* Minimize */
+ toolitem = gtk_tool_button_new(NULL, "_Bottom");
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-go-bottom-symbolic");
+ rcw_set_tooltip(GTK_WIDGET(toolitem), _("Minimize window"), remmina_pref.shortcutkey_minimize, 0);
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_minimize), cnnwin);
+ if (kioskmode)
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE);
+
+ /* Disconnect */
+ toolitem = gtk_tool_button_new(NULL, "_Disconnect");
+ gtk_tool_button_set_icon_name(GTK_TOOL_BUTTON(toolitem), "org.remmina.Remmina-disconnect-symbolic");
+ rcw_set_tooltip(GTK_WIDGET(toolitem), _("Disconnect"), remmina_pref.shortcutkey_disconnect, 0);
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), toolitem, -1);
+ gtk_widget_show(GTK_WIDGET(toolitem));
+ g_signal_connect(G_OBJECT(toolitem), "clicked", G_CALLBACK(rcw_toolbar_disconnect), cnnwin);
+
+ priv->toolbar_is_reconfiguring = FALSE;
+ return toolbar;
+}
+
+static void rcw_place_toolbar(GtkToolbar *toolbar, GtkGrid *grid, GtkWidget *sibling, int toolbar_placement)
+{
+ /* Place the toolbar inside the grid and set its orientation */
+
+ if (toolbar_placement == TOOLBAR_PLACEMENT_LEFT || toolbar_placement == TOOLBAR_PLACEMENT_RIGHT)
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(toolbar), GTK_ORIENTATION_VERTICAL);
+ else
+ gtk_orientable_set_orientation(GTK_ORIENTABLE(toolbar), GTK_ORIENTATION_HORIZONTAL);
+
+
+ switch (toolbar_placement) {
+ case TOOLBAR_PLACEMENT_TOP:
+ gtk_widget_set_hexpand(GTK_WIDGET(toolbar), TRUE);
+ gtk_widget_set_vexpand(GTK_WIDGET(toolbar), FALSE);
+ gtk_grid_attach_next_to(GTK_GRID(grid), GTK_WIDGET(toolbar), sibling, GTK_POS_TOP, 1, 1);
+ break;
+ case TOOLBAR_PLACEMENT_RIGHT:
+ gtk_widget_set_vexpand(GTK_WIDGET(toolbar), TRUE);
+ gtk_widget_set_hexpand(GTK_WIDGET(toolbar), FALSE);
+ gtk_grid_attach_next_to(GTK_GRID(grid), GTK_WIDGET(toolbar), sibling, GTK_POS_RIGHT, 1, 1);
+ break;
+ case TOOLBAR_PLACEMENT_BOTTOM:
+ gtk_widget_set_hexpand(GTK_WIDGET(toolbar), TRUE);
+ gtk_widget_set_vexpand(GTK_WIDGET(toolbar), FALSE);
+ gtk_grid_attach_next_to(GTK_GRID(grid), GTK_WIDGET(toolbar), sibling, GTK_POS_BOTTOM, 1, 1);
+ break;
+ case TOOLBAR_PLACEMENT_LEFT:
+ gtk_widget_set_vexpand(GTK_WIDGET(toolbar), TRUE);
+ gtk_widget_set_hexpand(GTK_WIDGET(toolbar), FALSE);
+ gtk_grid_attach_next_to(GTK_GRID(grid), GTK_WIDGET(toolbar), sibling, GTK_POS_LEFT, 1, 1);
+ break;
+ }
+}
+
+static void rco_update_toolbar(RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv;
+ GtkToolItem *toolitem;
+ gboolean bval, dynres_avail, scale_avail;
+ gboolean test_floating_toolbar;
+ RemminaScaleMode scalemode;
+
+ priv->toolbar_is_reconfiguring = TRUE;
+
+ rco_update_toolbar_autofit_button(cnnobj);
+
+ toolitem = priv->toolitem_switch_page;
+ if (kioskmode)
+ bval = FALSE;
+ else
+ bval = (gtk_notebook_get_n_pages(GTK_NOTEBOOK(priv->notebook)) > 1);
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), bval);
+
+ if (cnnobj->remmina_file->filename)
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->toolitem_duplicate), TRUE);
+ else
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->toolitem_duplicate), FALSE);
+
+ scalemode = get_current_allowed_scale_mode(cnnobj, &dynres_avail, &scale_avail);
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->toolitem_dynres), dynres_avail && cnnobj->connected);
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->toolitem_scale), scale_avail && cnnobj->connected);
+
+ switch (scalemode) {
+ case REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE:
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_dynres), FALSE);
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_scale), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->scaler_option_button), FALSE);
+ break;
+ case REMMINA_PROTOCOL_WIDGET_SCALE_MODE_SCALED:
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_dynres), FALSE);
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_scale), TRUE);
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->scaler_option_button), TRUE && cnnobj->connected);
+ break;
+ case REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES:
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_dynres), TRUE);
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_scale), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->scaler_option_button), FALSE);
+ break;
+ }
+
+ /* REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON */
+ toolitem = priv->toolitem_multimon;
+ if (toolitem) {
+ gint hasmultimon = remmina_protocol_widget_query_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto),
+ REMMINA_PROTOCOL_FEATURE_TYPE_MULTIMON);
+
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), cnnobj->connected);
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem),
+ remmina_file_get_int(cnnobj->remmina_file, "multimon", FALSE));
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), hasmultimon);
+ }
+
+ toolitem = priv->toolitem_grab;
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), cnnobj->connected);
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem),
+ remmina_file_get_int(cnnobj->remmina_file, "keyboard_grab", FALSE));
+ const gchar *protocol = remmina_file_get_string(cnnobj->remmina_file, "protocol");
+ if (g_strcmp0(protocol, "SFTP") == 0 || g_strcmp0(protocol, "SSH") == 0) {
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), FALSE);
+ gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(toolitem), FALSE);
+ remmina_file_set_int(cnnobj->remmina_file, "keyboard_grab", FALSE);
+ }
+
+ toolitem = priv->toolitem_preferences;
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), cnnobj->connected);
+ bval = remmina_protocol_widget_query_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto),
+ REMMINA_PROTOCOL_FEATURE_TYPE_PREF);
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), bval && cnnobj->connected);
+
+ toolitem = priv->toolitem_tools;
+ bval = remmina_protocol_widget_query_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto),
+ REMMINA_PROTOCOL_FEATURE_TYPE_TOOL);
+ gtk_widget_set_sensitive(GTK_WIDGET(toolitem), bval && cnnobj->connected);
+
+ gtk_widget_set_sensitive(GTK_WIDGET(priv->toolitem_screenshot), cnnobj->connected);
+
+ gtk_window_set_title(GTK_WINDOW(cnnobj->cnnwin), remmina_file_get_string(cnnobj->remmina_file, "name"));
+
+ test_floating_toolbar = (priv->floating_toolbar_widget != NULL);
+
+ if (test_floating_toolbar) {
+ const gchar *str = remmina_file_get_string(cnnobj->remmina_file, "name");
+ const gchar *format;
+ GdkRGBA rgba;
+ gchar *bg;
+
+ bg = g_strdup(remmina_pref.grab_color);
+ if (!gdk_rgba_parse(&rgba, bg)) {
+ REMMINA_DEBUG("%s cannot be parsed as a color", bg);
+ bg = g_strdup("#00FF00");
+ } else {
+ REMMINA_DEBUG("Using %s as background color", bg);
+ }
+
+ if (remmina_file_get_int(cnnobj->remmina_file, "keyboard_grab", FALSE)) {
+ if (remmina_pref_get_boolean("grab_color_switch")) {
+ gtk_widget_override_background_color(priv->overlay_ftb_fr, GTK_STATE_NORMAL, &rgba);
+ format = g_strconcat("<span bgcolor=\"", bg, "\" size=\"large\"><b>(G: ON) - \%s</b></span>", NULL);
+ } else {
+ gtk_widget_override_background_color(priv->overlay_ftb_fr, GTK_STATE_NORMAL, NULL);
+ format = "<big><b>(G: ON) - \%s</b></big>";
+ }
+ } else {
+ gtk_widget_override_background_color(priv->overlay_ftb_fr, GTK_STATE_NORMAL, NULL);
+ format = "<big><b>(G:OFF) - \%s</b></big>";
+ }
+ gchar *markup;
+
+ markup = g_markup_printf_escaped(format, str);
+ gtk_label_set_markup(GTK_LABEL(priv->floating_toolbar_label), markup);
+ g_free(markup);
+ g_free(bg);
+ }
+
+ priv->toolbar_is_reconfiguring = FALSE;
+}
+
+static void rcw_set_toolbar_visibility(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+
+ if (priv->view_mode == SCROLLED_WINDOW_MODE) {
+ if (remmina_pref.hide_connection_toolbar)
+ gtk_widget_hide(priv->toolbar);
+ else
+ gtk_widget_show(priv->toolbar);
+ }
+}
+
+#if DEBUG_KB_GRABBING
+static void print_crossing_event(GdkEventCrossing *event) {
+ printf("DEBUG_KB_GRABBING: --- Crossing event detail: ");
+ switch (event->detail) {
+ case GDK_NOTIFY_ANCESTOR: printf("GDK_NOTIFY_ANCESTOR"); break;
+ case GDK_NOTIFY_VIRTUAL: printf("GDK_NOTIFY_VIRTUAL"); break;
+ case GDK_NOTIFY_NONLINEAR: printf("GDK_NOTIFY_NONLINEAR"); break;
+ case GDK_NOTIFY_NONLINEAR_VIRTUAL: printf("GDK_NOTIFY_NONLINEAR_VIRTUAL"); break;
+ case GDK_NOTIFY_UNKNOWN: printf("GDK_NOTIFY_UNKNOWN"); break;
+ case GDK_NOTIFY_INFERIOR: printf("GDK_NOTIFY_INFERIOR"); break;
+ default: printf("unknown");
+ }
+ printf("\n");
+ printf("DEBUG_KB_GRABBING: --- Crossing event mode=");
+ switch (event->mode) {
+ case GDK_CROSSING_NORMAL: printf("GDK_CROSSING_NORMAL"); break;
+ case GDK_CROSSING_GRAB: printf("GDK_CROSSING_GRAB"); break;
+ case GDK_CROSSING_UNGRAB: printf("GDK_CROSSING_UNGRAB"); break;
+ case GDK_CROSSING_GTK_GRAB: printf("GDK_CROSSING_GTK_GRAB"); break;
+ case GDK_CROSSING_GTK_UNGRAB: printf("GDK_CROSSING_GTK_UNGRAB"); break;
+ case GDK_CROSSING_STATE_CHANGED: printf("GDK_CROSSING_STATE_CHANGED"); break;
+ case GDK_CROSSING_TOUCH_BEGIN: printf("GDK_CROSSING_TOUCH_BEGIN"); break;
+ case GDK_CROSSING_TOUCH_END: printf("GDK_CROSSING_TOUCH_END"); break;
+ case GDK_CROSSING_DEVICE_SWITCH: printf("GDK_CROSSING_DEVICE_SWITCH"); break;
+ default: printf("unknown");
+ }
+ printf("\n");
+}
+#endif
+
+static gboolean rcw_floating_toolbar_on_enter(GtkWidget *widget, GdkEventCrossing *event,
+ RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ rcw_floating_toolbar_show(cnnwin, TRUE);
+ return TRUE;
+}
+
+static gboolean rcw_floating_toolbar_on_leave(GtkWidget *widget, GdkEventCrossing *event,
+ RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ if (event->detail != GDK_NOTIFY_INFERIOR)
+ rcw_floating_toolbar_show(cnnwin, FALSE);
+ return TRUE;
+}
+
+
+static gboolean rcw_on_enter_notify_event(GtkWidget *widget, GdkEventCrossing *event,
+ gpointer user_data)
+{
+ TRACE_CALL(__func__);
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: enter-notify-event on rcw received\n");
+ print_crossing_event(event);
+#endif
+ return FALSE;
+}
+
+
+
+static gboolean rcw_on_leave_notify_event(GtkWidget *widget, GdkEventCrossing *event,
+ gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)widget;
+
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: leave-notify-event on rcw received\n");
+ print_crossing_event(event);
+#endif
+
+ if (event->mode != GDK_CROSSING_NORMAL && event->mode != GDK_CROSSING_UNGRAB) {
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: ignored because mode is not GDK_CROSSING_NORMAL GDK_CROSSING_UNGRAB\n");
+#endif
+ return FALSE;
+ }
+
+ if (cnnwin->priv->delayed_grab_eventsourceid) {
+ g_source_remove(cnnwin->priv->delayed_grab_eventsourceid);
+ cnnwin->priv->delayed_grab_eventsourceid = 0;
+ }
+
+ /* Workaround for https://gitlab.gnome.org/GNOME/mutter/-/issues/2450#note_1586570 */
+ if (event->mode != GDK_CROSSING_UNGRAB) {
+ rcw_kp_ungrab(cnnwin);
+ rcw_pointer_ungrab(cnnwin);
+ } else {
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: not ungrabbing, this event seems to be an unwanted event from GTK\n");
+#endif
+ }
+
+ return FALSE;
+}
+
+
+static gboolean rco_leave_protocol_widget(GtkWidget *widget, GdkEventCrossing *event,
+ RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: received leave event on RCO.\n");
+ print_crossing_event(event);
+#endif
+
+ if (cnnobj->cnnwin->priv->delayed_grab_eventsourceid) {
+ g_source_remove(cnnobj->cnnwin->priv->delayed_grab_eventsourceid);
+ cnnobj->cnnwin->priv->delayed_grab_eventsourceid = 0;
+ }
+
+ cnnobj->cnnwin->priv->pointer_entered = FALSE;
+
+ /* Ungrab only if the leave is due to normal mouse motion and not to an inferior */
+ if (event->mode == GDK_CROSSING_NORMAL && event->detail != GDK_NOTIFY_INFERIOR)
+ rcw_kp_ungrab(cnnobj->cnnwin);
+
+ return FALSE;
+}
+
+
+gboolean rco_enter_protocol_widget(GtkWidget *widget, GdkEventCrossing *event,
+ RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ gboolean active;
+
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: %s: enter on protocol widget event received\n", __func__);
+ print_crossing_event(event);
+#endif
+
+ RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv;
+ if (!priv->sticky && event->mode == GDK_CROSSING_NORMAL)
+ rcw_floating_toolbar_show(cnnobj->cnnwin, FALSE);
+
+ priv->pointer_entered = TRUE;
+
+ if (event->mode == GDK_CROSSING_UNGRAB) {
+ // Someone steal our grab, take note and do not attempt to regrab
+ cnnobj->cnnwin->priv->kbcaptured = FALSE;
+ cnnobj->cnnwin->priv->pointer_captured = FALSE;
+ return FALSE;
+ }
+
+ /* Check if we need grabbing */
+ active = gtk_window_is_active(GTK_WINDOW(cnnobj->cnnwin));
+ if (remmina_file_get_int(cnnobj->remmina_file, "keyboard_grab", FALSE) && active) {
+ rcw_keyboard_grab(cnnobj->cnnwin);
+ rcw_pointer_grab(cnnobj->cnnwin);
+ }
+
+ return FALSE;
+}
+
+static gboolean focus_in_delayed_grab(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: %s\n", __func__);
+#endif
+ if (cnnwin->priv->pointer_entered) {
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: delayed requesting kb and pointer grab, because of pointer inside\n");
+#endif
+ rcw_keyboard_grab(cnnwin);
+ rcw_pointer_grab(cnnwin);
+ }
+#if DEBUG_KB_GRABBING
+ else {
+ printf("DEBUG_KB_GRABBING: %s not grabbing because pointer_entered is false\n", __func__);
+ }
+#endif
+ cnnwin->priv->delayed_grab_eventsourceid = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void rcw_focus_in(RemminaConnectionWindow *cnnwin)
+{
+ /* This function is the default signal handler for focus-in-event,
+ * but can also be called after a window focus state change event
+ * from rcw_state_event(). So expect to be called twice
+ * when cnnwin gains the focus */
+
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ if (cnnobj && cnnobj->connected && remmina_file_get_int(cnnobj->remmina_file, "keyboard_grab", FALSE)) {
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: Received focus in on rcw, grabbing enabled: requesting kb grab, delayed\n");
+#endif
+ if (cnnwin->priv->delayed_grab_eventsourceid == 0)
+ cnnwin->priv->delayed_grab_eventsourceid = g_timeout_add(300, (GSourceFunc)focus_in_delayed_grab, cnnwin);
+ }
+#if DEBUG_KB_GRABBING
+ else {
+ printf("DEBUG_KB_GRABBING: Received focus in on rcw, but a condition will prevent to grab\n");
+ }
+#endif
+}
+
+static void rcw_focus_out(RemminaConnectionWindow *cnnwin)
+{
+ /* This function is the default signal handler for focus-out-event,
+ * but can also be called after a window focus state change event
+ * from rcw_state_event(). So expect to be called twice
+ * when cnnwin loses the focus */
+
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ rcw_kp_ungrab(cnnwin);
+
+ cnnwin->priv->hostkey_activated = FALSE;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ if (REMMINA_IS_SCROLLED_VIEWPORT(cnnobj->scrolled_container))
+ remmina_scrolled_viewport_remove_motion(REMMINA_SCROLLED_VIEWPORT(cnnobj->scrolled_container));
+
+ if (cnnobj->proto && cnnobj->scrolled_container)
+ remmina_protocol_widget_call_feature_by_type(REMMINA_PROTOCOL_WIDGET(cnnobj->proto),
+ REMMINA_PROTOCOL_FEATURE_TYPE_UNFOCUS, 0);
+}
+
+static gboolean
+rcw_floating_toolbar_hide(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+
+ priv->hidetb_eventsource = 0;
+ rcw_floating_toolbar_show(cnnwin, FALSE);
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean rcw_floating_toolbar_on_scroll(GtkWidget *widget, GdkEventScroll *event,
+ RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ int opacity;
+
+ cnnobj = rcw_get_visible_cnnobj(cnnwin);
+ if (!cnnobj)
+ return TRUE;
+
+ opacity = remmina_file_get_int(cnnobj->remmina_file, "toolbar_opacity", 0);
+ switch (event->direction) {
+ case GDK_SCROLL_UP:
+ if (opacity > 0) {
+ remmina_file_set_int(cnnobj->remmina_file, "toolbar_opacity", opacity - 1);
+ rcw_update_toolbar_opacity(cnnwin);
+ return TRUE;
+ }
+ break;
+ case GDK_SCROLL_DOWN:
+ if (opacity < TOOLBAR_OPACITY_LEVEL) {
+ remmina_file_set_int(cnnobj->remmina_file, "toolbar_opacity", opacity + 1);
+ rcw_update_toolbar_opacity(cnnwin);
+ return TRUE;
+ }
+ break;
+#if GTK_CHECK_VERSION(3, 4, 0)
+ case GDK_SCROLL_SMOOTH:
+ if (event->delta_y < 0 && opacity > 0) {
+ remmina_file_set_int(cnnobj->remmina_file, "toolbar_opacity", opacity - 1);
+ rcw_update_toolbar_opacity(cnnwin);
+ return TRUE;
+ }
+ if (event->delta_y > 0 && opacity < TOOLBAR_OPACITY_LEVEL) {
+ remmina_file_set_int(cnnobj->remmina_file, "toolbar_opacity", opacity + 1);
+ rcw_update_toolbar_opacity(cnnwin);
+ return TRUE;
+ }
+ break;
+#endif
+ default:
+ break;
+ }
+ return TRUE;
+}
+
+static gboolean rcw_after_configure_scrolled(gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gint width, height;
+ GdkWindowState s;
+ gint ipg, npages;
+ RemminaConnectionWindow *cnnwin;
+
+ cnnwin = (RemminaConnectionWindow *)user_data;
+
+ if (!cnnwin || !cnnwin->priv)
+ return FALSE;
+
+ s = gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(cnnwin)));
+
+
+ /* Changed window_maximize, window_width and window_height for all
+ * connections inside the notebook */
+ npages = gtk_notebook_get_n_pages(GTK_NOTEBOOK(cnnwin->priv->notebook));
+ for (ipg = 0; ipg < npages; ipg++) {
+ RemminaConnectionObject *cnnobj;
+ cnnobj = g_object_get_data(
+ G_OBJECT(gtk_notebook_get_nth_page(GTK_NOTEBOOK(cnnwin->priv->notebook), ipg)),
+ "cnnobj");
+ if (s & GDK_WINDOW_STATE_MAXIMIZED) {
+ remmina_file_set_int(cnnobj->remmina_file, "window_maximize", TRUE);
+ } else {
+ gtk_window_get_size(GTK_WINDOW(cnnobj->cnnwin), &width, &height);
+ remmina_file_set_int(cnnobj->remmina_file, "window_width", width);
+ remmina_file_set_int(cnnobj->remmina_file, "window_height", height);
+ remmina_file_set_int(cnnobj->remmina_file, "window_maximize", FALSE);
+ }
+ }
+ cnnwin->priv->acs_eventsourceid = 0;
+ return FALSE;
+}
+
+static gboolean rcw_on_configure(GtkWidget *widget, GdkEventConfigure *event,
+ gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindow *cnnwin;
+ RemminaConnectionObject *cnnobj;
+
+ if (!REMMINA_IS_CONNECTION_WINDOW(widget))
+ return FALSE;
+
+ cnnwin = (RemminaConnectionWindow *)widget;
+
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return FALSE;
+
+ if (cnnwin->priv->acs_eventsourceid) {
+ g_source_remove(cnnwin->priv->acs_eventsourceid);
+ cnnwin->priv->acs_eventsourceid = 0;
+ }
+
+ if (gtk_widget_get_window(GTK_WIDGET(cnnwin))
+ && cnnwin->priv->view_mode == SCROLLED_WINDOW_MODE)
+ /* Under GNOME Shell we receive this configure_event BEFORE a window
+ * is really unmaximized, so we must read its new state and dimensions
+ * later, not now */
+ cnnwin->priv->acs_eventsourceid = g_timeout_add(500, rcw_after_configure_scrolled, cnnwin);
+
+ if (cnnwin->priv->view_mode != SCROLLED_WINDOW_MODE)
+ /* Notify window of change so that scroll border can be hidden or shown if needed */
+ rco_check_resize(cnnobj);
+ return FALSE;
+}
+
+static void rcw_update_pin(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ if (cnnwin->priv->pin_down)
+ gtk_button_set_image(GTK_BUTTON(cnnwin->priv->pin_button),
+ gtk_image_new_from_icon_name("org.remmina.Remmina-pin-down-symbolic", GTK_ICON_SIZE_MENU));
+ else
+ gtk_button_set_image(GTK_BUTTON(cnnwin->priv->pin_button),
+ gtk_image_new_from_icon_name("org.remmina.Remmina-pin-up-symbolic", GTK_ICON_SIZE_MENU));
+}
+
+static void rcw_toolbar_pin(GtkWidget *widget, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ remmina_pref.toolbar_pin_down = cnnwin->priv->pin_down = !cnnwin->priv->pin_down;
+ remmina_pref_save();
+ rcw_update_pin(cnnwin);
+}
+
+static void rcw_create_floating_toolbar(RemminaConnectionWindow *cnnwin, gint mode)
+{
+ TRACE_CALL(__func__);
+
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+ GtkWidget *ftb_widget;
+ GtkWidget *vbox;
+ GtkWidget *hbox;
+ GtkWidget *label;
+ GtkWidget *pinbutton;
+ GtkWidget *tb;
+
+
+ /* A widget to be used for GtkOverlay for GTK >= 3.10 */
+ ftb_widget = gtk_event_box_new();
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_show(vbox);
+
+ gtk_container_add(GTK_CONTAINER(ftb_widget), vbox);
+
+ tb = rcw_create_toolbar(cnnwin, mode);
+ hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show(hbox);
+
+
+ /* The pin button */
+ pinbutton = gtk_button_new();
+ gtk_widget_show(pinbutton);
+ gtk_box_pack_start(GTK_BOX(hbox), pinbutton, FALSE, FALSE, 0);
+ gtk_button_set_relief(GTK_BUTTON(pinbutton), GTK_RELIEF_NONE);
+#if GTK_CHECK_VERSION(3, 20, 0)
+ gtk_widget_set_focus_on_click(GTK_WIDGET(pinbutton), FALSE);
+#else
+ gtk_button_set_focus_on_click(GTK_BUTTON(pinbutton), FALSE);
+#endif
+ gtk_widget_set_name(pinbutton, "remmina-pin-button");
+ g_signal_connect(G_OBJECT(pinbutton), "clicked", G_CALLBACK(rcw_toolbar_pin), cnnwin);
+ priv->pin_button = pinbutton;
+ priv->pin_down = remmina_pref.toolbar_pin_down;
+ rcw_update_pin(cnnwin);
+
+
+ label = gtk_label_new("");
+ gtk_label_set_max_width_chars(GTK_LABEL(label), 50);
+ gtk_widget_show(label);
+
+ gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);
+
+ priv->floating_toolbar_label = label;
+
+ if (remmina_pref.floating_toolbar_placement == FLOATING_TOOLBAR_PLACEMENT_BOTTOM) {
+ gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), tb, FALSE, FALSE, 0);
+ } else {
+ gtk_box_pack_start(GTK_BOX(vbox), tb, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
+ }
+
+ priv->floating_toolbar_widget = ftb_widget;
+ gtk_widget_show(ftb_widget);
+}
+
+static void rcw_toolbar_place_signal(RemminaConnectionWindow *cnnwin, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv;
+
+ priv = cnnwin->priv;
+ /* Detach old toolbar widget and reattach in new position in the grid */
+ if (priv->toolbar && priv->grid) {
+ g_object_ref(priv->toolbar);
+ gtk_container_remove(GTK_CONTAINER(priv->grid), priv->toolbar);
+ rcw_place_toolbar(GTK_TOOLBAR(priv->toolbar), GTK_GRID(priv->grid), GTK_WIDGET(priv->notebook), remmina_pref.toolbar_placement);
+ g_object_unref(priv->toolbar);
+ }
+}
+
+
+static void rcw_init(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv;
+
+ priv = g_new0(RemminaConnectionWindowPriv, 1);
+ cnnwin->priv = priv;
+
+ priv->view_mode = SCROLLED_WINDOW_MODE;
+ if (kioskmode && kioskmode == TRUE)
+ priv->view_mode = VIEWPORT_FULLSCREEN_MODE;
+
+ priv->floating_toolbar_opacity = 1.0;
+ priv->kbcaptured = FALSE;
+ priv->pointer_captured = FALSE;
+ priv->pointer_entered = FALSE;
+ priv->fss_view_mode = VIEWPORT_FULLSCREEN_MODE;
+ priv->ss_width = 640;
+ priv->ss_height = 480;
+ priv->ss_maximized = FALSE;
+
+ remmina_widget_pool_register(GTK_WIDGET(cnnwin));
+}
+
+static gboolean rcw_focus_in_event(GtkWidget *widget, GdkEventWindowState *event, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: RCW focus-in-event received\n");
+#endif
+ rcw_focus_in((RemminaConnectionWindow *)widget);
+ return FALSE;
+}
+
+static gboolean rcw_focus_out_event(GtkWidget *widget, GdkEventWindowState *event, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: RCW focus-out-event received\n");
+#endif
+ rcw_focus_out((RemminaConnectionWindow *)widget);
+ return FALSE;
+}
+
+
+static gboolean rcw_state_event(GtkWidget *widget, GdkEventWindowState *event, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ if (!REMMINA_IS_CONNECTION_WINDOW(widget))
+ return FALSE;
+
+#if DEBUG_KB_GRABBING
+ printf("DEBUG_KB_GRABBING: window-state-event received\n");
+#endif
+
+ if (event->changed_mask & GDK_WINDOW_STATE_FOCUSED) {
+ if (event->new_window_state & GDK_WINDOW_STATE_FOCUSED)
+ rcw_focus_in((RemminaConnectionWindow *)widget);
+ else
+ rcw_focus_out((RemminaConnectionWindow *)widget);
+ }
+
+ return FALSE;
+}
+
+static gboolean rcw_map_event(GtkWidget *widget, GdkEvent *event, gpointer data)
+{
+ TRACE_CALL(__func__);
+
+
+
+ RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)widget;
+ RemminaConnectionObject *cnnobj;
+ RemminaProtocolWidget *gp;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring) return FALSE;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return FALSE;
+
+ gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto);
+ REMMINA_DEBUG("Mapping: %s", gtk_widget_get_name(widget));
+ if (remmina_protocol_widget_map_event(gp))
+ REMMINA_DEBUG("Called plugin mapping function");
+ return FALSE;
+}
+
+static gboolean rcw_unmap_event(GtkWidget *widget, GdkEvent *event, gpointer data)
+{
+ TRACE_CALL(__func__);
+
+ RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)widget;
+ RemminaConnectionObject *cnnobj;
+ RemminaProtocolWidget *gp;
+
+ if (cnnwin->priv->toolbar_is_reconfiguring) return FALSE;
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return FALSE;
+
+ gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto);
+ REMMINA_DEBUG("Unmapping: %s", gtk_widget_get_name(widget));
+ if (remmina_protocol_widget_unmap_event(gp))
+ REMMINA_DEBUG("Called plugin mapping function");
+ return FALSE;
+}
+
+static gboolean rcw_map_event_fullscreen(GtkWidget *widget, GdkEvent *event, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+ gint target_monitor;
+
+ REMMINA_DEBUG("Mapping: %s", gtk_widget_get_name(widget));
+
+ if (!REMMINA_IS_CONNECTION_WINDOW(widget)) {
+ REMMINA_DEBUG("Remmina Connection Window undefined, cannot go fullscreen");
+ return FALSE;
+ }
+
+ //RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)data;
+ cnnobj = rcw_get_visible_cnnobj((RemminaConnectionWindow *)widget);
+ //cnnobj = g_object_get_data(G_OBJECT(widget), "cnnobj");
+ if (!cnnobj) {
+ REMMINA_DEBUG("Remmina Connection Object undefined, cannot go fullscreen");
+ return FALSE;
+ }
+
+ RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(cnnobj->proto);
+
+ if (!gp)
+ REMMINA_DEBUG("Remmina Protocol Widget undefined, cannot go fullscreen");
+
+ if (remmina_protocol_widget_get_multimon(gp) >= 1) {
+ REMMINA_DEBUG("Fullscreen on all monitor");
+ gdk_window_set_fullscreen_mode(gtk_widget_get_window(widget), GDK_FULLSCREEN_ON_ALL_MONITORS);
+ gdk_window_fullscreen(gtk_widget_get_window(widget));
+ return TRUE;
+ } else {
+ REMMINA_DEBUG("Fullscreen on one monitor");
+ }
+
+ target_monitor = GPOINTER_TO_INT(data);
+
+#if GTK_CHECK_VERSION(3, 18, 0)
+ if (remmina_pref.fullscreen_on_auto) {
+ if (target_monitor == FULL_SCREEN_TARGET_MONITOR_UNDEFINED)
+ gtk_window_fullscreen(GTK_WINDOW(widget));
+ else
+ gtk_window_fullscreen_on_monitor(GTK_WINDOW(widget), gtk_window_get_screen(GTK_WINDOW(widget)),
+ target_monitor);
+ } else {
+ REMMINA_DEBUG("Fullscreen managed by WM or by the user, as per settings");
+ gtk_window_fullscreen(GTK_WINDOW(widget));
+ }
+#else
+ REMMINA_DEBUG("Cannot fullscreen on a specific monitor, feature available from GTK 3.18");
+ gtk_window_fullscreen(GTK_WINDOW(widget));
+#endif
+
+ if (remmina_protocol_widget_map_event(gp))
+ REMMINA_DEBUG("Called plugin mapping function");
+
+ return FALSE;
+}
+
+static RemminaConnectionWindow *
+rcw_new(gboolean fullscreen, int full_screen_target_monitor)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindow *cnnwin;
+
+ cnnwin = RCW(g_object_new(REMMINA_TYPE_CONNECTION_WINDOW, NULL));
+ cnnwin->priv->on_delete_confirm_mode = RCW_ONDELETE_CONFIRM_IF_2_OR_MORE;
+
+ if (fullscreen)
+ /* Put the window in fullscreen after it is mapped to have it appear on the same monitor */
+ g_signal_connect(G_OBJECT(cnnwin), "map-event", G_CALLBACK(rcw_map_event_fullscreen), GINT_TO_POINTER(full_screen_target_monitor));
+ else
+ g_signal_connect(G_OBJECT(cnnwin), "map-event", G_CALLBACK(rcw_map_event), NULL);
+ g_signal_connect(G_OBJECT(cnnwin), "unmap-event", G_CALLBACK(rcw_unmap_event), NULL);
+
+ gtk_container_set_border_width(GTK_CONTAINER(cnnwin), 0);
+ g_signal_connect(G_OBJECT(cnnwin), "toolbar-place", G_CALLBACK(rcw_toolbar_place_signal), NULL);
+
+ g_signal_connect(G_OBJECT(cnnwin), "delete-event", G_CALLBACK(rcw_delete_event), NULL);
+ g_signal_connect(G_OBJECT(cnnwin), "destroy", G_CALLBACK(rcw_destroy), NULL);
+
+ /* Under Xorg focus-in-event and focus-out-event don’t work when keyboard is grabbed
+ * via gdk_device_grab. So we listen for window-state-event to detect focus in and focus out.
+ * But we must also listen focus-in-event and focus-out-event because some
+ * window managers missing _NET_WM_STATE_FOCUSED hint, does not update the window state
+ * in case of focus change */
+ g_signal_connect(G_OBJECT(cnnwin), "window-state-event", G_CALLBACK(rcw_state_event), NULL);
+ g_signal_connect(G_OBJECT(cnnwin), "focus-in-event", G_CALLBACK(rcw_focus_in_event), NULL);
+ g_signal_connect(G_OBJECT(cnnwin), "focus-out-event", G_CALLBACK(rcw_focus_out_event), NULL);
+
+ g_signal_connect(G_OBJECT(cnnwin), "enter-notify-event", G_CALLBACK(rcw_on_enter_notify_event), NULL);
+ g_signal_connect(G_OBJECT(cnnwin), "leave-notify-event", G_CALLBACK(rcw_on_leave_notify_event), NULL);
+
+
+ g_signal_connect(G_OBJECT(cnnwin), "configure_event", G_CALLBACK(rcw_on_configure), NULL);
+
+ return cnnwin;
+}
+
+/* This function will be called for the first connection. A tag is set to the window so that
+ * other connections can determine if whether a new tab should be append to the same window
+ */
+static void rcw_update_tag(RemminaConnectionWindow *cnnwin, RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ gchar *tag;
+
+ switch (remmina_pref.tab_mode) {
+ case REMMINA_TAB_BY_GROUP:
+ tag = g_strdup(remmina_file_get_string(cnnobj->remmina_file, "group"));
+ break;
+ case REMMINA_TAB_BY_PROTOCOL:
+ tag = g_strdup(remmina_file_get_string(cnnobj->remmina_file, "protocol"));
+ break;
+ default:
+ tag = NULL;
+ break;
+ }
+ g_object_set_data_full(G_OBJECT(cnnwin), "tag", tag, (GDestroyNotify)g_free);
+}
+
+void rcw_grab_focus(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ if (!(cnnobj = rcw_get_visible_cnnobj(cnnwin))) return;
+
+ if (GTK_IS_WIDGET(cnnobj->proto))
+ remmina_protocol_widget_grab_focus(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+}
+
+static GtkWidget *nb_find_page_by_cnnobj(GtkNotebook *notebook, RemminaConnectionObject *cnnobj)
+{
+ gint i, np;
+ GtkWidget *found_page, *pg;
+
+ if (cnnobj == NULL || cnnobj->cnnwin == NULL || cnnobj->cnnwin->priv == NULL)
+ return NULL;
+ found_page = NULL;
+ np = gtk_notebook_get_n_pages(cnnobj->cnnwin->priv->notebook);
+ for (i = 0; i < np; i++) {
+ pg = gtk_notebook_get_nth_page(cnnobj->cnnwin->priv->notebook, i);
+ if (g_object_get_data(G_OBJECT(pg), "cnnobj") == cnnobj) {
+ found_page = pg;
+ break;
+ }
+ }
+
+ return found_page;
+}
+
+
+void rco_closewin(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj = gp->cnnobj;
+ GtkWidget *page_to_remove;
+
+
+ if (cnnobj && cnnobj->scrolled_container && REMMINA_IS_SCROLLED_VIEWPORT(cnnobj->scrolled_container)) {
+ REMMINA_DEBUG("deleting motion");
+ remmina_scrolled_viewport_remove_motion(REMMINA_SCROLLED_VIEWPORT(cnnobj->scrolled_container));
+ }
+
+ if (cnnobj && cnnobj->cnnwin) {
+ page_to_remove = nb_find_page_by_cnnobj(cnnobj->cnnwin->priv->notebook, cnnobj);
+ if (page_to_remove) {
+ gtk_notebook_remove_page(
+ cnnobj->cnnwin->priv->notebook,
+ gtk_notebook_page_num(cnnobj->cnnwin->priv->notebook, page_to_remove));
+ /* Invalidate pointers to objects destroyed by page removal */
+ cnnobj->aspectframe = NULL;
+ cnnobj->viewport = NULL;
+ cnnobj->scrolled_container = NULL;
+ /* we cannot invalidate cnnobj->proto, because it can be already been
+ * detached from the widget hierarchy in rco_on_disconnect() */
+ }
+ }
+ if (cnnobj) {
+ cnnobj->remmina_file = NULL;
+ g_free(cnnobj);
+ gp->cnnobj = NULL;
+ }
+
+ remmina_application_condexit(REMMINA_CONDEXIT_ONDISCONNECT);
+}
+
+void rco_on_close_button_clicked(GtkButton *button, RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ if (REMMINA_IS_PROTOCOL_WIDGET(cnnobj->proto)) {
+ if (!remmina_protocol_widget_is_closed((RemminaProtocolWidget *)cnnobj->proto))
+ remmina_protocol_widget_close_connection(REMMINA_PROTOCOL_WIDGET(cnnobj->proto));
+ else
+ rco_closewin((RemminaProtocolWidget *)cnnobj->proto);
+ }
+}
+
+static GtkWidget *rco_create_tab_label(RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *hbox;
+ GtkWidget *widget;
+ GtkWidget *button;
+
+ hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
+ gtk_widget_show(hbox);
+
+ widget = gtk_image_new_from_icon_name(remmina_file_get_icon_name(cnnobj->remmina_file), GTK_ICON_SIZE_MENU);
+ gtk_widget_show(widget);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, FALSE, 0);
+
+ widget = gtk_label_new(remmina_file_get_string(cnnobj->remmina_file, "name"));
+ gtk_widget_set_valign(widget, GTK_ALIGN_CENTER);
+ gtk_widget_set_halign(widget, GTK_ALIGN_CENTER);
+
+ gtk_widget_show(widget);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0);
+
+ button = gtk_button_new(); // The "x" to close the tab
+ gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
+#if GTK_CHECK_VERSION(3, 20, 0)
+ gtk_widget_set_focus_on_click(GTK_WIDGET(widget), FALSE);
+#else
+ gtk_button_set_focus_on_click(GTK_BUTTON(button), FALSE);
+#endif
+ gtk_widget_set_name(button, "remmina-small-button");
+ gtk_widget_show(button);
+
+ widget = gtk_image_new_from_icon_name("window-close", GTK_ICON_SIZE_MENU);
+ gtk_widget_show(widget);
+ gtk_container_add(GTK_CONTAINER(button), widget);
+
+ gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
+
+ g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(rco_on_close_button_clicked), cnnobj);
+
+
+ return hbox;
+}
+
+static GtkWidget *rco_create_tab_page(RemminaConnectionObject *cnnobj)
+{
+ GtkWidget *page;
+
+ page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_name(page, "remmina-tab-page");
+
+
+ return page;
+}
+
+static GtkWidget *rcw_append_new_page(RemminaConnectionWindow *cnnwin, RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *page, *label;
+ GtkNotebook *notebook;
+
+ notebook = cnnwin->priv->notebook;
+
+ page = rco_create_tab_page(cnnobj);
+ g_object_set_data(G_OBJECT(page), "cnnobj", cnnobj);
+ label = rco_create_tab_label(cnnobj);
+
+ cnnobj->cnnwin = cnnwin;
+
+ gtk_notebook_append_page(notebook, page, label);
+ gtk_notebook_set_tab_reorderable(notebook, page, TRUE);
+ gtk_notebook_set_tab_detachable(notebook, page, TRUE);
+ /* This trick prevents the tab label from being focused */
+ gtk_widget_set_can_focus(gtk_widget_get_parent(label), FALSE);
+
+ if (gtk_widget_get_parent(cnnobj->scrolled_container) != NULL)
+ printf("REMMINA WARNING in %s: scrolled_container already has a parent\n", __func__);
+ gtk_box_pack_start(GTK_BOX(page), cnnobj->scrolled_container, TRUE, TRUE, 0);
+
+ gtk_widget_show(page);
+
+ return page;
+}
+
+
+static void rcw_update_notebook(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ GtkNotebook *notebook;
+ gint n;
+
+ notebook = GTK_NOTEBOOK(cnnwin->priv->notebook);
+
+ switch (cnnwin->priv->view_mode) {
+ case SCROLLED_WINDOW_MODE:
+ n = gtk_notebook_get_n_pages(notebook);
+ gtk_notebook_set_show_tabs(notebook, remmina_pref.always_show_tab ? TRUE : n > 1);
+ gtk_notebook_set_show_border(notebook, remmina_pref.always_show_tab ? TRUE : n > 1);
+ break;
+ default:
+ gtk_notebook_set_show_tabs(notebook, FALSE);
+ gtk_notebook_set_show_border(notebook, FALSE);
+ break;
+ }
+}
+
+static gboolean rcw_on_switch_page_finalsel(gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv;
+ RemminaConnectionObject *cnnobj;
+
+ if (!user_data)
+ return FALSE;
+
+ cnnobj = (RemminaConnectionObject *)user_data;
+ if (!cnnobj->cnnwin)
+ return FALSE;
+
+ priv = cnnobj->cnnwin->priv;
+
+ if (GTK_IS_WIDGET(cnnobj->cnnwin)) {
+ rcw_floating_toolbar_show(cnnobj->cnnwin, TRUE);
+ if (!priv->hidetb_eventsource)
+ priv->hidetb_eventsource = g_timeout_add(TB_HIDE_TIME_TIME, (GSourceFunc)
+ rcw_floating_toolbar_hide, cnnobj->cnnwin);
+ rco_update_toolbar(cnnobj);
+ rcw_grab_focus(cnnobj->cnnwin);
+ if (priv->view_mode != SCROLLED_WINDOW_MODE)
+ rco_check_resize(cnnobj);
+ }
+ priv->spf_eventsourceid = 0;
+ return FALSE;
+}
+
+static void rcw_on_switch_page(GtkNotebook *notebook, GtkWidget *newpage, guint page_num,
+ RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindowPriv *priv = cnnwin->priv;
+ RemminaConnectionObject *cnnobj_newpage;
+
+ cnnobj_newpage = g_object_get_data(G_OBJECT(newpage), "cnnobj");
+ if (priv->spf_eventsourceid)
+ g_source_remove(priv->spf_eventsourceid);
+ priv->spf_eventsourceid = g_idle_add(rcw_on_switch_page_finalsel, cnnobj_newpage);
+}
+
+static void rcw_on_page_added(GtkNotebook *notebook, GtkWidget *child, guint page_num,
+ RemminaConnectionWindow *cnnwin)
+{
+ if (gtk_notebook_get_n_pages(GTK_NOTEBOOK(cnnwin->priv->notebook)) > 0)
+ rcw_update_notebook(cnnwin);
+}
+
+static void rcw_on_page_removed(GtkNotebook *notebook, GtkWidget *child, guint page_num,
+ RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+
+ if (gtk_notebook_get_n_pages(GTK_NOTEBOOK(cnnwin->priv->notebook)) <= 0)
+ gtk_widget_destroy(GTK_WIDGET(cnnwin));
+
+}
+
+static GtkNotebook *
+rcw_on_notebook_create_window(GtkNotebook *notebook, GtkWidget *page, gint x, gint y, gpointer data)
+{
+ /* This signal callback is called by GTK when a detachable tab is dropped on the root window
+ * or in an existing window */
+
+ TRACE_CALL(__func__);
+ RemminaConnectionWindow *srccnnwin;
+ RemminaConnectionWindow *dstcnnwin;
+ RemminaConnectionObject *cnnobj;
+ GdkWindow *window;
+ gchar *srctag;
+ gint width, height;
+
+#if GTK_CHECK_VERSION(3, 20, 0)
+ GdkSeat *seat;
+#else
+ GdkDeviceManager *manager;
+#endif
+ GdkDevice *device = NULL;
+
+#if GTK_CHECK_VERSION(3, 20, 0)
+ seat = gdk_display_get_default_seat(gdk_display_get_default());
+ device = gdk_seat_get_pointer(seat);
+#else
+ manager = gdk_display_get_device_manager(gdk_display_get_default());
+ device = gdk_device_manager_get_client_pointer(manager);
+#endif
+
+ window = gdk_device_get_window_at_position(device, &x, &y);
+ srccnnwin = RCW(gtk_widget_get_toplevel(GTK_WIDGET(notebook)));
+ dstcnnwin = RCW(remmina_widget_pool_find_by_window(REMMINA_TYPE_CONNECTION_WINDOW, window));
+
+ if (srccnnwin == dstcnnwin)
+ return NULL;
+
+ if (gtk_notebook_get_n_pages(GTK_NOTEBOOK(srccnnwin->priv->notebook)) == 1 && !dstcnnwin)
+ return NULL;
+
+ cnnobj = (RemminaConnectionObject *)g_object_get_data(G_OBJECT(page), "cnnobj");
+
+ if (!dstcnnwin) {
+ /* Drop is directed to a new rcw: create a new scrolled window to accommodate
+ * the dropped connectionand move our cnnobj there. Width and
+ * height of the new window are cloned from the current window */
+ srctag = (gchar *)g_object_get_data(G_OBJECT(srccnnwin), "tag");
+ gtk_window_get_size(GTK_WINDOW(srccnnwin), &width, &height);
+ dstcnnwin = rcw_create_scrolled(width, height, FALSE); // New dropped window is never maximized
+ g_object_set_data_full(G_OBJECT(dstcnnwin), "tag", g_strdup(srctag), (GDestroyNotify)g_free);
+ /* when returning, GTK will move the whole tab to the new notebook.
+ * Prepare cnnobj to be hosted in the new cnnwin */
+ cnnobj->cnnwin = dstcnnwin;
+ } else {
+ cnnobj->cnnwin = dstcnnwin;
+ }
+
+ remmina_protocol_widget_set_hostkey_func(REMMINA_PROTOCOL_WIDGET(cnnobj->proto),
+ (RemminaHostkeyFunc)rcw_hostkey_func);
+
+ return GTK_NOTEBOOK(cnnobj->cnnwin->priv->notebook);
+}
+
+static GtkNotebook *
+rcw_create_notebook(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ GtkNotebook *notebook;
+
+ notebook = GTK_NOTEBOOK(gtk_notebook_new());
+
+ gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook), TRUE);
+ gtk_widget_show(GTK_WIDGET(notebook));
+
+ g_signal_connect(G_OBJECT(notebook), "create-window", G_CALLBACK(rcw_on_notebook_create_window), NULL);
+ g_signal_connect(G_OBJECT(notebook), "switch-page", G_CALLBACK(rcw_on_switch_page), cnnwin);
+ g_signal_connect(G_OBJECT(notebook), "page-added", G_CALLBACK(rcw_on_page_added), cnnwin);
+ g_signal_connect(G_OBJECT(notebook), "page-removed", G_CALLBACK(rcw_on_page_removed), cnnwin);
+ gtk_widget_set_can_focus(GTK_WIDGET(notebook), FALSE);
+
+ return notebook;
+}
+
+/* Create a scrolled toplevel window */
+static RemminaConnectionWindow *rcw_create_scrolled(gint width, gint height, gboolean maximize)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindow *cnnwin;
+ GtkWidget *grid;
+ GtkWidget *toolbar;
+ GtkNotebook *notebook;
+ GtkSettings *settings = gtk_settings_get_default();
+
+ cnnwin = rcw_new(FALSE, 0);
+ gtk_widget_realize(GTK_WIDGET(cnnwin));
+
+ gtk_window_set_default_size(GTK_WINDOW(cnnwin), width, height);
+ g_object_set(settings, "gtk-application-prefer-dark-theme", remmina_pref.dark_theme, NULL);
+
+ /* Create the toolbar */
+ toolbar = rcw_create_toolbar(cnnwin, SCROLLED_WINDOW_MODE);
+
+ /* Create the notebook */
+ notebook = rcw_create_notebook(cnnwin);
+
+ /* Create the grid container for toolbars+notebook and populate it */
+ grid = gtk_grid_new();
+
+
+ gtk_grid_attach(GTK_GRID(grid), GTK_WIDGET(notebook), 0, 0, 1, 1);
+
+ gtk_widget_set_hexpand(GTK_WIDGET(notebook), TRUE);
+ gtk_widget_set_vexpand(GTK_WIDGET(notebook), TRUE);
+
+ rcw_place_toolbar(GTK_TOOLBAR(toolbar), GTK_GRID(grid), GTK_WIDGET(notebook), remmina_pref.toolbar_placement);
+
+ gtk_container_add(GTK_CONTAINER(cnnwin), grid);
+
+ /* Add drag capabilities to the toolbar */
+ gtk_drag_source_set(GTK_WIDGET(toolbar), GDK_BUTTON1_MASK,
+ dnd_targets_tb, sizeof dnd_targets_tb / sizeof *dnd_targets_tb, GDK_ACTION_MOVE);
+ g_signal_connect_after(GTK_WIDGET(toolbar), "drag-begin", G_CALLBACK(rcw_tb_drag_begin), NULL);
+ g_signal_connect(GTK_WIDGET(toolbar), "drag-failed", G_CALLBACK(rcw_tb_drag_failed), cnnwin);
+
+ /* Add drop capabilities to the drop/dest target for the toolbar (the notebook) */
+ gtk_drag_dest_set(GTK_WIDGET(notebook), GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT,
+ dnd_targets_tb, sizeof dnd_targets_tb / sizeof *dnd_targets_tb, GDK_ACTION_MOVE);
+ gtk_drag_dest_set_track_motion(GTK_WIDGET(notebook), TRUE);
+ g_signal_connect(GTK_WIDGET(notebook), "drag-drop", G_CALLBACK(rcw_tb_drag_drop), cnnwin);
+
+ cnnwin->priv->view_mode = SCROLLED_WINDOW_MODE;
+ cnnwin->priv->toolbar = toolbar;
+ cnnwin->priv->grid = grid;
+ cnnwin->priv->notebook = notebook;
+ cnnwin->priv->ss_width = width;
+ cnnwin->priv->ss_height = height;
+ cnnwin->priv->ss_maximized = maximize;
+
+ /* The notebook and all its child must be realized now, or a reparent will
+ * call unrealize() and will destroy a GtkSocket */
+ gtk_widget_show(grid);
+ gtk_widget_show(GTK_WIDGET(cnnwin));
+ GtkWindowGroup *wingrp = gtk_window_group_new();
+
+ gtk_window_group_add_window(wingrp, GTK_WINDOW(cnnwin));
+ gtk_window_set_transient_for(GTK_WINDOW(cnnwin), NULL);
+
+ if (maximize)
+ gtk_window_maximize(GTK_WINDOW(cnnwin));
+
+
+ rcw_set_toolbar_visibility(cnnwin);
+
+ return cnnwin;
+}
+
+static void rcw_create_overlay_ftb_overlay(RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+
+ GtkWidget *revealer;
+ RemminaConnectionWindowPriv *priv;
+
+ priv = cnnwin->priv;
+
+ if (priv->overlay_ftb_overlay != NULL) {
+ gtk_widget_destroy(priv->overlay_ftb_overlay);
+ priv->overlay_ftb_overlay = NULL;
+ priv->revealer = NULL;
+ }
+ if (priv->overlay_ftb_fr != NULL) {
+ gtk_widget_destroy(priv->overlay_ftb_fr);
+ priv->overlay_ftb_fr = NULL;
+ }
+
+ rcw_create_floating_toolbar(cnnwin, priv->fss_view_mode);
+
+ priv->overlay_ftb_overlay = gtk_event_box_new();
+
+ GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+
+ gtk_container_set_border_width(GTK_CONTAINER(vbox), 0);
+
+ GtkWidget *handle = gtk_drawing_area_new();
+
+ gtk_widget_set_size_request(handle, 4, 4);
+ gtk_widget_set_name(handle, "ftb-handle");
+
+ revealer = gtk_revealer_new();
+
+ gtk_widget_set_halign(GTK_WIDGET(priv->overlay_ftb_overlay), GTK_ALIGN_CENTER);
+
+ if (remmina_pref.floating_toolbar_placement == FLOATING_TOOLBAR_PLACEMENT_BOTTOM) {
+ gtk_box_pack_start(GTK_BOX(vbox), handle, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), revealer, FALSE, FALSE, 0);
+ gtk_revealer_set_transition_type(GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP);
+ gtk_widget_set_valign(GTK_WIDGET(priv->overlay_ftb_overlay), GTK_ALIGN_END);
+ } else {
+ gtk_box_pack_start(GTK_BOX(vbox), revealer, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), handle, FALSE, FALSE, 0);
+ gtk_revealer_set_transition_type(GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
+ gtk_widget_set_valign(GTK_WIDGET(priv->overlay_ftb_overlay), GTK_ALIGN_START);
+ }
+
+
+ gtk_container_add(GTK_CONTAINER(revealer), priv->floating_toolbar_widget);
+ gtk_widget_set_halign(GTK_WIDGET(revealer), GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(GTK_WIDGET(revealer), GTK_ALIGN_START);
+
+ priv->revealer = revealer;
+
+ GtkWidget *fr;
+
+ fr = gtk_frame_new(NULL);
+ priv->overlay_ftb_fr = fr;
+ gtk_container_add(GTK_CONTAINER(priv->overlay_ftb_overlay), fr);
+ gtk_container_add(GTK_CONTAINER(fr), vbox);
+
+ gtk_widget_show(vbox);
+ gtk_widget_show(revealer);
+ gtk_widget_show(handle);
+ gtk_widget_show(priv->overlay_ftb_overlay);
+ gtk_widget_show(fr);
+
+ if (remmina_pref.floating_toolbar_placement == FLOATING_TOOLBAR_PLACEMENT_BOTTOM)
+ gtk_widget_set_name(fr, "ftbbox-lower");
+ else
+ gtk_widget_set_name(fr, "ftbbox-upper");
+
+ gtk_overlay_add_overlay(GTK_OVERLAY(priv->overlay), priv->overlay_ftb_overlay);
+
+ rcw_floating_toolbar_show(cnnwin, TRUE);
+
+ g_signal_connect(G_OBJECT(priv->overlay_ftb_overlay), "enter-notify-event", G_CALLBACK(rcw_floating_toolbar_on_enter), cnnwin);
+ g_signal_connect(G_OBJECT(priv->overlay_ftb_overlay), "leave-notify-event", G_CALLBACK(rcw_floating_toolbar_on_leave), cnnwin);
+ g_signal_connect(G_OBJECT(priv->overlay_ftb_overlay), "scroll-event", G_CALLBACK(rcw_floating_toolbar_on_scroll), cnnwin);
+ gtk_widget_add_events(
+ GTK_WIDGET(priv->overlay_ftb_overlay),
+ GDK_SCROLL_MASK
+#if GTK_CHECK_VERSION(3, 4, 0)
+ | GDK_SMOOTH_SCROLL_MASK
+#endif
+ );
+
+ /* Add drag and drop capabilities to the source */
+ gtk_drag_source_set(GTK_WIDGET(priv->overlay_ftb_overlay), GDK_BUTTON1_MASK,
+ dnd_targets_ftb, sizeof dnd_targets_ftb / sizeof *dnd_targets_ftb, GDK_ACTION_MOVE);
+ g_signal_connect_after(GTK_WIDGET(priv->overlay_ftb_overlay), "drag-begin", G_CALLBACK(rcw_ftb_drag_begin), cnnwin);
+
+ if (remmina_pref.fullscreen_toolbar_visibility == FLOATING_TOOLBAR_VISIBILITY_DISABLE)
+ /* toolbar in fullscreenmode disabled, hide everything */
+ gtk_widget_hide(fr);
+}
+
+
+static gboolean rcw_ftb_drag_drop(GtkWidget *widget, GdkDragContext *context,
+ gint x, gint y, guint time, RemminaConnectionWindow *cnnwin)
+{
+ TRACE_CALL(__func__);
+ GtkAllocation wa;
+ gint new_floating_toolbar_placement;
+ RemminaConnectionObject *cnnobj;
+
+ gtk_widget_get_allocation(widget, &wa);
+
+ if (y >= wa.height / 2)
+ new_floating_toolbar_placement = FLOATING_TOOLBAR_PLACEMENT_BOTTOM;
+ else
+ new_floating_toolbar_placement = FLOATING_TOOLBAR_PLACEMENT_TOP;
+
+ gtk_drag_finish(context, TRUE, TRUE, time);
+
+ if (new_floating_toolbar_placement != remmina_pref.floating_toolbar_placement) {
+ /* Destroy and recreate the FTB */
+ remmina_pref.floating_toolbar_placement = new_floating_toolbar_placement;
+ remmina_pref_save();
+ rcw_create_overlay_ftb_overlay(cnnwin);
+ cnnobj = rcw_get_visible_cnnobj(cnnwin);
+ if (cnnobj) rco_update_toolbar(cnnobj);
+ }
+
+ return TRUE;
+}
+
+static void rcw_ftb_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ cairo_surface_t *surface;
+ cairo_t *cr;
+ GtkAllocation wa;
+ double dashes[] = { 10 };
+
+ gtk_widget_get_allocation(widget, &wa);
+
+ surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, wa.width, wa.height);
+ cr = cairo_create(surface);
+ cairo_set_source_rgb(cr, 0.6, 0.6, 0.6);
+ cairo_set_line_width(cr, 2);
+ cairo_set_dash(cr, dashes, 1, 0);
+ cairo_rectangle(cr, 0, 0, wa.width, wa.height);
+ cairo_stroke(cr);
+ cairo_destroy(cr);
+
+ gtk_drag_set_icon_surface(context, surface);
+}
+
+RemminaConnectionWindow *rcw_create_fullscreen(GtkWindow *old, gint view_mode)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindow *cnnwin;
+ GtkNotebook *notebook;
+
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gint n_monitors;
+ gint i;
+ GdkMonitor *old_monitor;
+ GdkDisplay *old_display;
+ GdkWindow *old_window;
+#endif
+ gint full_screen_target_monitor;
+
+ full_screen_target_monitor = FULL_SCREEN_TARGET_MONITOR_UNDEFINED;
+ if (old) {
+#if GTK_CHECK_VERSION(3, 22, 0)
+ old_window = gtk_widget_get_window(GTK_WIDGET(old));
+ old_display = gdk_window_get_display(old_window);
+ old_monitor = gdk_display_get_monitor_at_window(old_display, old_window);
+ n_monitors = gdk_display_get_n_monitors(old_display);
+ for (i = 0; i < n_monitors; ++i) {
+ if (gdk_display_get_monitor(old_display, i) == old_monitor) {
+ full_screen_target_monitor = i;
+ break;
+ }
+ }
+#else
+ full_screen_target_monitor = gdk_screen_get_monitor_at_window(
+ gdk_screen_get_default(),
+ gtk_widget_get_window(GTK_WIDGET(old)));
+#endif
+ }
+
+ cnnwin = rcw_new(TRUE, full_screen_target_monitor);
+ gtk_widget_set_name(GTK_WIDGET(cnnwin), "remmina-connection-window-fullscreen");
+ gtk_widget_realize(GTK_WIDGET(cnnwin));
+
+ if (!view_mode)
+ view_mode = VIEWPORT_FULLSCREEN_MODE;
+
+ notebook = rcw_create_notebook(cnnwin);
+
+ cnnwin->priv->overlay = gtk_overlay_new();
+ gtk_container_add(GTK_CONTAINER(cnnwin), cnnwin->priv->overlay);
+ gtk_container_add(GTK_CONTAINER(cnnwin->priv->overlay), GTK_WIDGET(notebook));
+ gtk_widget_show(GTK_WIDGET(cnnwin->priv->overlay));
+
+ cnnwin->priv->notebook = notebook;
+ cnnwin->priv->view_mode = view_mode;
+ cnnwin->priv->fss_view_mode = view_mode;
+
+ /* Create the floating toolbar */
+ rcw_create_overlay_ftb_overlay(cnnwin);
+ /* Add drag and drop capabilities to the drop/dest target for floating toolbar */
+ gtk_drag_dest_set(GTK_WIDGET(cnnwin->priv->overlay), GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT,
+ dnd_targets_ftb, sizeof dnd_targets_ftb / sizeof *dnd_targets_ftb, GDK_ACTION_MOVE);
+ gtk_drag_dest_set_track_motion(GTK_WIDGET(cnnwin->priv->notebook), TRUE);
+ g_signal_connect(GTK_WIDGET(cnnwin->priv->overlay), "drag-drop", G_CALLBACK(rcw_ftb_drag_drop), cnnwin);
+
+ gtk_widget_show(GTK_WIDGET(cnnwin));
+ GtkWindowGroup *wingrp = gtk_window_group_new();
+ gtk_window_group_add_window(wingrp, GTK_WINDOW(cnnwin));
+ gtk_window_set_transient_for(GTK_WINDOW(cnnwin), NULL);
+
+ return cnnwin;
+}
+
+static gboolean rcw_hostkey_func(RemminaProtocolWidget *gp, guint keyval, gboolean release)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj = gp->cnnobj;
+ RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv;
+ const RemminaProtocolFeature *feature;
+ gint i;
+
+ if (release) {
+ if (remmina_pref.hostkey && keyval == remmina_pref.hostkey) {
+ priv->hostkey_activated = FALSE;
+ if (priv->hostkey_used)
+ /* hostkey pressed + something else */
+ return TRUE;
+ }
+ /* If hostkey is released without pressing other keys, we should execute the
+ * shortcut key which is the same as hostkey. Be default, this is grab/ungrab
+ * keyboard */
+ else if (priv->hostkey_activated) {
+ /* Trap all key releases when hostkey is pressed */
+ /* hostkey pressed + something else */
+ return TRUE;
+ } else {
+ /* Any key pressed, no hostkey */
+ return FALSE;
+ }
+ } else if (remmina_pref.hostkey && keyval == remmina_pref.hostkey) {
+ /** @todo Add callback for hostname transparent overlay #832 */
+ priv->hostkey_activated = TRUE;
+ priv->hostkey_used = FALSE;
+ return TRUE;
+ } else if (!priv->hostkey_activated) {
+ /* Any key pressed, no hostkey */
+ return FALSE;
+ }
+
+ priv->hostkey_used = TRUE;
+ keyval = gdk_keyval_to_lower(keyval);
+ if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Down
+ || keyval == GDK_KEY_Left || keyval == GDK_KEY_Right) {
+ GtkAdjustment *adjust;
+ int pos;
+
+ if (cnnobj->connected && GTK_IS_SCROLLED_WINDOW(cnnobj->scrolled_container)) {
+ if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Down)
+ adjust = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container));
+ else
+ adjust = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container));
+
+ if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Left)
+ pos = 0;
+ else
+ pos = gtk_adjustment_get_upper(adjust);
+
+ gtk_adjustment_set_value(adjust, pos);
+ if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Down)
+ gtk_scrolled_window_set_vadjustment(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container), adjust);
+ else
+ gtk_scrolled_window_set_hadjustment(GTK_SCROLLED_WINDOW(cnnobj->scrolled_container), adjust);
+ } else if (REMMINA_IS_SCROLLED_VIEWPORT(cnnobj->scrolled_container)) {
+ RemminaScrolledViewport *gsv;
+ GtkWidget *child;
+ GdkWindow *gsvwin;
+ gint sz;
+ GtkAdjustment *adj;
+ gdouble value;
+
+ if (!GTK_IS_BIN(cnnobj->scrolled_container))
+ return FALSE;
+
+ gsv = REMMINA_SCROLLED_VIEWPORT(cnnobj->scrolled_container);
+ child = gtk_bin_get_child(GTK_BIN(gsv));
+ if (!GTK_IS_VIEWPORT(child))
+ return FALSE;
+
+ gsvwin = gtk_widget_get_window(GTK_WIDGET(gsv));
+ if (!gsv)
+ return FALSE;
+
+ if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Down) {
+ sz = gdk_window_get_height(gsvwin) + 2; // Add 2px of black scroll border
+ adj = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(child));
+ } else {
+ sz = gdk_window_get_width(gsvwin) + 2; // Add 2px of black scroll border
+ adj = gtk_scrollable_get_hadjustment(GTK_SCROLLABLE(child));
+ }
+
+ if (keyval == GDK_KEY_Up || keyval == GDK_KEY_Left)
+ value = 0;
+ else
+ value = gtk_adjustment_get_upper(GTK_ADJUSTMENT(adj)) - (gdouble)sz + 2.0;
+
+ gtk_adjustment_set_value(GTK_ADJUSTMENT(adj), value);
+ }
+ }
+
+ if (keyval == remmina_pref.shortcutkey_fullscreen && !extrahardening) {
+ switch (priv->view_mode) {
+ case SCROLLED_WINDOW_MODE:
+ rcw_switch_viewmode(cnnobj->cnnwin, priv->fss_view_mode);
+ break;
+ case SCROLLED_FULLSCREEN_MODE:
+ case VIEWPORT_FULLSCREEN_MODE:
+ rcw_switch_viewmode(cnnobj->cnnwin, SCROLLED_WINDOW_MODE);
+ break;
+ default:
+ break;
+ }
+ } else if (keyval == remmina_pref.shortcutkey_autofit && !extrahardening) {
+ if (priv->toolitem_autofit && gtk_widget_is_sensitive(GTK_WIDGET(priv->toolitem_autofit)))
+ rcw_toolbar_autofit(GTK_TOOL_ITEM(gp), cnnobj->cnnwin);
+ } else if (keyval == remmina_pref.shortcutkey_nexttab && !extrahardening) {
+ i = gtk_notebook_get_current_page(GTK_NOTEBOOK(priv->notebook)) + 1;
+ if (i >= gtk_notebook_get_n_pages(GTK_NOTEBOOK(priv->notebook)))
+ i = 0;
+ gtk_notebook_set_current_page(GTK_NOTEBOOK(priv->notebook), i);
+ } else if (keyval == remmina_pref.shortcutkey_prevtab && !extrahardening) {
+ i = gtk_notebook_get_current_page(GTK_NOTEBOOK(priv->notebook)) - 1;
+ if (i < 0)
+ i = gtk_notebook_get_n_pages(GTK_NOTEBOOK(priv->notebook)) - 1;
+ gtk_notebook_set_current_page(GTK_NOTEBOOK(priv->notebook), i);
+ } else if (keyval == remmina_pref.shortcutkey_clipboard && !extrahardening) {
+ if (remmina_protocol_widget_plugin_receives_keystrokes(REMMINA_PROTOCOL_WIDGET(cnnobj->proto))) {
+ remmina_protocol_widget_send_clipboard((RemminaProtocolWidget*)cnnobj->proto, G_OBJECT(cnnobj->proto));
+ }
+ } else if (keyval == remmina_pref.shortcutkey_scale && !extrahardening) {
+ if (gtk_widget_is_sensitive(GTK_WIDGET(priv->toolitem_scale))) {
+ gtk_toggle_tool_button_set_active(
+ GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_scale),
+ !gtk_toggle_tool_button_get_active(
+ GTK_TOGGLE_TOOL_BUTTON(
+ priv->toolitem_scale)));
+ }
+ } else if (keyval == remmina_pref.shortcutkey_grab && !extrahardening) {
+ gtk_toggle_tool_button_set_active(
+ GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_grab),
+ !gtk_toggle_tool_button_get_active(
+ GTK_TOGGLE_TOOL_BUTTON(
+ priv->toolitem_grab)));
+ } else if (keyval == remmina_pref.shortcutkey_minimize && !extrahardening) {
+ rcw_toolbar_minimize(GTK_TOOL_ITEM(gp),
+ cnnobj->cnnwin);
+ } else if (keyval == remmina_pref.shortcutkey_viewonly && !extrahardening) {
+ remmina_file_set_int(cnnobj->remmina_file, "viewonly",
+ (remmina_file_get_int(cnnobj->remmina_file, "viewonly", 0)
+ == 0) ? 1 : 0);
+ } else if (keyval == remmina_pref.shortcutkey_screenshot && !extrahardening) {
+ rcw_toolbar_screenshot(GTK_TOOL_ITEM(gp),
+ cnnobj->cnnwin);
+ } else if (keyval == remmina_pref.shortcutkey_disconnect && !extrahardening) {
+ rco_disconnect_current_page(cnnobj);
+ } else if (keyval == remmina_pref.shortcutkey_toolbar && !extrahardening) {
+ if (priv->view_mode == SCROLLED_WINDOW_MODE) {
+ remmina_pref.hide_connection_toolbar =
+ !remmina_pref.hide_connection_toolbar;
+ rcw_set_toolbar_visibility(cnnobj->cnnwin);
+ }
+ } else {
+ for (feature =
+ remmina_protocol_widget_get_features(
+ REMMINA_PROTOCOL_WIDGET(
+ cnnobj->proto));
+ feature && feature->type;
+ feature++) {
+ if (feature->type
+ == REMMINA_PROTOCOL_FEATURE_TYPE_TOOL
+ && GPOINTER_TO_UINT(
+ feature->opt3)
+ == keyval) {
+ remmina_protocol_widget_call_feature_by_ref(
+ REMMINA_PROTOCOL_WIDGET(
+ cnnobj->proto),
+ feature);
+ break;
+ }
+ }
+ }
+ /* If a keypress makes the current cnnobj to move to another window,
+ * priv is now invalid. So we can no longer use priv here */
+ cnnobj->cnnwin->priv->hostkey_activated = FALSE;
+
+ /* Trap all keypresses when hostkey is pressed */
+ return TRUE;
+}
+
+static RemminaConnectionWindow *rcw_find(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ const gchar *tag;
+
+ switch (remmina_pref.tab_mode) {
+ case REMMINA_TAB_BY_GROUP:
+ tag = remmina_file_get_string(remminafile, "group");
+ break;
+ case REMMINA_TAB_BY_PROTOCOL:
+ tag = remmina_file_get_string(remminafile, "protocol");
+ break;
+ case REMMINA_TAB_ALL:
+ tag = NULL;
+ break;
+ case REMMINA_TAB_NONE:
+ default:
+ return NULL;
+ }
+ return RCW(remmina_widget_pool_find(REMMINA_TYPE_CONNECTION_WINDOW, tag));
+}
+
+gboolean rcw_delayed_window_present(gpointer user_data)
+{
+ RemminaConnectionWindow *cnnwin = (RemminaConnectionWindow *)user_data;
+
+ if (cnnwin) {
+ gtk_window_present_with_time(GTK_WINDOW(cnnwin), (guint32)(g_get_monotonic_time() / 1000));
+ rcw_grab_focus(cnnwin);
+ }
+ cnnwin->priv->dwp_eventsourceid = 0;
+ return G_SOURCE_REMOVE;
+}
+
+void rco_on_connect(RemminaProtocolWidget *gp, RemminaConnectionObject *cnnobj)
+{
+ TRACE_CALL(__func__);
+
+ REMMINA_DEBUG("Connect signal emitted");
+
+ /* This signal handler is called by a plugin when it’s correctly connected
+ * (and authenticated) */
+
+ cnnobj->connected = TRUE;
+
+ remmina_protocol_widget_set_hostkey_func(REMMINA_PROTOCOL_WIDGET(cnnobj->proto),
+ (RemminaHostkeyFunc)rcw_hostkey_func);
+
+ /** Remember recent list for quick connect, and save the current date
+ * in the last_used field.
+ */
+ if (remmina_file_get_filename(cnnobj->remmina_file) == NULL)
+ remmina_pref_add_recent(remmina_file_get_string(cnnobj->remmina_file, "protocol"),
+ remmina_file_get_string(cnnobj->remmina_file, "server"));
+ REMMINA_DEBUG("We save the last successful connection date");
+ //remmina_file_set_string(cnnobj->remmina_file, "last_success", last_success);
+ remmina_file_state_last_success(cnnobj->remmina_file);
+ //REMMINA_DEBUG("Last connection made on %s.", last_success);
+
+ REMMINA_DEBUG("Saving credentials");
+ /* Save credentials */
+ remmina_file_save(cnnobj->remmina_file);
+
+ if (cnnobj->cnnwin->priv->floating_toolbar_widget)
+ gtk_widget_show(cnnobj->cnnwin->priv->floating_toolbar_widget);
+
+ rco_update_toolbar(cnnobj);
+
+ REMMINA_DEBUG("Trying to present the window");
+ /* Try to present window */
+ cnnobj->cnnwin->priv->dwp_eventsourceid = g_timeout_add(200, rcw_delayed_window_present, (gpointer)cnnobj->cnnwin);
+}
+
+static void cb_lasterror_confirmed(void *cbdata, int btn)
+{
+ TRACE_CALL(__func__);
+ rco_closewin((RemminaProtocolWidget *)cbdata);
+}
+
+void rco_on_disconnect(RemminaProtocolWidget *gp, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj = gp->cnnobj;
+ RemminaConnectionWindowPriv *priv = cnnobj->cnnwin->priv;
+ GtkWidget *pparent;
+
+ REMMINA_DEBUG("Disconnect signal received on RemminaProtocolWidget");
+ /* Detach the protocol widget from the notebook now, or we risk that a
+ * window delete will destroy cnnobj->proto before we complete disconnection.
+ */
+ pparent = gtk_widget_get_parent(cnnobj->proto);
+ if (pparent != NULL) {
+ g_object_ref(cnnobj->proto);
+ gtk_container_remove(GTK_CONTAINER(pparent), cnnobj->proto);
+ }
+
+ cnnobj->connected = FALSE;
+
+ if (remmina_pref.save_view_mode) {
+ if (cnnobj->cnnwin)
+ remmina_file_set_int(cnnobj->remmina_file, "viewmode", cnnobj->cnnwin->priv->view_mode);
+ remmina_file_save(cnnobj->remmina_file);
+ }
+
+ rcw_kp_ungrab(cnnobj->cnnwin);
+ gtk_toggle_tool_button_set_active(
+ GTK_TOGGLE_TOOL_BUTTON(priv->toolitem_grab),
+ FALSE);
+
+ if (remmina_protocol_widget_has_error(gp)) {
+ /* We cannot close window immediately, but we must show a message panel */
+ RemminaMessagePanel *mp;
+ /* Destroy scrolled_container (and viewport) and all its children the plugin created
+ * on it, so they will not receive GUI signals */
+ if (cnnobj->scrolled_container) {
+ gtk_widget_destroy(cnnobj->scrolled_container);
+ cnnobj->scrolled_container = NULL;
+ }
+ cnnobj->viewport = NULL;
+ mp = remmina_message_panel_new();
+ remmina_message_panel_setup_message(mp, remmina_protocol_widget_get_error_message(gp), cb_lasterror_confirmed, gp);
+ rco_show_message_panel(gp->cnnobj, mp);
+ REMMINA_DEBUG("Could not disconnect");
+ } else {
+ rco_closewin(gp);
+ REMMINA_DEBUG("Disconnected");
+ }
+}
+
+void rco_on_desktop_resize(RemminaProtocolWidget *gp, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj = gp->cnnobj;
+
+ if (cnnobj && cnnobj->cnnwin && cnnobj->cnnwin->priv->view_mode != SCROLLED_WINDOW_MODE)
+ rco_check_resize(cnnobj);
+}
+
+void rco_on_update_align(RemminaProtocolWidget *gp, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj = gp->cnnobj;
+
+ remmina_protocol_widget_update_alignment(cnnobj);
+}
+
+void rco_on_lock_dynres(RemminaProtocolWidget *gp, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj = gp->cnnobj;
+
+ cnnobj->dynres_unlocked = FALSE;
+ rco_update_toolbar(cnnobj);
+}
+
+void rco_on_unlock_dynres(RemminaProtocolWidget *gp, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj = gp->cnnobj;
+
+ cnnobj->dynres_unlocked = TRUE;
+ rco_update_toolbar(cnnobj);
+}
+
+gboolean rcw_open_from_filename(const gchar *filename)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+ GtkWidget *dialog;
+
+ remminafile = remmina_file_manager_load_file(filename);
+ if (remminafile) {
+ if (remmina_file_get_int (remminafile, "profile-lock", FALSE)
+ && remmina_unlock_new(remmina_main_get_window()) == 0)
+ return FALSE;
+ rcw_open_from_file(remminafile);
+ return TRUE;
+ } else {
+ dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
+ _("The file “%s” is corrupted, unreadable, or could not be found."), filename);
+ g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_widget_show(dialog);
+ remmina_widget_pool_register(dialog);
+ return FALSE;
+ }
+}
+
+static gboolean open_connection_last_stage(gpointer user_data)
+{
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)user_data;
+
+ /* Now we have an allocated size for our RemminaProtocolWidget. We can proceed with the connection */
+ remmina_protocol_widget_update_remote_resolution(gp);
+ remmina_protocol_widget_open_connection(gp);
+ rco_check_resize(gp->cnnobj);
+
+ return FALSE;
+}
+
+static void rpw_size_allocated_on_connection(GtkWidget *w, GdkRectangle *allocation, gpointer user_data)
+{
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)w;
+
+ /* Disconnect signal handler to avoid to be called again after a normal resize */
+ g_signal_handler_disconnect(w, gp->cnnobj->deferred_open_size_allocate_handler);
+
+ /* Allow extra 100 ms for size allocation (do we really need it?) */
+ g_timeout_add(100, open_connection_last_stage, gp);
+
+ return;
+}
+
+void rcw_open_from_file(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ rcw_open_from_file_full(remminafile, NULL, NULL, NULL);
+}
+
+static void set_label_selectable(gpointer data, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget = GTK_WIDGET(data);
+
+ if (GTK_IS_LABEL(widget))
+ gtk_label_set_selectable(GTK_LABEL(widget), TRUE);
+}
+
+/**
+ * @brief These define the response id's of the
+ * gtksocket-is-not-available-warning-dialog buttons.
+ */
+enum GTKSOCKET_NOT_AVAIL_RESPONSE_TYPE {
+ GTKSOCKET_NOT_AVAIL_RESPONSE_OPEN_BROWSER = 0,
+ GTKSOCKET_NOT_AVAIL_RESPONSE_NUM
+};
+
+/**
+ * @brief Gets called if the user interacts with the
+ * gtksocket-is-not-available-warning-dialog
+ */
+static void rcw_gtksocket_not_available_dialog_response(GtkDialog * self,
+ gint response_id,
+ RemminaConnectionObject * cnnobj)
+{
+ TRACE_CALL(__func__);
+
+ GError *error = NULL;
+
+ if (response_id == GTKSOCKET_NOT_AVAIL_RESPONSE_OPEN_BROWSER) {
+ gtk_show_uri_on_window(
+ NULL,
+ // TRANSLATORS: This should be a link to the Remmina wiki page:
+ // TRANSLATORS: 'GtkSocket feature is not available'.
+ _("https://gitlab.com/Remmina/Remmina/-/wikis/GtkSocket-feature-is-not-available-in-a-Wayland-session"),
+ GDK_CURRENT_TIME, &error
+ );
+ }
+
+ // Close the current page since it's useless without GtkSocket.
+ // The user would need to manually click the close button.
+ if (cnnobj) rco_disconnect_current_page(cnnobj);
+
+ gtk_widget_destroy(GTK_WIDGET(self));
+}
+
+GtkWidget *rcw_open_from_file_full(RemminaFile *remminafile, GCallback disconnect_cb, gpointer data, guint *handler)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionObject *cnnobj;
+
+ gint ret;
+ GtkWidget *dialog;
+ GtkWidget *newpage;
+ gint width, height;
+ gboolean maximize;
+ gint view_mode;
+ const gchar *msg;
+ RemminaScaleMode scalemode;
+
+ if (disconnect_cb) {
+ g_print("disconnect_cb is deprecated inside rcw_open_from_file_full() and should be null\n");
+ return NULL;
+ }
+
+
+ /* Create the RemminaConnectionObject */
+ cnnobj = g_new0(RemminaConnectionObject, 1);
+ cnnobj->remmina_file = remminafile;
+
+ /* Create the RemminaProtocolWidget */
+ cnnobj->proto = remmina_protocol_widget_new();
+ remmina_protocol_widget_setup((RemminaProtocolWidget *)cnnobj->proto, remminafile, cnnobj);
+ if (remmina_protocol_widget_has_error((RemminaProtocolWidget *)cnnobj->proto)) {
+ GtkWindow *wparent;
+ wparent = remmina_main_get_window();
+ msg = remmina_protocol_widget_get_error_message((RemminaProtocolWidget *)cnnobj->proto);
+ dialog = gtk_message_dialog_new(wparent, GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", msg);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ /* We should destroy cnnobj->proto and cnnobj now… TODO: Fix this leak */
+ return NULL;
+ }
+
+
+
+ /* Set a name for the widget, for CSS selector */
+ gtk_widget_set_name(GTK_WIDGET(cnnobj->proto), "remmina-protocol-widget");
+
+ gtk_widget_set_halign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL);
+ gtk_widget_set_valign(GTK_WIDGET(cnnobj->proto), GTK_ALIGN_FILL);
+
+ if (data)
+ g_object_set_data(G_OBJECT(cnnobj->proto), "user-data", data);
+
+ view_mode = remmina_file_get_int(cnnobj->remmina_file, "viewmode", 0);
+ if (kioskmode)
+ view_mode = VIEWPORT_FULLSCREEN_MODE;
+ gint ismultimon = remmina_file_get_int(cnnobj->remmina_file, "multimon", 0);
+
+ if (ismultimon)
+ view_mode = VIEWPORT_FULLSCREEN_MODE;
+
+ if (fullscreen)
+ view_mode = VIEWPORT_FULLSCREEN_MODE;
+
+ /* Create the viewport to make the RemminaProtocolWidget scrollable */
+ cnnobj->viewport = gtk_viewport_new(NULL, NULL);
+ gtk_widget_set_name(cnnobj->viewport, "remmina-cw-viewport");
+ gtk_widget_show(cnnobj->viewport);
+ gtk_container_set_border_width(GTK_CONTAINER(cnnobj->viewport), 0);
+ gtk_viewport_set_shadow_type(GTK_VIEWPORT(cnnobj->viewport), GTK_SHADOW_NONE);
+
+ /* Create the scrolled container */
+ scalemode = get_current_allowed_scale_mode(cnnobj, NULL, NULL);
+ cnnobj->scrolled_container = rco_create_scrolled_container(scalemode, view_mode);
+
+ gtk_container_add(GTK_CONTAINER(cnnobj->scrolled_container), cnnobj->viewport);
+
+ /* Determine whether the plugin can scale or not. If the plugin can scale and we do
+ * not want to expand, then we add a GtkAspectFrame to maintain aspect ratio during scaling */
+ cnnobj->plugin_can_scale = remmina_plugin_manager_query_feature_by_type(REMMINA_PLUGIN_TYPE_PROTOCOL,
+ remmina_file_get_string(remminafile, "protocol"),
+ REMMINA_PROTOCOL_FEATURE_TYPE_SCALE);
+
+ cnnobj->aspectframe = NULL;
+ gtk_container_add(GTK_CONTAINER(cnnobj->viewport), cnnobj->proto);
+
+ /* Determine whether this connection will be put on a new window
+ * or in an existing one */
+ cnnobj->cnnwin = rcw_find(remminafile);
+ if (!cnnobj->cnnwin) {
+ /* Connection goes on a new toplevel window */
+ switch (view_mode) {
+ case SCROLLED_FULLSCREEN_MODE:
+ case VIEWPORT_FULLSCREEN_MODE:
+ cnnobj->cnnwin = rcw_create_fullscreen(NULL, view_mode);
+ break;
+ case SCROLLED_WINDOW_MODE:
+ default:
+ width = remmina_file_get_int(cnnobj->remmina_file, "window_width", 640);
+ height = remmina_file_get_int(cnnobj->remmina_file, "window_height", 480);
+ maximize = remmina_file_get_int(cnnobj->remmina_file, "window_maximize", FALSE) ? TRUE : FALSE;
+ cnnobj->cnnwin = rcw_create_scrolled(width, height, maximize);
+ break;
+ }
+ rcw_update_tag(cnnobj->cnnwin, cnnobj);
+ rcw_append_new_page(cnnobj->cnnwin, cnnobj);
+ } else {
+ newpage = rcw_append_new_page(cnnobj->cnnwin, cnnobj);
+ gtk_window_present(GTK_WINDOW(cnnobj->cnnwin));
+ nb_set_current_page(cnnobj->cnnwin->priv->notebook, newpage);
+ }
+
+ // Do not call remmina_protocol_widget_update_alignment(cnnobj); here or cnnobj->proto will not fill its parent size
+ // and remmina_protocol_widget_update_remote_resolution() cannot autodetect available space
+
+ gtk_widget_show(cnnobj->proto);
+ g_signal_connect(G_OBJECT(cnnobj->proto), "connect", G_CALLBACK(rco_on_connect), cnnobj);
+ g_signal_connect(G_OBJECT(cnnobj->proto), "disconnect", G_CALLBACK(rco_on_disconnect), NULL);
+ g_signal_connect(G_OBJECT(cnnobj->proto), "desktop-resize", G_CALLBACK(rco_on_desktop_resize), NULL);
+ g_signal_connect(G_OBJECT(cnnobj->proto), "update-align", G_CALLBACK(rco_on_update_align), NULL);
+ g_signal_connect(G_OBJECT(cnnobj->proto), "lock-dynres", G_CALLBACK(rco_on_lock_dynres), NULL);
+ g_signal_connect(G_OBJECT(cnnobj->proto), "unlock-dynres", G_CALLBACK(rco_on_unlock_dynres), NULL);
+ g_signal_connect(G_OBJECT(cnnobj->proto), "enter-notify-event", G_CALLBACK(rco_enter_protocol_widget), cnnobj);
+ g_signal_connect(G_OBJECT(cnnobj->proto), "leave-notify-event", G_CALLBACK(rco_leave_protocol_widget), cnnobj);
+
+ if (!remmina_pref.save_view_mode)
+ remmina_file_set_int(cnnobj->remmina_file, "viewmode", remmina_pref.default_mode);
+
+
+ /* If it is a GtkSocket plugin and X11 is not available, we inform the
+ * user and close the connection */
+ ret = remmina_plugin_manager_query_feature_by_type(REMMINA_PLUGIN_TYPE_PROTOCOL,
+ remmina_file_get_string(remminafile, "protocol"),
+ REMMINA_PROTOCOL_FEATURE_TYPE_GTKSOCKET);
+ if (ret && !remmina_gtksocket_available()) {
+ gchar *title = _("Warning: This plugin requires GtkSocket, but this "
+ "feature is unavailable in a Wayland session.");
+
+ gchar *err_msg =
+ // TRANSLATORS: This should be a link to the Remmina wiki page:
+ // 'GtkSocket feature is not available'.
+ _("Plugins relying on GtkSocket can't run in a "
+ "Wayland session.\nFor more info and a possible "
+ "workaround, please visit the Remmina wiki at:\n\n"
+ "https://gitlab.com/Remmina/Remmina/-/wikis/GtkSocket-feature-is-not-available-in-a-Wayland-session");
+
+ dialog = gtk_message_dialog_new(
+ GTK_WINDOW(cnnobj->cnnwin),
+ GTK_DIALOG_MODAL,
+ GTK_MESSAGE_WARNING,
+ GTK_BUTTONS_OK,
+ "%s", title);
+
+ gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s",
+ err_msg);
+ gtk_dialog_add_button(GTK_DIALOG(dialog), _("Open in web browser"),
+ GTKSOCKET_NOT_AVAIL_RESPONSE_OPEN_BROWSER);
+
+ REMMINA_CRITICAL(g_strdup_printf("%s\n%s", title, err_msg));
+
+ g_signal_connect(G_OBJECT(dialog),
+ "response",
+ G_CALLBACK(rcw_gtksocket_not_available_dialog_response),
+ cnnobj);
+
+ // Make Text selectable. Usefull because of the link in the text.
+ GtkWidget *area = gtk_message_dialog_get_message_area(
+ GTK_MESSAGE_DIALOG(dialog));
+ GtkContainer *box = (GtkContainer *)area;
+
+ GList *children = gtk_container_get_children(box);
+ g_list_foreach(children, set_label_selectable, NULL);
+ g_list_free(children);
+
+ gtk_widget_show(dialog);
+
+ return NULL; /* Should we destroy something before returning? */
+ }
+
+ if (cnnobj->cnnwin->priv->floating_toolbar_widget)
+ gtk_widget_show(cnnobj->cnnwin->priv->floating_toolbar_widget);
+
+ if (remmina_protocol_widget_has_error((RemminaProtocolWidget *)cnnobj->proto)) {
+ printf("OK, an error occurred in initializing the protocol plugin before connecting. The error is %s.\n"
+ "TODO: Put this string as an error to show", remmina_protocol_widget_get_error_message((RemminaProtocolWidget *)cnnobj->proto));
+ return cnnobj->proto;
+ }
+
+
+ /* GTK window setup is done here, and we are almost ready to call remmina_protocol_widget_open_connection().
+ * But size has not yet been allocated by GTK
+ * to the widgets. If we are in RES_USE_INITIAL_WINDOW_SIZE resolution mode or scale is REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES,
+ * we should wait for a size allocation from GTK for cnnobj->proto
+ * before connecting */
+
+ cnnobj->deferred_open_size_allocate_handler = g_signal_connect(G_OBJECT(cnnobj->proto), "size-allocate", G_CALLBACK(rpw_size_allocated_on_connection), NULL);
+
+ return cnnobj->proto;
+}
+
+GtkWindow *rcw_get_gtkwindow(RemminaConnectionObject *cnnobj)
+{
+ return &cnnobj->cnnwin->window;
+}
+GtkWidget *rcw_get_gtkviewport(RemminaConnectionObject *cnnobj)
+{
+ return cnnobj->viewport;
+}
+
+void rcw_set_delete_confirm_mode(RemminaConnectionWindow *cnnwin, RemminaConnectionWindowOnDeleteConfirmMode mode)
+{
+ TRACE_CALL(__func__);
+ cnnwin->priv->on_delete_confirm_mode = mode;
+}
+
+/**
+ * Deletes a RemminaMessagePanel from the current cnnobj
+ * and if it was visible, make visible the last remaining one.
+ */
+void rco_destroy_message_panel(RemminaConnectionObject *cnnobj, RemminaMessagePanel *mp)
+{
+ TRACE_CALL(__func__);
+ GList *childs, *cc;
+ RemminaMessagePanel *lastPanel;
+ gboolean was_visible;
+ GtkWidget *page;
+
+ page = nb_find_page_by_cnnobj(cnnobj->cnnwin->priv->notebook, cnnobj);
+ childs = gtk_container_get_children(GTK_CONTAINER(page));
+ cc = g_list_first(childs);
+ while (cc != NULL) {
+ if ((RemminaMessagePanel *)cc->data == mp)
+ break;
+ cc = g_list_next(cc);
+ }
+ g_list_free(childs);
+
+ if (cc == NULL) {
+ printf("Remmina: Warning. There was a request to destroy a RemminaMessagePanel that is not on the page\n");
+ return;
+ }
+ was_visible = gtk_widget_is_visible(GTK_WIDGET(mp));
+ gtk_widget_destroy(GTK_WIDGET(mp));
+
+ /* And now, show the last remaining message panel, if needed */
+ if (was_visible) {
+ childs = gtk_container_get_children(GTK_CONTAINER(page));
+ cc = g_list_first(childs);
+ lastPanel = NULL;
+ while (cc != NULL) {
+ if (G_TYPE_CHECK_INSTANCE_TYPE(cc->data, REMMINA_TYPE_MESSAGE_PANEL))
+ lastPanel = (RemminaMessagePanel *)cc->data;
+ cc = g_list_next(cc);
+ }
+ g_list_free(childs);
+ if (lastPanel)
+ gtk_widget_show(GTK_WIDGET(lastPanel));
+ }
+}
+
+/**
+ * Each cnnobj->page can have more than one RemminaMessagePanel, but 0 or 1 are visible.
+ *
+ * This function adds a RemminaMessagePanel to cnnobj->page, move it to top,
+ * and makes it the only visible one.
+ */
+void rco_show_message_panel(RemminaConnectionObject *cnnobj, RemminaMessagePanel *mp)
+{
+ TRACE_CALL(__func__);
+ GList *childs, *cc;
+ GtkWidget *page;
+
+ /* Hides all RemminaMessagePanels childs of cnnobj->page */
+ page = nb_find_page_by_cnnobj(cnnobj->cnnwin->priv->notebook, cnnobj);
+ childs = gtk_container_get_children(GTK_CONTAINER(page));
+ cc = g_list_first(childs);
+ while (cc != NULL) {
+ if (G_TYPE_CHECK_INSTANCE_TYPE(cc->data, REMMINA_TYPE_MESSAGE_PANEL))
+ gtk_widget_hide(GTK_WIDGET(cc->data));
+ cc = g_list_next(cc);
+ }
+ g_list_free(childs);
+
+ /* Add the new message panel at the top of cnnobj->page */
+ gtk_box_pack_start(GTK_BOX(page), GTK_WIDGET(mp), FALSE, FALSE, 0);
+ gtk_box_reorder_child(GTK_BOX(page), GTK_WIDGET(mp), 0);
+
+ /* Show the message panel */
+ gtk_widget_show_all(GTK_WIDGET(mp));
+
+ /* Focus the correct field of the RemminaMessagePanel */
+ remmina_message_panel_focus_auth_entry(mp);
+}
diff --git a/src/rcw.h b/src/rcw.h
new file mode 100644
index 0000000..6011abe
--- /dev/null
+++ b/src/rcw.h
@@ -0,0 +1,93 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <gtk/gtk.h>
+#include "remmina_file.h"
+#include "remmina_message_panel.h"
+
+G_BEGIN_DECLS
+
+#define REMMINA_TYPE_CONNECTION_WINDOW (rcw_get_type())
+#define RCW(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_CONNECTION_WINDOW, RemminaConnectionWindow))
+#define RCW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_CONNECTION_WINDOW, RemminaConnectionWindowClass))
+#define REMMINA_IS_CONNECTION_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_CONNECTION_WINDOW))
+#define REMMINA_IS_CONNECTION_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_CONNECTION_WINDOW))
+#define RCW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_CONNECTION_WINDOW, RemminaConnectionWindowClass))
+
+typedef struct _RemminaConnectionWindowPriv RemminaConnectionWindowPriv;
+
+typedef struct _RemminaConnectionWindow {
+ GtkWindow window;
+ RemminaConnectionWindowPriv * priv;
+} RemminaConnectionWindow;
+
+typedef struct _RemminaConnectionWindowClass {
+ GtkWindowClass parent_class;
+ void (*toolbar_place)(RemminaConnectionWindow *gp);
+} RemminaConnectionWindowClass;
+
+typedef struct _RemminaConnectionObject RemminaConnectionObject;
+
+typedef enum {
+ RCW_ONDELETE_CONFIRM_IF_2_OR_MORE = 0,
+ RCW_ONDELETE_NOCONFIRM = 1
+} RemminaConnectionWindowOnDeleteConfirmMode;
+
+GType rcw_get_type(void)
+G_GNUC_CONST;
+
+/* Open a new connection window for a .remmina file */
+gboolean rcw_open_from_filename(const gchar *filename);
+/* Open a new connection window for a given RemminaFile struct. The struct will be freed after the call */
+void rcw_open_from_file(RemminaFile *remminafile);
+gboolean rcw_delete(RemminaConnectionWindow *cnnwin);
+void rcw_set_delete_confirm_mode(RemminaConnectionWindow *cnnwin, RemminaConnectionWindowOnDeleteConfirmMode mode);
+GtkWidget *rcw_open_from_file_full(RemminaFile *remminafile, GCallback disconnect_cb, gpointer data, guint *handler);
+GtkWindow* rcw_get_gtkwindow(RemminaConnectionObject *cnnobj);
+GtkWidget* rcw_get_gtkviewport(RemminaConnectionObject *cnnobj);
+
+void rco_destroy_message_panel(RemminaConnectionObject *cnnobj, RemminaMessagePanel *mp);
+void rco_show_message_panel(RemminaConnectionObject *cnnobj, RemminaMessagePanel *mp);
+void rco_get_monitor_geometry(RemminaConnectionObject *cnnobj, GdkRectangle *sz);
+
+
+
+#define MESSAGE_PANEL_SPINNER 0x00000001
+#define MESSAGE_PANEL_OKBUTTON 0x00000002
+
+G_END_DECLS
diff --git a/src/remmina.1 b/src/remmina.1
new file mode 100644
index 0000000..27fc505
--- /dev/null
+++ b/src/remmina.1
@@ -0,0 +1,229 @@
+.\" Generated by scdoc 1.11.2
+.\" Complete documentation for this program is not available as a GNU info page
+.ie \n(.g .ds Aq \(aq
+.el .ds Aq '
+.nh
+.ad l
+.\" Begin generated content:
+.TH "REMMINA" "1" "2022-03-25"
+.PP
+.SH NAME
+.PP
+remmina -- Remmina the GTK+ Remote Desktop Client
+.PP
+.SH SYNOPSIS
+.PP
+remmina [-a|i|n|q|v] [-c FILE] [-e FILE] [-p TABINDEX] [-s SERVER]
+[-t PROTOCOL] [-x PLUGIN] [--update-profile]
+[--set-option OPTION[=VALUE]] [--display DISPLAY]
+.PP
+.SH DESCRIPTION
+.PP
+Remmina is a remote desktop client written in GTK+, aiming to be useful for
+system administrators and travellers, who need to work with lots of remote
+computers in front of either large monitors or tiny netbooks.\& Remmina
+supports multiple network protocols in an integrated and consistent user
+interface.\&
+.PP
+Currently RDP, VNC, SSH, SPICE, and WWW are supported.\&
+.PP
+Remmina is released in separated source packages:
+.PP
+.PD 0
+.IP \(bu 4
+"remmina", the main GTK+ application
+.IP \(bu 4
+"remmina-plugins", a set of plugins
+.PD
+.PP
+Remmina is free and open-source software, released under GNU GPL license.\&
+.PP
+.SH FILES
+.PP
+${XDG_CONFIG_DIRS}/remmina.\&pref or ${XDG_CONFIG_HOME}/remmina/remmina.\&pref :
+.PP
+Remmina configuration files.\&
+.PP
+At the first Remmina execution the system wide Remmina configuration files,
+will be copied in the ${XDG_CONFIG_HOME}
+.PP
+${XDG_DATA_DIRS}/FILE.\&remmina or ${XDG_DATA_HOME}/remmina/FILE.\&remmina :
+.PP
+Remmina profiles, the file name is autogenerated, but you can create
+manually your own files with the FILE name you prefer.\& It'\&s possible to
+specify a custom profile name and location.\&
+.PP
+.SH OPTIONS
+.PP
+\fB-h, --help\fR
+.RS 4
+Show help options
+.PP
+.RE
+\fB-a\fR
+.RS 4
+Show about dialog
+.PP
+.RE
+\fB-c, --connect=FILE\fR
+.RS 4
+Connect directly either to a desktop using options described in a file,
+or a supported URI (RDP, VNC, SSH or SPICE).\&
+.br
+The filetype can be ".\&remmina" or one supported by a plugin capable of
+importing files
+.PP
+.RE
+\fB-e, --edit=FILE\fR
+.RS 4
+Open and edit desktop connection using options described by file,
+file type can be either .\&remmina or one supported by a file
+import capable plugin
+.PP
+.RE
+\fB-k, --kiosk\fR
+.RS 4
+Start Remmina in kiosk mode (thin client)
+.PP
+.RE
+\fB-n, --new\fR
+.RS 4
+Create a new connection profile
+.PP
+.RE
+\fB-p, --pref=TABINDEX\fR
+.RS 4
+Show preferences dialog page
+.PP
+.RE
+\fB-x, --plugin=PLUGIN\fR
+.RS 4
+Execute the plugin
+.PP
+.RE
+\fB-q, --quit\fR
+.RS 4
+Quit the application
+.PP
+.RE
+\fB-s, --server=SERVER\fR
+.RS 4
+Use default server name (for \fB--new\fR)
+.PP
+.RE
+\fB-t, --protocol=PROTOCOL\fR
+.RS 4
+Use default protocol (for \fB--new\fR)
+.PP
+.RE
+\fB-i, --icon\fR
+.RS 4
+Start as tray icon
+.PP
+.RE
+\fB-v, --version\fR
+.RS 4
+Show the application'\&s version
+.PP
+.RE
+\fB--update-profile\fR
+.RS 4
+Modify connection profile, require also \fB--set-option\fR
+.PP
+.RE
+\fB--set-option OPTION[=VALUE]\fR
+.RS 4
+Set one or more profile settings, to be used with \fB--update-pro-file\fR
+.PP
+.RE
+\fB--encrypt-password\fR
+.RS 4
+Encrypt a password
+.PP
+.RE
+\fB--display=DISPLAY\fR
+.RS 4
+X display to use
+.PP
+.RE
+\fB--disable-toolbar\fR
+.RS 4
+Disable toolbar
+.PP
+.RE
+\fB--enable-fullscreen\fR
+.RS 4
+Enable fullscreen
+.PP
+.RE
+\fB--enable-extra-hardening\fR
+.RS 4
+Enable extra hardening (disable closing confirmation, disable unsafe shortcut keys, hide tabs, hide search bar)
+.br
+List of disabled shortcut keys:
+.br
+fullscreen, autofit, prevtab and nexttab, scale, grab, minimize, viewonly, screenshot, disconnect, toolbar
+.PP
+.RE
+\fB--no-tray-icon\fR
+.RS 4
+Disable tray icon
+.PP
+.RE
+.SH EXAMPLES
+.PP
+To connect using an exisitng connection profile use:
+\fBremmina -c FILE.\&remmina\fR
+.PP
+To quick connect using a URI:
+.PP
+remmina -c rdp://username@server
+.br
+remmina -c rdp://domain\\\\username@server
+.br
+remmina -c vnc://username@server
+.br
+remmina -c vnc://server?\&VncUsername=username
+.br
+remmina -c ssh://user@server
+.br
+remmina -c spice://server
+.PP
+To quick connect using a URI along with an encrypted password:
+.PP
+remmina -c rdp://username:encrypted-password@server
+.br
+remmina -c vnc://username:encrypted-password@server
+.br
+remmina -c vnc://server?\&VncUsername=username\\&VncPassword=encrypted-password
+.PP
+To encrypt a password for use with a URI:
+.PP
+remmina --encrypt-password
+.PP
+To update username and password and set a different resolution mode of a
+remmina connection profile use:
+.PP
+echo "ausernamenapassword" | remmina --update-profile /PATH/TO/FOO.\&remmina --set-option username --set-option resolution_mode=2 --set-option password
+.PP
+.SH SEE ALSO
+.PP
+remmina-file-wrapper(1)
+.PP
+.SH AUTHORS
+.PP
+Antenore Gatta <antenore at simbiosi dot org> and
+Giovanni Panozzo <giovanni at panozzo dot it>
+.PP
+See the THANKS file for a more detailed list.\&
+.PP
+Remmina was initially written by Vic Lee <llyzs@163.\&com>
+.PP
+This manual page was written by Antenore Gatta <antenore at simbiosi dot org>.\&
+.PP
+.SH COPYRIGHT
+.PP
+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, or (at your option) any later
+version.\&
diff --git a/src/remmina.c b/src/remmina.c
new file mode 100644
index 0000000..cd8ce56
--- /dev/null
+++ b/src/remmina.c
@@ -0,0 +1,489 @@
+/*
+ * 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.
+ *
+ */
+#include <curl/curl.h>
+#include <gdk/gdk.h>
+
+#define G_LOG_USE_STRUCTURED
+#ifndef G_LOG_DOMAIN
+#define G_LOG_DOMAIN ((gchar*)"remmina")
+#endif /* G_LOG_DOMAIN */
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#elif defined(GDK_WINDOWING_WAYLAND)
+#include <gdk/gdkwayland.h>
+#endif
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+#include <stdlib.h>
+
+#include "config.h"
+#include "remmina_sodium.h"
+#include "remmina.h"
+#include "remmina_exec.h"
+#include "remmina_file_manager.h"
+#include "remmina_icon.h"
+#include "remmina_log.h"
+#include "remmina_main.h"
+#include "remmina_masterthread_exec.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_plugin_native.h"
+#ifdef WITH_PYTHONLIBS
+#include "remmina_plugin_python.h"
+#endif
+#include "remmina_pref.h"
+#include "remmina_public.h"
+#include "remmina_sftp_plugin.h"
+#include "remmina_ssh_plugin.h"
+#include "remmina_widget_pool.h"
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_info.h"
+
+#ifdef HAVE_ERRNO_H
+#include <errno.h>
+#endif
+#include <pthread.h>
+#ifdef HAVE_LIBGCRYPT
+#include <gcrypt.h>
+# if GCRYPT_VERSION_NUMBER < 0x010600
+GCRY_THREAD_OPTION_PTHREAD_IMPL;
+#endif /* !GCRYPT_VERSION_NUMBER */
+#endif /* HAVE_LIBGCRYPT */
+
+#ifdef HAVE_LIBGCRYPT
+# if GCRYPT_VERSION_NUMBER < 0x010600
+static int gcrypt_thread_initialized = 0;
+#endif /* !GCRYPT_VERSION_NUMBER */
+#endif /* HAVE_LIBGCRYPT */
+
+gboolean kioskmode;
+gboolean imode;
+gboolean disablenews;
+gboolean disablestats;
+gboolean disabletoolbar;
+gboolean fullscreen;
+gboolean extrahardening;
+gboolean disabletrayicon;
+
+static GOptionEntry remmina_options[] =
+{
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "about", 'a', 0, G_OPTION_ARG_NONE, NULL, N_("Show \'About\'"), NULL },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "connect", 'c', 0, G_OPTION_ARG_FILENAME_ARRAY, NULL, N_("Connect either to a desktop described in a file (.remmina or a filetype supported by a plugin) or a supported URI (RDP, VNC, SSH or SPICE)"), N_("FILE") },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { G_OPTION_REMAINING, '\0', 0, G_OPTION_ARG_FILENAME_ARRAY, NULL, N_("Connect to a desktop described in a file (.remmina or a filetype supported by a plugin)"), N_("FILE") },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "edit", 'e', 0, G_OPTION_ARG_FILENAME_ARRAY, NULL, N_("Edit desktop connection described in file (.remmina or a filetype supported by plugin)"), N_("FILE") },
+ { "help", '?', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, NULL, NULL, NULL },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "kiosk", 'k', 0, G_OPTION_ARG_NONE, NULL, N_("Start in kiosk mode"), NULL },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "new", 'n', 0, G_OPTION_ARG_NONE, NULL, N_("Create new connection profile"), NULL },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "pref", 'p', 0, G_OPTION_ARG_STRING, NULL, N_("Show preferences"), N_("TABINDEX") },
+#if 0
+ /* This option was used mainly for telepathy, let's keep it if we will need it in the future */
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ //{ "plugin", 'x', 0, G_OPTION_ARG_STRING, NULL, N_("Run a plugin"), N_("PLUGIN") },
+#endif
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "quit", 'q', 0, G_OPTION_ARG_NONE, NULL, N_("Quit"), NULL },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "server", 's', 0, G_OPTION_ARG_STRING, NULL, N_("Use default server name (for --new)"), N_("SERVER") },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "protocol", 't', 0, G_OPTION_ARG_STRING, NULL, N_("Use default protocol (for --new)"), N_("PROTOCOL") },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "icon", 'i', 0, G_OPTION_ARG_NONE, NULL, N_("Start in tray"), NULL },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "version", 'v', 0, G_OPTION_ARG_NONE, NULL, N_("Show the application version"), NULL },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "full-version", 'V', 0, G_OPTION_ARG_NONE, NULL, N_("Show version of the application and its plugins"), NULL },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "update-profile", 0, 0, G_OPTION_ARG_FILENAME, NULL, N_("Modify connection profile (requires --set-option)"), NULL },
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ { "set-option", 0, 0, G_OPTION_ARG_STRING_ARRAY, NULL, N_("Set one or more profile settings, to be used with --update-profile"), NULL },
+ { "encrypt-password", 0, 0, G_OPTION_ARG_NONE, NULL, N_("Encrypt a password"), NULL },
+ { "disable-news", 0, 0, G_OPTION_ARG_NONE, NULL, N_("Disable news"), NULL },
+ { "disable-stats", 0, 0, G_OPTION_ARG_NONE, NULL, N_("Disable stats"), NULL },
+ { "disable-toolbar", 0, 0, G_OPTION_ARG_NONE, NULL, N_("Disable toolbar"), NULL },
+ { "enable-fullscreen", 0, 0, G_OPTION_ARG_NONE, NULL, N_("Enable fullscreen"), NULL },
+ { "enable-extra-hardening", 0, 0, G_OPTION_ARG_NONE, NULL, N_("Enable extra hardening (disable closing confirmation, disable unsafe shortcut keys, hide tabs, hide search bar)"), NULL },
+ { "no-tray-icon", 0, 0, G_OPTION_ARG_NONE, NULL, N_("Disable tray icon"), NULL },
+ { NULL }
+};
+
+#ifdef WITH_LIBGCRYPT
+static int
+_gpg_error_to_errno(gcry_error_t e)
+{
+ /* be lazy right now */
+ if (e == GPG_ERR_NO_ERROR)
+ return 0;
+ else
+ return EINVAL;
+}
+#endif /* !WITH_LIBGCRYPT */
+
+static gint remmina_on_command_line(GApplication *app, GApplicationCommandLine *cmdline)
+{
+ TRACE_CALL(__func__);
+
+ gint status = 0;
+ gboolean executed = FALSE;
+ GVariantDict *opts;
+ gchar *str;
+ const gchar **files;
+ const gchar **remaining_args;
+ gchar *protocol;
+ gchar *server;
+
+#if SODIUM_VERSION_INT >= 90200
+ remmina_sodium_init();
+#endif
+ opts = g_application_command_line_get_options_dict(cmdline);
+
+ if (g_variant_dict_lookup_value(opts, "disable-news", NULL)) {
+ disablenews = TRUE;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "disable-stats", NULL)) {
+ disablestats = TRUE;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "disable-toolbar", NULL)) {
+ disabletoolbar = TRUE;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "enable-fullscreen", NULL)) {
+ fullscreen = TRUE;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "enable-extra-hardening", NULL)) {
+ extrahardening = TRUE;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "no-tray-icon", NULL)) {
+ disabletrayicon = TRUE;
+ }
+
+ remmina_pref_init();
+
+ if (g_variant_dict_lookup_value(opts, "quit", NULL)) {
+ remmina_exec_command(REMMINA_COMMAND_EXIT, NULL);
+ executed = TRUE;
+ status = 1;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "about", NULL)) {
+ imode = TRUE;
+ remmina_exec_command(REMMINA_COMMAND_ABOUT, NULL);
+ executed = TRUE;
+ }
+
+ /** @warning To be used like -c FILE -c FILE -c FILE …
+ *
+ */
+ if (g_variant_dict_lookup(opts, "connect", "^aay", &files)) {
+ if (files)
+ for (gint i = 0; files[i]; i++) {
+ g_debug ("Connecting to: %s", files[i]);
+ remmina_exec_command(REMMINA_COMMAND_CONNECT, files[i]);
+ }
+ executed = TRUE;
+ }
+
+ if (g_variant_dict_lookup(opts, G_OPTION_REMAINING, "^a&ay", &remaining_args)) {
+ remmina_exec_command(REMMINA_COMMAND_CONNECT, remaining_args[0]);
+ g_free(remaining_args);
+ executed = TRUE;
+ }
+
+ if (g_variant_dict_lookup(opts, "edit", "^aay", &files)) {
+ imode = TRUE;
+ if (files)
+ for (gint i = 0; files[i]; i++) {
+ g_debug ("Editing file: %s", files[i]);
+ remmina_exec_command(REMMINA_COMMAND_EDIT, files[i]);
+ }
+ //remmina_exec_command(REMMINA_COMMAND_EDIT, str);
+ //g_free(str);
+ executed = TRUE;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "kiosk", NULL)) {
+ kioskmode = TRUE;
+ imode = TRUE;
+ remmina_exec_command(REMMINA_COMMAND_MAIN, NULL);
+ executed = TRUE;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "new", NULL)) {
+ if (!g_variant_dict_lookup(opts, "protocol", "&s", &protocol))
+ protocol = NULL;
+
+ if (g_variant_dict_lookup(opts, "server", "&s", &server))
+ str = g_strdup_printf("%s,%s", protocol, server);
+ else
+ str = g_strdup(protocol);
+
+ remmina_exec_command(REMMINA_COMMAND_NEW, str);
+ g_free(str);
+ executed = TRUE;
+ }
+
+ if (g_variant_dict_lookup(opts, "pref", "&s", &str)) {
+ imode = TRUE;
+ remmina_exec_command(REMMINA_COMMAND_PREF, str);
+ executed = TRUE;
+ }
+
+ if (g_variant_dict_lookup(opts, "plugin", "&s", &str)) {
+ remmina_exec_command(REMMINA_COMMAND_PLUGIN, str);
+ executed = TRUE;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "icon", NULL)) {
+ remmina_exec_command(REMMINA_COMMAND_NONE, NULL);
+ executed = TRUE;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "encrypt-password", NULL)) {
+ remmina_exec_command(REMMINA_COMMAND_ENCRYPT_PASSWORD, NULL);
+ executed = TRUE;
+ status = 1;
+ }
+
+ if (!executed)
+ remmina_exec_command(REMMINA_COMMAND_MAIN, NULL);
+
+ return status;
+}
+
+static void remmina_on_startup(GApplication *app)
+{
+ TRACE_CALL(__func__);
+
+ RemminaSecretPlugin *secret_plugin;
+
+ remmina_widget_pool_init();
+ remmina_sftp_plugin_register();
+ remmina_ssh_plugin_register();
+ remmina_icon_init();
+ g_set_application_name("Remmina");
+ gtk_window_set_default_icon_name(REMMINA_APP_ID);
+
+ /* Setting the X11 program class (WM_CLASS) is necessary to group
+ * windows with .desktop file which has the same StartupWMClass */
+ gdk_set_program_class(REMMINA_APP_ID);
+
+ gtk_icon_theme_append_search_path(gtk_icon_theme_get_default(),
+ REMMINA_RUNTIME_DATADIR G_DIR_SEPARATOR_S "icons");
+ g_application_hold(app);
+ remmina_info_schedule();
+
+ gchar *log_filename = g_build_filename(g_get_tmp_dir(), LOG_FILE_NAME, NULL);
+ FILE *log_file = fopen(log_filename, "w"); // Flush log file
+ g_free(log_filename);
+
+ if (log_file != NULL) {
+ fclose(log_file);
+ }
+
+ /* Check for secret plugin and service initialization and show console warnings if
+ * something is missing */
+ remmina_plugin_manager_get_available_plugins();
+ secret_plugin = remmina_plugin_manager_get_secret_plugin();
+ if (!secret_plugin)
+ g_print("Warning: Remmina is running without a secret plugin. Passwords will be saved in a less secure way.\n");
+ else
+ if (!secret_plugin->is_service_available(secret_plugin))
+ g_print("Warning: Remmina is running with a secrecy plugin, but it cannot connect to a secrecy service.\n");
+
+ remmina_exec_command(REMMINA_COMMAND_AUTOSTART, NULL);
+}
+
+static gint remmina_on_local_cmdline(GApplication *app, GVariantDict *opts, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ int status = -1;
+ gchar *str;
+ gchar **settings;
+
+ /* Here you handle any command line options that you want to be executed
+ * in the local instance (the non-unique instance) */
+
+ if (g_variant_dict_lookup_value(opts, "version", NULL)) {
+ remmina_exec_command(REMMINA_COMMAND_VERSION, NULL);
+ status = 0;
+ }
+
+ if (g_variant_dict_lookup_value(opts, "full-version", NULL)) {
+ remmina_exec_command(REMMINA_COMMAND_FULL_VERSION, NULL);
+ status = 0;
+ }
+
+ if (g_variant_dict_lookup(opts, "update-profile", "^&ay", &str)) { /* ^&ay no need to free */
+ if (g_variant_dict_lookup(opts, "set-option", "^a&s", &settings)) {
+ if (settings != NULL) {
+ status = remmina_exec_set_setting(str, settings);
+ g_free(settings);
+ } else {
+ status = 1;
+ }
+ } else {
+ status = 1;
+ g_print("Error: --update-profile requires --set-option\n");
+ }
+ }
+
+ /* Returning a non negative value here makes the application exit */
+ return status;
+}
+
+int main(int argc, char *argv[])
+{
+ TRACE_CALL(__func__);
+ GtkApplication *app;
+ const gchar *app_id;
+ int status;
+
+ g_unsetenv("GDK_CORE_DEVICE_EVENTS");
+
+ // Checking for environment variable "G_MESSAGES_DEBUG"
+ // Give the less familiar with GLib a tip on where to get
+ // more debugging info.
+ if(!getenv("G_MESSAGES_DEBUG")) {
+ /* TRANSLATORS:
+ * This link should point to a resource explaining how to get Remmina
+ * to log more verbose statements.
+ */
+ g_message(_("Remmina does not log all output statements. "
+ "Turn on more verbose output by using "
+ "\"G_MESSAGES_DEBUG=remmina\" as an environment variable.\n"
+ "More info available on the Remmina wiki at:\n"
+ "https://gitlab.com/Remmina/Remmina/-/wikis/Usage/Remmina-debugging"
+ ));
+ }
+
+ /* Enable wayland backend only after GTK 3.22.27 or the clipboard
+ * will not work. See GTK bug 790031 */
+ if (remmina_gtk_check_version(3, 22, 27))
+ gdk_set_allowed_backends("wayland,x11,broadway,quartz,mir");
+ else
+ gdk_set_allowed_backends("x11,broadway,quartz,mir");
+
+ remmina_masterthread_exec_save_main_thread_id();
+
+ bindtextdomain(GETTEXT_PACKAGE, REMMINA_RUNTIME_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+ textdomain(GETTEXT_PACKAGE);
+
+#ifdef HAVE_LIBGCRYPT
+# if GCRYPT_VERSION_NUMBER < 0x010600
+ gcry_error_t e;
+ if (!gcrypt_thread_initialized) {
+ if ((e = gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread)) != GPG_ERR_NO_ERROR)
+ return -1;
+ gcrypt_thread_initialized++;
+ }
+#endif /* !GCRYPT_VERSION_NUMBER */
+ gcry_check_version(NULL);
+ gcry_control(GCRYCTL_DISABLE_SECMEM, 0);
+ gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
+#endif /* !HAVE_LIBGCRYPT */
+
+ /* Initialize some Remmina parts needed also on a local instance for correct handle-local-options */
+ curl_global_init(CURL_GLOBAL_ALL);
+ remmina_pref_init();
+ remmina_file_manager_init();
+ remmina_plugin_manager_init();
+
+
+
+ app_id = g_application_id_is_valid(REMMINA_APP_ID) ? REMMINA_APP_ID : NULL;
+ app = gtk_application_new(app_id, G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_CAN_OVERRIDE_APP_ID);
+#if !GTK_CHECK_VERSION(4, 0, 0) /* This is not needed anymore starting from GTK 4 */
+ g_set_prgname(app_id);
+#endif
+ g_application_add_main_option_entries(G_APPLICATION(app), remmina_options);
+#if GLIB_CHECK_VERSION(2,56,0)
+ gchar *summary = g_strdup_printf ("%s %s", app_id, VERSION);
+ g_application_set_option_context_summary (G_APPLICATION(app), summary);
+ g_free(summary);
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ g_application_set_option_context_parameter_string (G_APPLICATION(app), _("- or protocol://username:encryptedpassword@host:port"));
+ // TRANSLATORS: Shown in terminal. Do not use characters that may be not supported on a terminal
+ g_application_set_option_context_description (G_APPLICATION(app),
+ _("Examples:\n"
+ "To connect using an existing connection profile, use:\n"
+ "\n"
+ "\tremmina -c FILE.remmina\n"
+ "\n"
+ "To quick connect using a URI:\n"
+ "\n"
+ "\tremmina -c rdp://username@server\n"
+ "\tremmina -c rdp://domain\\\\username@server\n"
+ "\tremmina -c vnc://username@server\n"
+ "\tremmina -c vnc://server?VncUsername=username\n"
+ "\tremmina -c ssh://user@server\n"
+ "\tremmina -c spice://server\n"
+ "\n"
+ "To quick connect using a URI along with an encrypted password:\n"
+ "\n"
+ "\tremmina -c rdp://username:encrypted-password@server\n"
+ "\tremmina -c vnc://username:encrypted-password@server\n"
+ "\tremmina -c vnc://server?VncUsername=username\\&VncPassword=encrypted-password\n"
+ "\n"
+ "To encrypt a password for use with a URI:\n"
+ "\n"
+ "\tremmina --encrypt-password\n"
+ "\n"
+ "To update username and password and set a different resolution mode of a Remmina connection profile, use:\n"
+ "\n"
+ "\techo \"username\\napassword\" | remmina --update-profile /PATH/TO/FOO.remmina --set-option username --set-option resolution_mode=2 --set-option password\n"));
+#endif
+
+ g_signal_connect(app, "startup", G_CALLBACK(remmina_on_startup), NULL);
+ g_signal_connect(app, "command-line", G_CALLBACK(remmina_on_command_line), NULL);
+ g_signal_connect(app, "handle-local-options", G_CALLBACK(remmina_on_local_cmdline), NULL);
+
+
+ g_application_set_inactivity_timeout(G_APPLICATION(app), 10000);
+ status = g_application_run(G_APPLICATION(app), argc, argv);
+ g_object_unref(app);
+
+ return status;
+}
diff --git a/src/remmina.h b/src/remmina.h
new file mode 100644
index 0000000..05d8f06
--- /dev/null
+++ b/src/remmina.h
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ *
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+extern gboolean kioskmode;
+extern gboolean imode;
+G_END_DECLS
diff --git a/src/remmina.scd b/src/remmina.scd
new file mode 100644
index 0000000..c9eec11
--- /dev/null
+++ b/src/remmina.scd
@@ -0,0 +1,172 @@
+REMMINA(1)
+
+# NAME
+
+remmina -- Remmina the GTK+ Remote Desktop Client
+
+# SYNOPSIS
+
+remmina [-a|i|n|q|v] [-c FILE] [-e FILE] [-p TABINDEX] [-s SERVER] \
+[-t PROTOCOL] [-x PLUGIN] [--update-profile] \
+[--set-option OPTION[=VALUE]] [--display DISPLAY]
+
+# DESCRIPTION
+
+Remmina is a remote desktop client written in GTK+, aiming to be useful for
+system administrators and travellers, who need to work with lots of remote
+computers in front of either large monitors or tiny netbooks. Remmina
+supports multiple network protocols in an integrated and consistent user
+interface.
+
+Currently RDP, VNC, SSH, SPICE, and WWW are supported.
+
+Remmina is released in separated source packages:
+
+- "remmina", the main GTK+ application
+- "remmina-plugins", a set of plugins
+
+Remmina is free and open-source software, released under GNU GPL license.
+
+# FILES
+
+${XDG_CONFIG_DIRS}/remmina.pref or ${XDG_CONFIG_HOME}/remmina/remmina.pref :
+
+Remmina configuration files.
+
+At the first Remmina execution the system wide Remmina configuration files,
+will be copied in the ${XDG_CONFIG_HOME}
+
+${XDG_DATA_DIRS}/FILE.remmina or ${XDG_DATA_HOME}/remmina/FILE.remmina :
+
+Remmina profiles, the file name is autogenerated, but you can create
+manually your own files with the FILE name you prefer. It's possible to
+specify a custom profile name and location.
+
+# OPTIONS
+
+*-h, --help*
+ Show help options
+
+*-a*
+ Show about dialog
+
+*-c, --connect=FILE*
+ Connect directly either to a desktop using options described in a file,
+ or a supported URI (RDP, VNC, SSH or SPICE).++
+The filetype can be ".remmina" or one supported by a plugin capable of
+ importing files
+
+*-e, --edit=FILE*
+ Open and edit desktop connection using options described by file,
+ file type can be either .remmina or one supported by a file
+ import capable plugin
+
+*-k, --kiosk*
+ Start Remmina in kiosk mode (thin client)
+
+*-n, --new*
+ Create a new connection profile
+
+*-p, --pref=TABINDEX*
+ Show preferences dialog page
+
+*-x, --plugin=PLUGIN*
+ Execute the plugin
+
+*-q, --quit*
+ Quit the application
+
+*-s, --server=SERVER*
+ Use default server name (for *--new*)
+
+*-t, --protocol=PROTOCOL*
+ Use default protocol (for *--new*)
+
+*-i, --icon*
+ Start as tray icon
+
+*-v, --version*
+ Show the application's version
+
+*--update-profile*
+ Modify connection profile, require also *--set-option*
+
+*--set-option OPTION[=VALUE]*
+ Set one or more profile settings, to be used with *--update-pro-file*
+
+*--encrypt-password*
+ Encrypt a password
+
+*--display=DISPLAY*
+ X display to use
+
+*--disable-news*
+ Disable news
+
+*--disable-stats*
+ Disable stats
+
+*--disable-toolbar*
+ Disable toolbar
+
+*--enable-fullscreen*
+ Enable fullscreen
+
+*--enable-extra-hardening*
+ Enable extra hardening (disable closing confirmation, disable unsafe shortcut keys, hide tabs, hide search bar)++
+List of disabled shortcut keys:++
+fullscreen, autofit, prevtab and nexttab, scale, grab, minimize, viewonly, screenshot, disconnect, toolbar
+
+*--no-tray-icon*
+ Disable tray icon
+
+# EXAMPLES
+
+To connect using an exisitng connection profile use:
+*remmina -c FILE.remmina*
+
+To quick connect using a URI:
+
+remmina -c rdp://username@server++
+remmina -c rdp://domain\\\\username@server++
+remmina -c vnc://username@server++
+remmina -c vnc://server?VncUsername=username++
+remmina -c ssh://user@server++
+remmina -c spice://server
+
+To quick connect using a URI along with an encrypted password:
+
+remmina -c rdp://username:encrypted-password@server++
+remmina -c vnc://username:encrypted-password@server++
+remmina -c vnc://server?VncUsername=username\\&VncPassword=encrypted-password
+
+To encrypt a password for use with a URI:
+
+remmina --encrypt-password
+
+To update username and password and set a different resolution mode of a
+remmina connection profile use:
+
+echo "ausername\napassword" | remmina --update-profile /PATH/TO/FOO.remmina --set-option username --set-option resolution_mode=2 --set-option password
+
+# SEE ALSO
+
+remmina-file-wrapper(1)
+
+# AUTHORS
+
+Antenore Gatta <antenore at simbiosi dot org> and
+Giovanni Panozzo <giovanni at panozzo dot it>
+
+See the THANKS file for a more detailed list.
+
+Remmina was initially written by Vic Lee <llyzs@163.com>
+
+This manual page was written by Antenore Gatta <antenore at simbiosi dot org>.
+
+# COPYRIGHT
+
+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, or (at your option) any later
+version.
diff --git a/src/remmina_about.c b/src/remmina_about.c
new file mode 100644
index 0000000..b8d4a29
--- /dev/null
+++ b/src/remmina_about.c
@@ -0,0 +1,64 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 <glib/gi18n.h>
+#include "remmina_about.h"
+#include "remmina_public.h"
+#include "remmina/remmina_trace_calls.h"
+
+/* Show the about dialog from the file ui/remmina_about.glade */
+void remmina_about_open(GtkWindow *parent)
+{
+ TRACE_CALL(__func__);
+
+ GtkBuilder *builder = remmina_public_gtk_builder_new_from_resource ("/org/remmina/Remmina/src/../data/ui/remmina_about.glade");
+ GtkDialog *dialog = GTK_DIALOG(gtk_builder_get_object(builder, "dialog_remmina_about"));
+
+ gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(dialog), VERSION " (git " REMMINA_GIT_REVISION ")");
+ // TRANSLATORS: translator-credits should be replaced with a formatted list of translators in your own language
+ gtk_about_dialog_set_translator_credits(GTK_ABOUT_DIALOG(dialog), _("translator-credits"));
+
+ if (parent) {
+ gtk_window_set_transient_for(GTK_WINDOW(dialog), parent);
+ gtk_window_set_destroy_with_parent(GTK_WINDOW(dialog), TRUE);
+ }
+
+ g_signal_connect(dialog, "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_window_present(GTK_WINDOW(dialog));
+
+ g_object_unref(G_OBJECT(builder));
+}
diff --git a/src/remmina_about.h b/src/remmina_about.h
new file mode 100644
index 0000000..ac746c0
--- /dev/null
+++ b/src/remmina_about.h
@@ -0,0 +1,44 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+void remmina_about_open(GtkWindow *parent);
+
+G_END_DECLS
diff --git a/src/remmina_applet_menu.c b/src/remmina_applet_menu.c
new file mode 100644
index 0000000..fa1362f
--- /dev/null
+++ b/src/remmina_applet_menu.c
@@ -0,0 +1,283 @@
+/*
+ * 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 "config.h"
+
+#include <glib/gi18n.h>
+#include <string.h>
+
+#include "remmina_public.h"
+#include "remmina_applet_menu.h"
+#include "remmina_file_manager.h"
+#include "remmina_pref.h"
+#include "remmina/remmina_trace_calls.h"
+
+G_DEFINE_TYPE( RemminaAppletMenu, remmina_applet_menu, GTK_TYPE_MENU)
+
+struct _RemminaAppletMenuPriv {
+ gboolean hide_count;
+};
+
+enum {
+ LAUNCH_ITEM_SIGNAL, EDIT_ITEM_SIGNAL, LAST_SIGNAL
+};
+
+static guint remmina_applet_menu_signals[LAST_SIGNAL] =
+{ 0 };
+
+static void remmina_applet_menu_destroy(RemminaAppletMenu *menu, gpointer data)
+{
+ TRACE_CALL(__func__);
+ g_free(menu->priv);
+}
+
+static void remmina_applet_menu_class_init(RemminaAppletMenuClass *klass)
+{
+ TRACE_CALL(__func__);
+ remmina_applet_menu_signals[LAUNCH_ITEM_SIGNAL] = g_signal_new("launch-item", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaAppletMenuClass, launch_item), NULL, NULL,
+ g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, G_TYPE_OBJECT);
+ remmina_applet_menu_signals[EDIT_ITEM_SIGNAL] = g_signal_new("edit-item", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaAppletMenuClass, edit_item), NULL, NULL,
+ g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, G_TYPE_OBJECT);
+}
+
+static void remmina_applet_menu_init(RemminaAppletMenu *menu)
+{
+ TRACE_CALL(__func__);
+ menu->priv = g_new0(RemminaAppletMenuPriv, 1);
+
+ g_signal_connect(G_OBJECT(menu), "destroy", G_CALLBACK(remmina_applet_menu_destroy), NULL);
+}
+
+static void remmina_applet_menu_on_item_activate(RemminaAppletMenuItem *menuitem, RemminaAppletMenu *menu)
+{
+ TRACE_CALL(__func__);
+ g_signal_emit(G_OBJECT(menu), remmina_applet_menu_signals[LAUNCH_ITEM_SIGNAL], 0, menuitem);
+}
+
+static GtkWidget*
+remmina_applet_menu_add_group(GtkWidget *menu, const gchar *group, gint position, RemminaAppletMenuItem *menuitem,
+ GtkWidget **groupmenuitem)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+ GtkWidget *submenu;
+
+ widget = gtk_menu_item_new_with_label(group);
+ gtk_widget_show(widget);
+
+ g_object_set_data_full(G_OBJECT(widget), "group", g_strdup(group), g_free);
+ g_object_set_data(G_OBJECT(widget), "count", GINT_TO_POINTER(0));
+ if (groupmenuitem) {
+ *groupmenuitem = widget;
+ }
+ if (position < 0) {
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), widget);
+ }else {
+ gtk_menu_shell_insert(GTK_MENU_SHELL(menu), widget, position);
+ }
+
+ submenu = gtk_menu_new();
+ gtk_widget_show(submenu);
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(widget), submenu);
+
+ return submenu;
+}
+
+static void remmina_applet_menu_increase_group_count(GtkWidget *widget)
+{
+ TRACE_CALL(__func__);
+ gint cnt;
+ gchar *s;
+
+ cnt = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "count")) + 1;
+ g_object_set_data(G_OBJECT(widget), "count", GINT_TO_POINTER(cnt));
+ s = g_strdup_printf("%s (%i)", (const gchar*)g_object_get_data(G_OBJECT(widget), "group"), cnt);
+ gtk_menu_item_set_label(GTK_MENU_ITEM(widget), s);
+ g_free(s);
+}
+
+void remmina_applet_menu_register_item(RemminaAppletMenu *menu, RemminaAppletMenuItem *menuitem)
+{
+ TRACE_CALL(__func__);
+ g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(remmina_applet_menu_on_item_activate), menu);
+}
+
+void remmina_applet_menu_add_item(RemminaAppletMenu *menu, RemminaAppletMenuItem *menuitem)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *submenu;
+ GtkWidget *groupmenuitem;
+ GtkMenuItem *submenuitem;
+ gchar *s, *p1, *p2, *mstr;
+ GList *childs, *child;
+ gint position;
+
+ submenu = GTK_WIDGET(menu);
+ s = g_strdup(menuitem->group);
+ p1 = s;
+ p2 = p1 ? strchr(p1, '/') : NULL;
+ if (p2)
+ *p2++ = '\0';
+ while (p1 && p1[0]) {
+ groupmenuitem = NULL;
+ childs = gtk_container_get_children(GTK_CONTAINER(submenu));
+ position = -1;
+ for (child = g_list_first(childs); child; child = g_list_next(child)) {
+ if (!GTK_IS_MENU_ITEM(child->data))
+ continue;
+ position++;
+ submenuitem = GTK_MENU_ITEM(child->data);
+ if (gtk_menu_item_get_submenu(submenuitem)) {
+ mstr = (gchar*)g_object_get_data(G_OBJECT(submenuitem), "group");
+ if (g_strcmp0(p1, mstr) == 0) {
+ /* Found existing group menu */
+ submenu = gtk_menu_item_get_submenu(submenuitem);
+ groupmenuitem = GTK_WIDGET(submenuitem);
+ break;
+ }else {
+ /* Redo comparison ignoring case and respecting international
+ * collation, to set menu sort order */
+ if (strcoll(p1, mstr) < 0) {
+ submenu = remmina_applet_menu_add_group(submenu, p1, position, menuitem,
+ &groupmenuitem);
+ break;
+ }
+ }
+ }else {
+ submenu = remmina_applet_menu_add_group(submenu, p1, position, menuitem, &groupmenuitem);
+ break;
+ }
+
+ }
+
+ if (!child) {
+ submenu = remmina_applet_menu_add_group(submenu, p1, -1, menuitem, &groupmenuitem);
+ }
+ g_list_free(childs);
+ if (groupmenuitem && !menu->priv->hide_count) {
+ remmina_applet_menu_increase_group_count(groupmenuitem);
+ }
+ p1 = p2;
+ p2 = p1 ? strchr(p1, '/') : NULL;
+ if (p2)
+ *p2++ = '\0';
+ }
+ g_free(s);
+
+ childs = gtk_container_get_children(GTK_CONTAINER(submenu));
+ position = -1;
+ for (child = g_list_first(childs); child; child = g_list_next(child)) {
+ if (!GTK_IS_MENU_ITEM(child->data))
+ continue;
+ position++;
+ submenuitem = GTK_MENU_ITEM(child->data);
+ if (gtk_menu_item_get_submenu(submenuitem))
+ continue;
+ if (!REMMINA_IS_APPLET_MENU_ITEM(submenuitem))
+ continue;
+ if (strcoll(menuitem->name, REMMINA_APPLET_MENU_ITEM(submenuitem)->name) <= 0) {
+ gtk_menu_shell_insert(GTK_MENU_SHELL(submenu), GTK_WIDGET(menuitem), position);
+ break;
+ }
+ }
+ if (!child) {
+ gtk_menu_shell_append(GTK_MENU_SHELL(submenu), GTK_WIDGET(menuitem));
+ }
+ g_list_free(childs);
+ remmina_applet_menu_register_item(menu, menuitem);
+}
+
+GtkWidget*
+remmina_applet_menu_new(void)
+{
+ TRACE_CALL(__func__);
+ RemminaAppletMenu *menu;
+
+ menu = REMMINA_APPLET_MENU(g_object_new(REMMINA_TYPE_APPLET_MENU, NULL));
+
+ return GTK_WIDGET(menu);
+}
+
+void remmina_applet_menu_set_hide_count(RemminaAppletMenu *menu, gboolean hide_count)
+{
+ TRACE_CALL(__func__);
+ menu->priv->hide_count = hide_count;
+}
+
+void remmina_applet_menu_populate(RemminaAppletMenu *menu)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *menuitem;
+ gchar filename[MAX_PATH_LEN];
+ GDir *dir;
+ gchar *remmina_data_dir;
+ const gchar *name;
+ gint count = 0;
+
+ gboolean new_ontop = remmina_pref.applet_new_ontop;
+
+ remmina_data_dir = remmina_file_get_datadir();
+ dir = g_dir_open(remmina_data_dir, 0, NULL);
+ if (dir != NULL) {
+ /* Iterate all remote desktop profiles */
+ while ((name = g_dir_read_name(dir)) != NULL) {
+ if (!g_str_has_suffix(name, ".remmina"))
+ continue;
+ g_snprintf(filename, sizeof(filename), "%s/%s", remmina_data_dir, name);
+
+ menuitem = remmina_applet_menu_item_new(REMMINA_APPLET_MENU_ITEM_FILE, filename);
+ if (menuitem != NULL) {
+ remmina_applet_menu_add_item(menu, REMMINA_APPLET_MENU_ITEM(menuitem));
+ gtk_widget_show(menuitem);
+ count++;
+ }
+ }
+ g_dir_close(dir);
+ if (count > 0) {
+ /* Separator */
+ menuitem = gtk_separator_menu_item_new();
+ gtk_widget_show(menuitem);
+ if (new_ontop)
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem);
+ else
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ }
+ }
+ g_free(remmina_data_dir);
+}
+
diff --git a/src/remmina_applet_menu.h b/src/remmina_applet_menu.h
new file mode 100644
index 0000000..1558bca
--- /dev/null
+++ b/src/remmina_applet_menu.h
@@ -0,0 +1,80 @@
+/*
+ * 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
+
+#include <gtk/gtk.h>
+#include "remmina_applet_menu_item.h"
+
+G_BEGIN_DECLS
+
+#define REMMINA_TYPE_APPLET_MENU (remmina_applet_menu_get_type())
+#define REMMINA_APPLET_MENU(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_APPLET_MENU, RemminaAppletMenu))
+#define REMMINA_APPLET_MENU_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_APPLET_MENU, RemminaAppletMenuClass))
+#define REMMINA_IS_APPLET_MENU(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_APPLET_MENU))
+#define REMMINA_IS_APPLET_MENU_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_APPLET_MENU))
+#define REMMINA_APPLET_MENU_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_APPLET_MENU, RemminaAppletMenuClass))
+
+typedef enum {
+ REMMINA_APPLET_MENU_NEW_CONNECTION_NONE,
+ REMMINA_APPLET_MENU_NEW_CONNECTION_TOP,
+ REMMINA_APPLET_MENU_NEW_CONNECTION_BOTTOM
+} RemminaAppletMenuNewConnectionType;
+
+typedef struct _RemminaAppletMenuPriv RemminaAppletMenuPriv;
+
+typedef struct _RemminaAppletMenu {
+ GtkMenu menu;
+
+ RemminaAppletMenuPriv * priv;
+} RemminaAppletMenu;
+
+typedef struct _RemminaAppletMenuClass {
+ GtkMenuClass parent_class;
+
+ void (*launch_item)(RemminaAppletMenu *menu);
+ void (*edit_item)(RemminaAppletMenu *menu);
+} RemminaAppletMenuClass;
+
+GType remmina_applet_menu_get_type(void)
+G_GNUC_CONST;
+
+void remmina_applet_menu_register_item(RemminaAppletMenu *menu, RemminaAppletMenuItem *menuitem);
+void remmina_applet_menu_add_item(RemminaAppletMenu *menu, RemminaAppletMenuItem *menuitem);
+GtkWidget *remmina_applet_menu_new(void);
+void remmina_applet_menu_set_hide_count(RemminaAppletMenu *menu, gboolean hide_count);
+void remmina_applet_menu_populate(RemminaAppletMenu *menu);
+
+G_END_DECLS
diff --git a/src/remmina_applet_menu_item.c b/src/remmina_applet_menu_item.c
new file mode 100644
index 0000000..59da0db
--- /dev/null
+++ b/src/remmina_applet_menu_item.c
@@ -0,0 +1,199 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-2010 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 <glib/gi18n.h>
+#include <glib/gprintf.h>
+#include <string.h>
+#include <stdarg.h>
+#include "config.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_applet_menu_item.h"
+#include "remmina/remmina_trace_calls.h"
+
+G_DEFINE_TYPE( RemminaAppletMenuItem, remmina_applet_menu_item, GTK_TYPE_MENU_ITEM)
+
+#define IS_EMPTY(s) ((!s) || (s[0] == 0))
+
+static void remmina_applet_menu_item_destroy(RemminaAppletMenuItem* item, gpointer data)
+{
+ TRACE_CALL(__func__);
+ g_free(item->filename);
+ g_free(item->name);
+ g_free(item->group);
+ g_free(item->protocol);
+ g_free(item->server);
+}
+
+static void remmina_applet_menu_item_class_init(RemminaAppletMenuItemClass* klass)
+{
+ TRACE_CALL(__func__);
+}
+
+static void remmina_applet_menu_item_init(RemminaAppletMenuItem* item)
+{
+ TRACE_CALL(__func__);
+ item->filename = NULL;
+ item->name = NULL;
+ item->group = NULL;
+ item->protocol = NULL;
+ item->server = NULL;
+ item->ssh_tunnel_enabled = FALSE;
+ g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remmina_applet_menu_item_destroy), NULL);
+}
+
+GtkWidget* remmina_applet_menu_item_new(RemminaAppletMenuItemType item_type, ...)
+{
+ TRACE_CALL(__func__);
+ va_list ap;
+ RemminaAppletMenuItem* item;
+ GKeyFile* gkeyfile;
+ GtkWidget* widget;
+ GtkWidget* box;
+ GtkWidget* icon;
+
+ va_start(ap, item_type);
+
+ item = REMMINA_APPLET_MENU_ITEM(g_object_new(REMMINA_TYPE_APPLET_MENU_ITEM, NULL));
+
+ item->item_type = item_type;
+
+ switch (item_type) {
+ case REMMINA_APPLET_MENU_ITEM_FILE:
+ item->filename = g_strdup(va_arg(ap, const gchar*));
+
+ /* Load the file */
+ gkeyfile = g_key_file_new();
+
+ if (!g_key_file_load_from_file(gkeyfile, item->filename, G_KEY_FILE_NONE, NULL)) {
+ g_key_file_free(gkeyfile);
+ va_end(ap);
+ return NULL;
+ }
+
+ item->name = g_key_file_get_string(gkeyfile, "remmina", "name", NULL);
+ item->group = g_key_file_get_string(gkeyfile, "remmina", "group", NULL);
+ item->protocol = g_key_file_get_string(gkeyfile, "remmina", "protocol", NULL);
+ item->server = g_key_file_get_string(gkeyfile, "remmina", "server", NULL);
+ item->ssh_tunnel_enabled = g_key_file_get_boolean(gkeyfile, "remmina", "ssh_tunnel_enabled", NULL);
+
+ g_key_file_free(gkeyfile);
+
+ if (item->name == NULL) {
+ g_printf("WARNING: missing name= line in file %s. Skipping.\n", item->filename);
+ va_end(ap);
+ return NULL;
+ }
+
+ break;
+
+ case REMMINA_APPLET_MENU_ITEM_DISCOVERED:
+ item->name = g_strdup(va_arg(ap, const gchar *));
+ item->group = g_strdup(_("Discovered"));
+ item->protocol = g_strdup("VNC");
+ break;
+
+ case REMMINA_APPLET_MENU_ITEM_NEW:
+ item->name = g_strdup(_("New Connection"));
+ break;
+ }
+
+ va_end(ap);
+
+
+ /* Get the icon based on the protocol */
+ const gchar* icon_name;
+ RemminaProtocolPlugin *plugin;
+ plugin = (RemminaProtocolPlugin *)remmina_plugin_manager_get_plugin(REMMINA_PLUGIN_TYPE_PROTOCOL,
+ item->protocol);
+ if (!plugin) {
+ icon_name = g_strconcat(REMMINA_APP_ID, "-symbolic", NULL);
+ } else {
+ icon_name = item->ssh_tunnel_enabled ? plugin->icon_name_ssh : plugin->icon_name;
+ }
+ icon = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_MENU);
+
+ /* Create the label */
+ widget = gtk_label_new(item->name);
+ box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ gtk_widget_show(widget);
+ gtk_widget_show(icon);
+ gtk_widget_show(box);
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_container_add(GTK_CONTAINER(box), icon);
+ gtk_container_add(GTK_CONTAINER(box), widget);
+ gtk_container_add(GTK_CONTAINER(item), box);
+
+ if (item->server) {
+ gtk_widget_set_tooltip_text(GTK_WIDGET(item), item->server);
+ }
+
+ return GTK_WIDGET(item);
+}
+
+gint remmina_applet_menu_item_compare(gconstpointer a, gconstpointer b, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gint cmp;
+ RemminaAppletMenuItem* itema;
+ RemminaAppletMenuItem* itemb;
+
+ /* Passed in parameters are pointers to pointers */
+ itema = REMMINA_APPLET_MENU_ITEM(*((void**)a));
+ itemb = REMMINA_APPLET_MENU_ITEM(*((void**)b));
+
+ /* Put ungrouped items to the last */
+ if (IS_EMPTY(itema->group) && !IS_EMPTY(itemb->group))
+ return 1;
+ if (!IS_EMPTY(itema->group) && IS_EMPTY(itemb->group))
+ return -1;
+
+ /* Put discovered items the last group */
+ if (itema->item_type == REMMINA_APPLET_MENU_ITEM_DISCOVERED && itemb->item_type != REMMINA_APPLET_MENU_ITEM_DISCOVERED)
+ return 1;
+ if (itema->item_type != REMMINA_APPLET_MENU_ITEM_DISCOVERED && itemb->item_type == REMMINA_APPLET_MENU_ITEM_DISCOVERED)
+ return -1;
+
+ if (itema->item_type != REMMINA_APPLET_MENU_ITEM_DISCOVERED && !IS_EMPTY(itema->group)) {
+ cmp = g_strcmp0(itema->group, itemb->group);
+
+ if (cmp != 0)
+ return cmp;
+ }
+
+ return g_strcmp0(itema->name, itemb->name);
+}
diff --git a/src/remmina_applet_menu_item.h b/src/remmina_applet_menu_item.h
new file mode 100644
index 0000000..9dbceed
--- /dev/null
+++ b/src/remmina_applet_menu_item.h
@@ -0,0 +1,75 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define REMMINA_TYPE_APPLET_MENU_ITEM (remmina_applet_menu_item_get_type())
+#define REMMINA_APPLET_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_APPLET_MENU_ITEM, RemminaAppletMenuItem))
+#define REMMINA_APPLET_MENU_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_APPLET_MENU_ITEM, RemminaAppletMenuItemClass))
+#define REMMINA_IS_APPLET_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_APPLET_MENU_ITEM))
+#define REMMINA_IS_APPLET_MENU_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_APPLET_MENU_ITEM))
+#define REMMINA_APPLET_MENU_ITEM_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_APPLET_MENU_ITEM, RemminaAppletMenuItemClass))
+
+typedef enum {
+ REMMINA_APPLET_MENU_ITEM_FILE, REMMINA_APPLET_MENU_ITEM_NEW, REMMINA_APPLET_MENU_ITEM_DISCOVERED
+} RemminaAppletMenuItemType;
+
+typedef struct _RemminaAppletMenuItem {
+ GtkMenuItem menu_item;
+
+ RemminaAppletMenuItemType item_type;
+ gchar * filename;
+ gchar * name;
+ gchar * group;
+ gchar * protocol;
+ gchar * server;
+ gboolean ssh_tunnel_enabled;
+} RemminaAppletMenuItem;
+
+typedef struct _RemminaAppletMenuItemClass {
+ GtkMenuItemClass parent_class;
+} RemminaAppletMenuItemClass;
+
+GType remmina_applet_menu_item_get_type(void)
+G_GNUC_CONST;
+
+GtkWidget *remmina_applet_menu_item_new(RemminaAppletMenuItemType item_type, ...);
+gint remmina_applet_menu_item_compare(gconstpointer a, gconstpointer b, gpointer user_data);
+
+G_END_DECLS
diff --git a/src/remmina_avahi.c b/src/remmina_avahi.c
new file mode 100644
index 0000000..e51cfbf
--- /dev/null
+++ b/src/remmina_avahi.c
@@ -0,0 +1,300 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 "config.h"
+#include "remmina_avahi.h"
+#include "remmina/remmina_trace_calls.h"
+
+#ifdef HAVE_LIBAVAHI_CLIENT
+
+#include <avahi-client/client.h>
+#include <avahi-client/lookup.h>
+#include <avahi-common/simple-watch.h>
+#include <avahi-common/malloc.h>
+#include <avahi-common/error.h>
+
+struct _RemminaAvahiPriv {
+ AvahiSimplePoll* simple_poll;
+ AvahiClient* client;
+ AvahiServiceBrowser* sb;
+ guint iterate_handler;
+ gboolean has_event;
+};
+
+static void
+remmina_avahi_resolve_callback(
+ AvahiServiceResolver* r,
+ AVAHI_GCC_UNUSED AvahiIfIndex interface,
+ AVAHI_GCC_UNUSED AvahiProtocol protocol,
+ AvahiResolverEvent event,
+ const char* name,
+ const char* type,
+ const char* domain,
+ const char* host_name,
+ const AvahiAddress* address,
+ uint16_t port,
+ AvahiStringList* txt,
+ AvahiLookupResultFlags flags,
+ AVAHI_GCC_UNUSED void* userdata)
+{
+ TRACE_CALL(__func__);
+ gchar* key;
+ gchar* value;
+ RemminaAvahi* ga = (RemminaAvahi*)userdata;
+
+ assert(r);
+
+ ga->priv->has_event = TRUE;
+
+ switch (event) {
+ case AVAHI_RESOLVER_FAILURE:
+ g_print("(remmina-applet avahi-resolver) Failed to resolve service '%s' of type '%s' in domain '%s': %s\n",
+ name, type, domain, avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))));
+ break;
+
+ case AVAHI_RESOLVER_FOUND:
+ key = g_strdup_printf("%s,%s,%s", name, type, domain);
+ if (g_hash_table_lookup(ga->discovered_services, key)) {
+ g_free(key);
+ break;
+ }
+ value = g_strdup_printf("[%s]:%i", host_name, port);
+ g_hash_table_insert(ga->discovered_services, key, value);
+ /* key and value will be freed with g_free when the has table is freed */
+
+ g_print("(remmina-applet avahi-resolver) Added service '%s'\n", value);
+
+ break;
+ }
+
+ avahi_service_resolver_free(r);
+}
+
+static void
+remmina_avahi_browse_callback(
+ AvahiServiceBrowser* b,
+ AvahiIfIndex interface,
+ AvahiProtocol protocol,
+ AvahiBrowserEvent event,
+ const char* name,
+ const char* type,
+ const char* domain,
+ AVAHI_GCC_UNUSED AvahiLookupResultFlags flags,
+ void* userdata)
+{
+ TRACE_CALL(__func__);
+ gchar* key;
+ RemminaAvahi* ga = (RemminaAvahi*)userdata;
+
+ assert(b);
+
+ ga->priv->has_event = TRUE;
+
+ switch (event) {
+ case AVAHI_BROWSER_FAILURE:
+ g_print("(remmina-applet avahi-browser) %s\n",
+ avahi_strerror(avahi_client_errno(avahi_service_browser_get_client(b))));
+ return;
+
+ case AVAHI_BROWSER_NEW:
+ key = g_strdup_printf("%s,%s,%s", name, type, domain);
+ if (g_hash_table_lookup(ga->discovered_services, key)) {
+ g_free(key);
+ break;
+ }
+ g_free(key);
+
+ g_print("(remmina-applet avahi-browser) Found service '%s' of type '%s' in domain '%s'\n", name, type, domain);
+
+ if (!(avahi_service_resolver_new(ga->priv->client, interface, protocol, name, type, domain,
+ AVAHI_PROTO_UNSPEC, 0, remmina_avahi_resolve_callback, ga))) {
+ g_print("(remmina-applet avahi-browser) Failed to resolve service '%s': %s\n",
+ name, avahi_strerror(avahi_client_errno(ga->priv->client)));
+ }
+ break;
+
+ case AVAHI_BROWSER_REMOVE:
+ g_print("(remmina-applet avahi-browser) Removed service '%s' of type '%s' in domain '%s'\n", name, type, domain);
+ key = g_strdup_printf("%s,%s,%s", name, type, domain);
+ g_hash_table_remove(ga->discovered_services, key);
+ g_free(key);
+ break;
+
+ case AVAHI_BROWSER_ALL_FOR_NOW:
+ case AVAHI_BROWSER_CACHE_EXHAUSTED:
+ break;
+ }
+}
+
+static void remmina_avahi_client_callback(AvahiClient* c, AvahiClientState state, AVAHI_GCC_UNUSED void * userdata)
+{
+ TRACE_CALL(__func__);
+ RemminaAvahi* ga = (RemminaAvahi*)userdata;
+
+ ga->priv->has_event = TRUE;
+
+ if (state == AVAHI_CLIENT_FAILURE) {
+ g_print("(remmina-applet avahi) Server connection failure: %s\n", avahi_strerror(avahi_client_errno(c)));
+ }
+}
+
+static gboolean remmina_avahi_iterate(RemminaAvahi* ga)
+{
+ TRACE_CALL(__func__);
+ while (TRUE) {
+ /* Call the iteration until no further events */
+ ga->priv->has_event = FALSE;
+ avahi_simple_poll_iterate(ga->priv->simple_poll, 0);
+ if (!ga->priv->has_event)
+ break;
+ }
+
+ return TRUE;
+}
+
+RemminaAvahi* remmina_avahi_new(void)
+{
+ TRACE_CALL(__func__);
+ RemminaAvahi* ga;
+
+ ga = g_new(RemminaAvahi, 1);
+ ga->discovered_services = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+ ga->started = FALSE;
+ ga->priv = g_new(RemminaAvahiPriv, 1);
+ ga->priv->simple_poll = NULL;
+ ga->priv->client = NULL;
+ ga->priv->sb = NULL;
+ ga->priv->iterate_handler = 0;
+ ga->priv->has_event = FALSE;
+
+ return ga;
+}
+
+void remmina_avahi_start(RemminaAvahi* ga)
+{
+ TRACE_CALL(__func__);
+ int error;
+
+ if (ga->started)
+ return;
+
+ ga->started = TRUE;
+
+ ga->priv->simple_poll = avahi_simple_poll_new();
+ if (!ga->priv->simple_poll) {
+ g_print("Failed to create simple poll object.\n");
+ return;
+ }
+
+ ga->priv->client = avahi_client_new(avahi_simple_poll_get(ga->priv->simple_poll), 0, remmina_avahi_client_callback, ga,
+ &error);
+ if (!ga->priv->client) {
+ g_print("Failed to create client: %s\n", avahi_strerror(error));
+ return;
+ }
+
+ /** @todo Customize the default domain here */
+ ga->priv->sb = avahi_service_browser_new(ga->priv->client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, "_rfb._tcp", NULL, 0,
+ remmina_avahi_browse_callback, ga);
+ if (!ga->priv->sb) {
+ g_print("Failed to create service browser: %s\n", avahi_strerror(avahi_client_errno(ga->priv->client)));
+ return;
+ }
+
+ ga->priv->iterate_handler = g_timeout_add(5000, (GSourceFunc)remmina_avahi_iterate, ga);
+}
+
+void remmina_avahi_stop(RemminaAvahi* ga)
+{
+ TRACE_CALL(__func__);
+ g_hash_table_remove_all(ga->discovered_services);
+ if (ga->priv->iterate_handler) {
+ g_source_remove(ga->priv->iterate_handler);
+ ga->priv->iterate_handler = 0;
+ }
+ if (ga->priv->sb) {
+ avahi_service_browser_free(ga->priv->sb);
+ ga->priv->sb = NULL;
+ }
+ if (ga->priv->client) {
+ avahi_client_free(ga->priv->client);
+ ga->priv->client = NULL;
+ }
+ if (ga->priv->simple_poll) {
+ avahi_simple_poll_free(ga->priv->simple_poll);
+ ga->priv->simple_poll = NULL;
+ }
+ ga->started = FALSE;
+}
+
+void remmina_avahi_free(RemminaAvahi* ga)
+{
+ TRACE_CALL(__func__);
+ if (ga == NULL)
+ return;
+
+ remmina_avahi_stop(ga);
+
+ g_free(ga->priv);
+ g_hash_table_destroy(ga->discovered_services);
+ g_free(ga);
+}
+
+#else
+
+RemminaAvahi* remmina_avahi_new(void)
+{
+ TRACE_CALL(__func__);
+ return NULL;
+}
+
+void remmina_avahi_start(RemminaAvahi* ga)
+{
+ TRACE_CALL(__func__);
+}
+
+void remmina_avahi_stop(RemminaAvahi* ga)
+{
+ TRACE_CALL(__func__);
+}
+
+void remmina_avahi_free(RemminaAvahi* ga)
+{
+ TRACE_CALL(__func__);
+}
+
+#endif
+
diff --git a/src/remmina_avahi.h b/src/remmina_avahi.h
new file mode 100644
index 0000000..70686cd
--- /dev/null
+++ b/src/remmina_avahi.h
@@ -0,0 +1,56 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+typedef struct _RemminaAvahiPriv RemminaAvahiPriv;
+
+typedef struct _RemminaAvahi {
+ GHashTable * discovered_services;
+ gboolean started;
+
+ RemminaAvahiPriv * priv;
+} RemminaAvahi;
+
+RemminaAvahi *remmina_avahi_new(void);
+void remmina_avahi_start(RemminaAvahi *ga);
+void remmina_avahi_stop(RemminaAvahi *ga);
+void remmina_avahi_free(RemminaAvahi *ga);
+
+G_END_DECLS
diff --git a/src/remmina_bug_report.c b/src/remmina_bug_report.c
new file mode 100644
index 0000000..7512223
--- /dev/null
+++ b/src/remmina_bug_report.c
@@ -0,0 +1,411 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-2011 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, 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.
+ *
+ */
+
+#include <glib/gi18n.h>
+#include <string.h>
+#include "remmina_log.h"
+#include "remmina_bug_report.h"
+#include "remmina_public.h"
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_info.h"
+#include "remmina_curl_connector.h"
+#include "remmina_utils.h"
+
+#if !JSON_CHECK_VERSION(1, 2, 0)
+ #define json_node_unref(x) json_node_free(x)
+#endif
+
+#define BUG_REPORT_UPLOAD_URL "https://info.remmina.org/bug_report/bug_report"
+static RemminaBugReportDialog *remmina_bug_report_dialog;
+
+#define GET_OBJECT(object_name) gtk_builder_get_object(remmina_bug_report_dialog->builder, object_name)
+
+#define TEMP_LOG_TEXT_FILE_NAME "remmina_shorter_logs.log"
+#define COMPRESSED_LOG_FILE_NAME "remmina_log_file.gz"
+#define LOG_RECENT_WINDOW 4000
+#define MAX_DESCRIPTION_LENGTH 8000
+const char *bug_report_preview_text_md = \
+"## Problem Description\n"
+"\n"
+"Write a detailed description of the problem.\n"
+"\n"
+"## What is the expected correct behavior?\n"
+"\n"
+"(What you want to see instead.)\n"
+"\n"
+"## Remote System Description\n"
+"\n"
+"* Server (OS name and version):\n"
+"* Special notes regarding the remote system (i.e. gateways, tunnel, etc.):\n"
+"\n"
+"## Debugging\n"
+"\n"
+"Please check the Include Debug Data box to allow for debug log to be sent with this bug report\n"
+"\n"
+"## Local System Description\n"
+"\n"
+"* Client (OS name and version):\n"
+"* Remmina version ( ```remmina --version``` ):\n"
+"* Installation(s):\n"
+" - [ ] Distribution package.\n"
+" - [ ] PPA.\n"
+" - [ ] Snap.\n"
+" - [ ] Flatpak.\n"
+" - [ ] Compiled from sources.\n"
+" - [ ] Other - detail:\n"
+"* Desktop environment (GNOME, Unity, KDE, ..):\n"
+"* Plugin(s):\n"
+" - [ ] RDP - freerdp version ( ```xfreerdp --version``` ):\n"
+" - [ ] VNC\n"
+" - [ ] SSH\n"
+" - [ ] SFTP\n"
+" - [ ] SPICE\n"
+" - [ ] WWW\n"
+" - [ ] EXEC\n"
+" - [ ] Other (please specify):\n"
+"* GTK backend (Wayland, Xorg):\n"
+"\n";
+
+
+/* Show the bug report dialog from the file ui/remmina_bug_report.glade */
+void remmina_bug_report_open(GtkWindow *parent)
+{
+ TRACE_CALL(__func__);
+ GtkTextBuffer* bug_report_preview_text_buffer;
+ remmina_bug_report_dialog = g_new0(RemminaBugReportDialog, 1);
+
+ remmina_bug_report_dialog->builder = remmina_public_gtk_builder_new_from_resource ("/org/remmina/Remmina/src/../data/ui/remmina_bug_report.glade");
+ remmina_bug_report_dialog->dialog = GTK_WIDGET(gtk_builder_get_object(remmina_bug_report_dialog->builder, "RemminaBugReportDialog"));
+ remmina_bug_report_dialog->bug_report_submit_button = GTK_BUTTON(GET_OBJECT("bug_report_submit_button"));
+ remmina_bug_report_dialog->bug_report_name_entry = GTK_ENTRY(GET_OBJECT("bug_report_name_entry"));
+ remmina_bug_report_dialog->bug_report_email_entry = GTK_ENTRY(GET_OBJECT("bug_report_email_entry"));
+ remmina_bug_report_dialog->bug_report_title_entry = GTK_ENTRY(GET_OBJECT("bug_report_title_entry"));
+ remmina_bug_report_dialog->bug_report_description_textview = GTK_TEXT_VIEW(GET_OBJECT("bug_report_description_textview"));
+ remmina_bug_report_dialog->bug_report_submit_status_label = GTK_LABEL(GET_OBJECT("bug_report_submit_status_label"));
+ remmina_bug_report_dialog->bug_report_debug_data_check_button = GTK_CHECK_BUTTON(GET_OBJECT("bug_report_debug_data_check_button"));
+ remmina_bug_report_dialog->bug_report_include_system_info_check_button = GTK_CHECK_BUTTON(GET_OBJECT("bug_report_include_system_info_check_button"));
+
+ // Set bug report markdown text in bug description window
+ bug_report_preview_text_buffer = gtk_text_buffer_new(NULL);
+ gtk_text_buffer_set_text(bug_report_preview_text_buffer, bug_report_preview_text_md, strlen(bug_report_preview_text_md));
+ gtk_text_view_set_buffer(remmina_bug_report_dialog->bug_report_description_textview, bug_report_preview_text_buffer);
+
+ if (parent) {
+ gtk_window_set_transient_for(GTK_WINDOW(remmina_bug_report_dialog->dialog), parent);
+ gtk_window_set_destroy_with_parent(GTK_WINDOW(remmina_bug_report_dialog->dialog), TRUE);
+ }
+
+ g_signal_connect(remmina_bug_report_dialog->bug_report_submit_button, "clicked",
+ G_CALLBACK(remmina_bug_report_dialog_on_action_submit), (gpointer)remmina_bug_report_dialog);
+
+ gtk_window_present(GTK_WINDOW(remmina_bug_report_dialog->dialog));
+ g_object_unref(bug_report_preview_text_buffer);
+}
+
+
+void remmina_bug_report_dialog_on_action_submit(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ JsonGenerator *g;
+ JsonObject *o;
+ gchar *b;
+
+ REMMINA_DEBUG("Submit Button Clicked. Uploading Bug Report data.");
+
+ gchar *markup = GRAY_TEXT("Sending Bug Report...");
+ gtk_label_set_markup(remmina_bug_report_dialog->bug_report_submit_status_label, markup);
+ g_free(markup);
+
+ // Store bug report info in JSON blob and encode before submitting
+ JsonNode *bug_report_data = remmina_bug_report_get_all();
+
+ if (bug_report_data == NULL || (o = json_node_get_object(bug_report_data)) == NULL) {
+ REMMINA_DEBUG("Failed to grab bug report data, no request is sent");
+ gchar *markup = RED_TEXT("Failure: Unable to generate bug report message. Please try again.");
+ gtk_label_set_markup(remmina_bug_report_dialog->bug_report_submit_status_label, markup);
+ g_free(markup);
+ json_node_unref(bug_report_data);
+ return;
+ } else if (strcmp(json_object_get_string_member(json_node_get_object(bug_report_data), "Name"), "") == 0) {
+ REMMINA_DEBUG("No text in name entry of bug report data, no request is sent");
+ gchar *markup = RED_TEXT("Failure: Name/Username is required. Please enter a Name/Username.");
+ gtk_label_set_markup(remmina_bug_report_dialog->bug_report_submit_status_label, markup);
+ g_free(markup);
+ json_node_unref(bug_report_data);
+ return;
+ } else {
+ gchar *email_text = g_strdup(json_object_get_string_member(json_node_get_object(bug_report_data), "Email"));
+ if (strcmp(email_text, "") == 0) {
+ REMMINA_DEBUG("No text in email entry of bug report data, no request is sent");
+ gchar *markup = RED_TEXT("Failure: Email is required. Please enter an email.");
+ gtk_label_set_markup(remmina_bug_report_dialog->bug_report_submit_status_label, markup);
+ g_free(markup);
+ g_free(email_text);
+ json_node_unref(bug_report_data);
+ return;
+ } else {
+ gchar *save_ptr;
+ gchar *check_for_at_symbol = strtok_r(email_text, "@", &save_ptr);
+ gchar *check_for_dot = strtok_r(NULL, ".", &save_ptr);
+ gchar *check_for_domain = strtok_r(NULL, "", &save_ptr);
+ if (check_for_at_symbol == NULL || check_for_dot == NULL || check_for_domain == NULL) {
+ REMMINA_DEBUG("Text in email entry of bug report data is not a valid email, no request is sent");
+ gchar *markup = RED_TEXT("Failure: A valid email is required. Email is missing a prefix or domain.");
+ gtk_label_set_markup(remmina_bug_report_dialog->bug_report_submit_status_label, markup);
+ g_free(markup);
+ g_free(email_text);
+ json_node_unref(bug_report_data);
+ return;
+ } else if (strpbrk(check_for_at_symbol, "@.") != NULL || strpbrk(check_for_dot, "@.") != NULL || strpbrk(check_for_domain, "@.") != NULL) {
+ REMMINA_DEBUG("Text in email entry of bug report data is not a valid email, no request is sent");
+ gchar *markup = RED_TEXT("Failure: A valid email is required. Email contains extra @ and . characters.");
+ gtk_label_set_markup(remmina_bug_report_dialog->bug_report_submit_status_label, markup);
+ g_free(markup);
+ g_free(email_text);
+ json_node_unref(bug_report_data);
+ return;
+ }
+ }
+ g_free(email_text);
+ if (strcmp(json_object_get_string_member(json_node_get_object(bug_report_data), "Bug_Title"), "") == 0) {
+ REMMINA_DEBUG("No text in bug title entry of bug report data, no request is sent");
+ gchar *markup = RED_TEXT("Failure: Bug Title is required. Please enter a Bug Title.");
+ gtk_label_set_markup(remmina_bug_report_dialog->bug_report_submit_status_label, markup);
+ g_free(markup);
+ json_node_unref(bug_report_data);
+ return;
+ } else if (strcmp(json_object_get_string_member(json_node_get_object(bug_report_data), "Bug_Description"), "") == 0 ||
+ strcmp(json_object_get_string_member(json_node_get_object(bug_report_data), "Bug_Description"), bug_report_preview_text_md) == 0) {
+ REMMINA_DEBUG("No text in bug description of bug report data, no request is sent");
+ gchar *markup = RED_TEXT("Failure: Bug Description is required. Please fill out the template.");
+ gtk_label_set_markup(remmina_bug_report_dialog->bug_report_submit_status_label, markup);
+ g_free(markup);
+ json_node_unref(bug_report_data);
+ return;
+ }
+ }
+
+ g = json_generator_new();
+ json_generator_set_root(g, bug_report_data);
+ b = json_generator_to_data(g, NULL);
+ g_object_unref(g);
+
+ remmina_curl_compose_message(b, "POST", BUG_REPORT_UPLOAD_URL, remmina_bug_report_dialog->bug_report_submit_status_label);
+ json_node_unref(bug_report_data);
+}
+
+JsonNode *remmina_bug_report_get_all()
+{
+ TRACE_CALL(__func__);
+ JsonBuilder *b_inner, *b_outer; // holds entire JSON blob
+ JsonGenerator *g;
+ JsonNode *n; // temp JSON node
+ gchar *unenc_b, *enc_b;
+ gchar *uid;
+
+ // Initialize JSON blob
+ b_outer = json_builder_new();
+ if (b_outer == NULL) {
+ g_object_unref(b_outer);
+ return NULL;
+ }
+ json_builder_begin_object(b_outer);
+
+ gboolean include_debug_data = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_bug_report_dialog->bug_report_debug_data_check_button));
+ gboolean include_system_info = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_bug_report_dialog->bug_report_include_system_info_check_button));
+
+ if (include_debug_data || include_system_info) {
+ b_inner = json_builder_new();
+ if (b_inner == NULL) {
+ g_object_unref(b_inner);
+ g_object_unref(b_outer);
+ return NULL;
+ }
+ json_builder_begin_object(b_inner);
+
+ if (include_system_info){
+ n = remmina_info_stats_get_all();
+
+ json_builder_set_member_name(b_outer, "System_Info_Enabled");
+ json_builder_add_string_value(b_outer, "ON");
+
+ json_builder_set_member_name(b_inner, "System_Info");
+ json_builder_add_value(b_inner, n);
+ }
+ else{
+ json_builder_set_member_name(b_outer, "System_Info_Enabled");
+ json_builder_add_string_value(b_outer, "OFF");
+ }
+
+ if (include_debug_data){
+ gboolean log_file_result;
+ gchar *log_text;
+ gsize log_length;
+ GError *log_read_error;
+ gchar *log_filename = g_build_filename(g_get_tmp_dir(), LOG_FILE_NAME, NULL);
+ log_file_result = g_file_get_contents(log_filename, &log_text, &log_length, &log_read_error);
+ g_free(log_filename);
+ if (!log_file_result && log_read_error == NULL) {
+ REMMINA_DEBUG("Failed to grab entire log text, read error: %s", log_read_error->message);
+ }
+ gboolean shorter_logs_result;
+ gchar *temp_log_filename = g_build_filename(g_get_tmp_dir(), TEMP_LOG_TEXT_FILE_NAME, NULL);
+ gchar *recent_log_text;
+ if (log_length <= LOG_RECENT_WINDOW) {
+ recent_log_text = log_text;
+ shorter_logs_result = g_file_set_contents(temp_log_filename, recent_log_text, log_length, NULL);
+ }
+ else {
+ recent_log_text = log_text + (log_length - LOG_RECENT_WINDOW);
+ shorter_logs_result = g_file_set_contents(temp_log_filename, recent_log_text, LOG_RECENT_WINDOW, NULL);
+ }
+ g_free(log_text);
+ if (!shorter_logs_result) {
+ REMMINA_DEBUG("Failed to write recent window of logs to temp file.");
+ }
+
+ gchar *comp_log_filename = g_build_filename(g_get_tmp_dir(), COMPRESSED_LOG_FILE_NAME, NULL);
+ GFile *in = g_file_new_for_path(temp_log_filename);
+ GFile *out = g_file_new_for_path(comp_log_filename);
+ // Clear out old compressed log file to prevent appending
+ g_file_delete(out, NULL, NULL);
+ remmina_compress_from_file_to_file(in, out);
+ g_free(temp_log_filename);
+
+ gboolean com_log_file_result;
+ gchar *compressed_logs;
+ gsize com_log_length;
+ GError *com_log_read_error;
+ com_log_file_result = g_file_get_contents(comp_log_filename, &compressed_logs, &com_log_length, &com_log_read_error);
+ n = json_node_alloc();
+ if (!com_log_file_result && com_log_read_error != NULL) {
+ REMMINA_DEBUG("Failed to grab entire compressed log text, read error: %s", com_log_read_error->message);
+ json_node_init_string(n, "");
+ }
+ else {
+ gchar *logs_base64 = g_base64_encode((guchar *)compressed_logs, com_log_length);
+ json_node_init_string(n, logs_base64);
+ g_free(logs_base64);
+ }
+
+ json_builder_set_member_name(b_outer, "Debug_Log_Enabled");
+ json_builder_add_string_value(b_outer, "ON");
+ json_builder_set_member_name(b_inner, "Compressed_Logs");
+ json_builder_add_value(b_inner, n);
+ g_free(compressed_logs);
+ g_free(comp_log_filename);
+
+ }
+ else{
+ json_builder_set_member_name(b_outer, "Debug_Log_Enabled");
+ json_builder_add_string_value(b_outer, "OFF");
+ }
+
+ // Wrap up by setting n to the final JSON blob
+ json_builder_end_object(b_inner);
+ n = json_builder_get_root(b_inner);
+ g_object_unref(b_inner);
+
+ g = json_generator_new();
+ json_generator_set_root(g, n);
+ json_node_unref(n);
+ unenc_b = json_generator_to_data(g, NULL); // unenc_b=bug report data
+ g_object_unref(g);
+
+ // Now base64 encode report data
+ enc_b = g_base64_encode((guchar *)unenc_b, strlen(unenc_b));
+ if (enc_b == NULL) {
+ g_object_unref(b_outer);
+ g_free(unenc_b);
+ REMMINA_DEBUG("Failed to encode inner JSON");
+ return NULL;
+ }
+ g_free(unenc_b);
+
+ json_builder_set_member_name(b_outer, "encdata");
+ json_builder_add_string_value(b_outer, enc_b);
+ g_free(enc_b);
+ }
+ else {
+ json_builder_set_member_name(b_outer, "System_Info_Enabled");
+ json_builder_add_string_value(b_outer, "OFF");
+ json_builder_set_member_name(b_outer, "Debug_Log_Enabled");
+ json_builder_add_string_value(b_outer, "OFF");
+ }
+
+ n = remmina_info_stats_get_uid();
+ uid = g_strdup(json_node_get_string(n));
+ json_builder_set_member_name(b_outer, "UID");
+ json_builder_add_string_value(b_outer, uid);
+
+ // Allocate new json node for each text entry and add to total blob
+ n = json_node_alloc();
+ json_node_init_string(n, gtk_entry_get_text(remmina_bug_report_dialog->bug_report_name_entry));
+ json_builder_set_member_name(b_outer, "Name");
+ json_builder_add_value(b_outer, n);
+
+ n = json_node_alloc();
+ json_node_init_string(n, gtk_entry_get_text(remmina_bug_report_dialog->bug_report_email_entry));
+ json_builder_set_member_name(b_outer, "Email");
+ json_builder_add_value(b_outer, n);
+
+ n = json_node_alloc();
+ json_node_init_string(n, gtk_entry_get_text(remmina_bug_report_dialog->bug_report_title_entry));
+ json_builder_set_member_name(b_outer, "Bug_Title");
+ json_builder_add_value(b_outer, n);
+
+ GtkTextBuffer *buffer;
+ gchar *description_text;
+ GtkTextIter start, end;
+ buffer = gtk_text_view_get_buffer(remmina_bug_report_dialog->bug_report_description_textview);
+ gtk_text_buffer_get_start_iter(buffer, &start);
+ gtk_text_buffer_get_iter_at_offset(buffer, &end, MAX_DESCRIPTION_LENGTH);
+ description_text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
+
+ n = json_node_alloc();
+ json_node_init_string(n, description_text);
+ json_builder_set_member_name(b_outer, "Bug_Description");
+ json_builder_add_value(b_outer, n);
+
+ json_builder_end_object(b_outer);
+ n = json_builder_get_root(b_outer);
+ g_object_unref(b_outer);
+
+ g_free(uid);
+
+ return n;
+}
diff --git a/src/remmina_bug_report.h b/src/remmina_bug_report.h
new file mode 100644
index 0000000..0969323
--- /dev/null
+++ b/src/remmina_bug_report.h
@@ -0,0 +1,61 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-2011 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-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.
+ *
+ */
+
+#pragma once
+#include <gtk/gtk.h>
+#include "json-glib/json-glib.h"
+
+typedef struct _RemminaBugReportDialog {
+ GtkBuilder *builder;
+ GtkWidget *dialog;
+ GtkButton *bug_report_submit_button;
+ GtkEntry *bug_report_name_entry;
+ GtkEntry *bug_report_email_entry;
+ GtkEntry *bug_report_title_entry;
+ GtkTextView *bug_report_description_textview;
+ GtkLabel *bug_report_submit_status_label;
+ GtkCheckButton *bug_report_debug_data_check_button;
+ GtkCheckButton *bug_report_include_system_info_check_button;
+} RemminaBugReportDialog;
+
+G_BEGIN_DECLS
+
+void remmina_bug_report_open(GtkWindow *parent);
+void remmina_bug_report_dialog_on_action_submit(GSimpleAction *action, GVariant *param, gpointer data);
+JsonNode *remmina_bug_report_get_all(void);
+
+G_END_DECLS
diff --git a/src/remmina_chat_window.c b/src/remmina_chat_window.c
new file mode 100644
index 0000000..da58fb4
--- /dev/null
+++ b/src/remmina_chat_window.c
@@ -0,0 +1,255 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <glib/gi18n.h>
+#include <gdk/gdkkeysyms.h>
+#include "remmina_chat_window.h"
+#include "remmina/remmina_trace_calls.h"
+
+G_DEFINE_TYPE( RemminaChatWindow, remmina_chat_window, GTK_TYPE_WINDOW)
+
+enum {
+ SEND_SIGNAL,
+ LAST_SIGNAL
+};
+
+static guint remmina_chat_window_signals[LAST_SIGNAL] = { 0 };
+
+static void remmina_chat_window_class_init(RemminaChatWindowClass* klass)
+{
+ TRACE_CALL(__func__);
+ remmina_chat_window_signals[SEND_SIGNAL] = g_signal_new("send", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaChatWindowClass, send), NULL, NULL,
+ g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING);
+}
+
+static void remmina_chat_window_init(RemminaChatWindow* window)
+{
+ TRACE_CALL(__func__);
+ window->history_text = NULL;
+ window->send_text = NULL;
+}
+
+static void remmina_chat_window_clear_send_text(GtkWidget* widget, RemminaChatWindow* window)
+{
+ TRACE_CALL(__func__);
+ GtkTextBuffer* buffer;
+
+ buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(window->send_text));
+ gtk_text_buffer_set_text(buffer, "", -1);
+ gtk_widget_grab_focus(window->send_text);
+}
+
+static gboolean remmina_chat_window_scroll_proc(RemminaChatWindow* window)
+{
+ TRACE_CALL(__func__);
+ GtkTextBuffer* buffer;
+ GtkTextIter iter;
+
+ buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(window->history_text));
+ gtk_text_buffer_get_end_iter(buffer, &iter);
+ gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(window->history_text), &iter, 0.0, FALSE, 0.0, 0.0);
+
+ return FALSE;
+}
+
+static void remmina_chat_window_append_text(RemminaChatWindow* window, const gchar* name, const gchar* tagname,
+ const gchar* text)
+{
+ TRACE_CALL(__func__);
+ GtkTextBuffer* buffer;
+ GtkTextIter iter;
+ gchar* ptr;
+
+ buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(window->history_text));
+
+ if (name) {
+ ptr = g_strdup_printf("(%s) ", name);
+ gtk_text_buffer_get_end_iter(buffer, &iter);
+ if (tagname) {
+ gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, ptr, -1, tagname, NULL);
+ }else {
+ gtk_text_buffer_insert(buffer, &iter, ptr, -1);
+ }
+ g_free(ptr);
+ }
+
+ if (text && text[0] != 0) {
+ gtk_text_buffer_get_end_iter(buffer, &iter);
+ if (text[strlen(text) - 1] == '\n') {
+ gtk_text_buffer_insert(buffer, &iter, text, -1);
+ }else {
+ ptr = g_strdup_printf("%s\n", text);
+ gtk_text_buffer_insert(buffer, &iter, ptr, -1);
+ g_free(ptr);
+ }
+ }
+
+ /* Use g_idle_add to make the scroll happen after the text has been actually updated */
+ g_idle_add((GSourceFunc)remmina_chat_window_scroll_proc, window);
+}
+
+static void remmina_chat_window_send(GtkWidget* widget, RemminaChatWindow* window)
+{
+ TRACE_CALL(__func__);
+ GtkTextBuffer* buffer;
+ GtkTextIter start, end;
+ gchar* text;
+
+ buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(window->send_text));
+ gtk_text_buffer_get_bounds(buffer, &start, &end);
+ text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
+
+ if (!text || text[0] == '\0')
+ return;
+
+ g_signal_emit_by_name(G_OBJECT(window), "send", text);
+
+ remmina_chat_window_append_text(window, g_get_user_name(), "sender-foreground", text);
+
+ g_free(text);
+
+ remmina_chat_window_clear_send_text(widget, window);
+}
+
+static gboolean remmina_chat_window_send_text_on_key(GtkWidget* widget, GdkEventKey* event, RemminaChatWindow* window)
+{
+ TRACE_CALL(__func__);
+ if (event->keyval == GDK_KEY_Return) {
+ remmina_chat_window_send(widget, window);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+GtkWidget*
+remmina_chat_window_new(GtkWindow* parent, const gchar* chat_with)
+{
+ TRACE_CALL(__func__);
+ RemminaChatWindow* window;
+ gchar buf[100];
+ GtkWidget* grid;
+ GtkWidget* scrolledwindow;
+ GtkWidget* widget;
+ GtkWidget* image;
+ GtkTextBuffer* buffer;
+
+ window = REMMINA_CHAT_WINDOW(g_object_new(REMMINA_TYPE_CHAT_WINDOW, NULL));
+
+ if (parent) {
+ gtk_window_set_transient_for(GTK_WINDOW(window), parent);
+ }
+
+ /* Title */
+ g_snprintf(buf, sizeof(buf), _("Chat with %s"), chat_with);
+ gtk_window_set_title(GTK_WINDOW(window), buf);
+ gtk_window_set_default_size(GTK_WINDOW(window), 450, 300);
+
+ /* Main container */
+ grid = gtk_grid_new();
+ gtk_widget_show(grid);
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 4);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 4);
+ gtk_container_set_border_width(GTK_CONTAINER(grid), 8);
+ gtk_container_add(GTK_CONTAINER(window), grid);
+
+ /* Chat history */
+ scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_show(scrolledwindow);
+ gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scrolledwindow), 100);
+ gtk_widget_set_hexpand(GTK_WIDGET(scrolledwindow), TRUE);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
+ gtk_grid_attach(GTK_GRID(grid), scrolledwindow, 0, 0, 3, 1);
+
+ widget = gtk_text_view_new();
+ gtk_widget_show(widget);
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget), GTK_WRAP_WORD_CHAR);
+ gtk_text_view_set_editable(GTK_TEXT_VIEW(widget), FALSE);
+ gtk_container_add(GTK_CONTAINER(scrolledwindow), widget);
+
+ buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
+ gtk_text_buffer_create_tag(buffer, "sender-foreground", "foreground", "blue", NULL);
+ gtk_text_buffer_create_tag(buffer, "receiver-foreground", "foreground", "red", NULL);
+
+ window->history_text = widget;
+
+ /* Chat message to be sent */
+ scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_show(scrolledwindow);
+ gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scrolledwindow), 100);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
+ gtk_widget_set_hexpand(GTK_WIDGET(scrolledwindow), TRUE);
+ gtk_grid_attach(GTK_GRID(grid), scrolledwindow, 0, 1, 3, 1);
+
+ widget = gtk_text_view_new();
+ gtk_widget_show(widget);
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget), GTK_WRAP_WORD_CHAR);
+ gtk_container_add(GTK_CONTAINER(scrolledwindow), widget);
+ g_signal_connect(G_OBJECT(widget), "key-press-event", G_CALLBACK(remmina_chat_window_send_text_on_key), window);
+
+ window->send_text = widget;
+
+ /* Send button */
+ image = gtk_image_new_from_icon_name("org.remmina.Remmina-document-send-symbolic", GTK_ICON_SIZE_BUTTON);
+ gtk_widget_show(image);
+
+ widget = gtk_button_new_with_mnemonic(_("_Send"));
+ gtk_widget_show(widget);
+ gtk_button_set_image(GTK_BUTTON(widget), image);
+ gtk_grid_attach(GTK_GRID(grid), widget, 2, 2, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(remmina_chat_window_send), window);
+
+ /* Clear button */
+ image = gtk_image_new_from_icon_name("edit-clear", GTK_ICON_SIZE_BUTTON);
+ gtk_widget_show(image);
+
+ widget = gtk_button_new_with_mnemonic(_("_Clear"));
+ gtk_widget_show(widget);
+ gtk_button_set_image(GTK_BUTTON(widget), image);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 2, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(remmina_chat_window_clear_send_text), window);
+
+ gtk_widget_grab_focus(window->send_text);
+
+ return GTK_WIDGET(window);
+}
+
+void remmina_chat_window_receive(RemminaChatWindow* window, const gchar* name, const gchar* text)
+{
+ TRACE_CALL(__func__);
+ remmina_chat_window_append_text(window, name, "receiver-foreground", text);
+}
+
diff --git a/src/remmina_chat_window.h b/src/remmina_chat_window.h
new file mode 100644
index 0000000..68992d1
--- /dev/null
+++ b/src/remmina_chat_window.h
@@ -0,0 +1,68 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define REMMINA_TYPE_CHAT_WINDOW (remmina_chat_window_get_type())
+#define REMMINA_CHAT_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_CHAT_WINDOW, RemminaChatWindow))
+#define REMMINA_CHAT_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_CHAT_WINDOW, RemminaChatWindowClass))
+#define REMMINA_IS_CHAT_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_CHAT_WINDOW))
+#define REMMINA_IS_CHAT_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_CHAT_WINDOW))
+#define REMMINA_CHAT_WINDOW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_CHAT_WINDOW, RemminaChatWindowClass))
+
+typedef struct _RemminaChatWindow {
+ GtkWindow window;
+
+ GtkWidget * history_text;
+ GtkWidget * send_text;
+} RemminaChatWindow;
+
+typedef struct _RemminaChatWindowClass {
+ GtkWindowClass parent_class;
+
+ void (*send)(RemminaChatWindow *window);
+} RemminaChatWindowClass;
+
+GType remmina_chat_window_get_type(void)
+G_GNUC_CONST;
+
+GtkWidget *remmina_chat_window_new(GtkWindow *parent, const gchar *chat_with);
+void remmina_chat_window_receive(RemminaChatWindow *window, const gchar *name, const gchar *text);
+
+G_END_DECLS
diff --git a/src/remmina_crypt.c b/src/remmina_crypt.c
new file mode 100644
index 0000000..0ed6c5b
--- /dev/null
+++ b/src/remmina_crypt.c
@@ -0,0 +1,198 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 "config.h"
+#ifdef HAVE_LIBGCRYPT
+#include <gcrypt.h>
+#endif
+#include "remmina_pref.h"
+#include "remmina_crypt.h"
+#include "remmina/remmina_trace_calls.h"
+
+#ifdef HAVE_LIBGCRYPT
+
+static gboolean remmina_crypt_init(gcry_cipher_hd_t *phd)
+{
+ TRACE_CALL(__func__);
+ guchar* secret;
+ gcry_error_t err;
+ gsize secret_len;
+
+ secret = g_base64_decode(remmina_pref.secret, &secret_len);
+
+ if (secret_len < 32) {
+ g_debug("secret corrupted\n");
+ g_free(secret);
+ return FALSE;
+ }
+
+ err = gcry_cipher_open(phd, GCRY_CIPHER_3DES, GCRY_CIPHER_MODE_CBC, 0);
+
+ if (err) {
+ g_debug("gcry_cipher_open failure: %s\n", gcry_strerror(err));
+ g_free(secret);
+ return FALSE;
+ }
+
+ err = gcry_cipher_setkey((*phd), secret, 24);
+
+ if (err) {
+ g_debug("gcry_cipher_setkey failure: %s\n", gcry_strerror(err));
+ g_free(secret);
+ gcry_cipher_close((*phd));
+ return FALSE;
+ }
+
+ err = gcry_cipher_setiv((*phd), secret + 24, 8);
+
+ if (err) {
+ g_debug("gcry_cipher_setiv failure: %s\n", gcry_strerror(err));
+ g_free(secret);
+ gcry_cipher_close((*phd));
+ return FALSE;
+ }
+
+ g_free(secret);
+
+ return TRUE;
+}
+
+gchar* remmina_crypt_encrypt(const gchar *str)
+{
+ TRACE_CALL(__func__);
+ guchar* buf;
+ gint buf_len;
+ gchar* result;
+ gcry_error_t err;
+ gcry_cipher_hd_t hd;
+
+ if (!str || str[0] == '\0')
+ return NULL;
+
+ if (!remmina_crypt_init(&hd))
+ return NULL;
+
+ buf_len = strlen(str);
+ /* Pack to 64bit block size, and make sure it’s always 0-terminated */
+ buf_len += 8 - buf_len % 8;
+ buf = (guchar*)g_malloc(buf_len);
+ memset(buf, 0, buf_len);
+ memcpy(buf, str, strlen(str));
+
+ err = gcry_cipher_encrypt(hd, buf, buf_len, NULL, 0);
+
+ if (err) {
+ g_debug("gcry_cipher_encrypt failure: %s/%s\n",
+ gcry_strsource(err),
+ gcry_strerror(err));
+ g_free(buf);
+ gcry_cipher_close(hd);
+ return NULL;
+ }
+
+ result = g_base64_encode(buf, buf_len);
+
+ g_free(buf);
+ gcry_cipher_close(hd);
+
+ return result;
+}
+
+gchar* remmina_crypt_decrypt(const gchar *str)
+{
+ TRACE_CALL(__func__);
+ guchar* buf;
+ gsize buf_len;
+ gcry_error_t err;
+ gcry_cipher_hd_t hd;
+
+ if (!str || str[0] == '\0')
+ return NULL;
+
+ if (!remmina_crypt_init(&hd))
+ return NULL;
+
+ buf = g_base64_decode(str, &buf_len);
+
+#if 0
+ g_debug ("%s base64 encoded as %p with length %lu",
+ str,
+ &buf,
+ buf_len);
+#endif
+
+ err = gcry_cipher_decrypt(
+ hd, // gcry_cipher_hd_t
+ buf, // guchar
+ buf_len, // gsize
+ NULL, // NULL and 0 -> in place decrypt
+ 0);
+
+ if (err) {
+ g_debug("gcry_cipher_decrypt failure: %s/%s\n",
+ gcry_strsource(err),
+ gcry_strerror(err));
+ g_free(buf);
+ gcry_cipher_close(hd);
+ //return NULL;
+ return g_strdup(str);
+ }
+
+ gcry_cipher_close(hd);
+
+ /* Just in case */
+ if (buf_len > 0)
+ buf[buf_len - 1] = '\0';
+
+ return (gchar*)buf;
+}
+
+#else
+
+gchar* remmina_crypt_encrypt(const gchar *str)
+{
+ TRACE_CALL(__func__);
+ return NULL;
+}
+
+gchar* remmina_crypt_decrypt(const gchar *str)
+{
+ TRACE_CALL(__func__);
+ return NULL;
+}
+
+#endif
+
diff --git a/src/remmina_crypt.h b/src/remmina_crypt.h
new file mode 100644
index 0000000..10ef0e8
--- /dev/null
+++ b/src/remmina_crypt.h
@@ -0,0 +1,45 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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>
+
+G_BEGIN_DECLS
+
+gchar *remmina_crypt_encrypt(const gchar *str);
+gchar *remmina_crypt_decrypt(const gchar *str);
+
+G_END_DECLS
diff --git a/src/remmina_curl_connector.c b/src/remmina_curl_connector.c
new file mode 100644
index 0000000..588d38a
--- /dev/null
+++ b/src/remmina_curl_connector.c
@@ -0,0 +1,275 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-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.
+ *
+ */
+
+#include <curl/curl.h>
+#include <gtk/gtk.h>
+#include <json-glib/json-glib.h>
+
+#include "remmina.h"
+#include "remmina_info.h"
+#include "remmina_log.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_pref.h"
+#include "remmina_curl_connector.h"
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_utils.h"
+
+
+#ifdef GDK_WINDOWING_WAYLAND
+ #include <gdk/gdkwayland.h>
+#endif
+#ifdef GDK_WINDOWING_X11
+ #include <gdk/gdkx.h>
+#endif
+
+extern gboolean info_disable_stats;
+extern gboolean info_disable_news;
+extern gboolean info_disable_tip;
+
+struct curl_msg {
+ gchar *body;
+ gchar *url;
+ gpointer *user_data;
+ char* response;
+ size_t size;
+};
+
+enum ShowValue {ShowNews, ShowTip, ShowNone};
+
+
+// If we receive a response from the server parse out the message
+// and call the appropiate functions to handle the message.
+void handle_resp(struct curl_msg * message){
+ gchar *news_checksum = NULL;
+ JsonParser *parser = NULL;
+ enum ShowValue to_show = ShowNone;
+
+ if (!imode) {
+ parser = json_parser_new();
+ if (!parser) {
+ return;
+ }
+ if (!json_parser_load_from_data(parser, message->response , message->size, NULL)){
+ g_object_unref(parser);
+ return;
+ }
+ JsonReader *reader = json_reader_new(json_parser_get_root(parser));
+ if (!reader) {
+ g_object_unref(parser);
+ return;
+ }
+ const gchar *json_tip_string;
+ const gchar *json_news_string;
+ gint64 json_stats_int = 0;
+
+ if (!json_reader_read_element(reader, 0)) {
+ g_object_unref(parser);
+ g_object_unref(reader);
+ return;
+ }
+
+ if (!json_reader_read_member(reader, "TIP")) {
+ json_tip_string = NULL;
+ }
+ else {
+ json_tip_string = json_reader_get_string_value(reader);
+ }
+ json_reader_end_member(reader);
+
+ if (!json_reader_read_member(reader, "NEWS")) {
+ json_news_string = NULL;
+ }
+ else {
+ json_news_string = json_reader_get_string_value(reader);
+
+ }
+ json_reader_end_member(reader);
+
+ if (!json_reader_read_member(reader, "STATS")) {
+ json_stats_int = 0;
+ }
+ else {
+ json_stats_int = json_reader_get_int_value(reader);
+ }
+ json_reader_end_member(reader);
+
+ if (json_reader_read_member(reader, "LIST")){
+ g_idle_add(remmina_plugin_manager_parse_plugin_list, json_reader_new(json_parser_get_root(parser)));
+ }
+ json_reader_end_member(reader);
+
+ if (json_reader_read_member(reader, "PLUGINS")){
+ g_idle_add(remmina_plugin_manager_download_plugins, json_reader_new(json_parser_get_root(parser)));
+ }
+
+ json_reader_end_member(reader);
+ json_reader_end_element(reader);
+ g_object_unref(reader);
+
+ if (json_news_string == NULL) {
+ if (json_tip_string == NULL) {
+ to_show = ShowNone;
+ }
+ else if (info_disable_tip == 0) {
+ to_show = ShowTip;
+ }
+ }
+ else {
+ news_checksum = remmina_sha256_buffer((const guchar *)json_news_string, strlen(json_news_string));
+ if (news_checksum == NULL) {
+ REMMINA_DEBUG("News checksum is NULL");
+ }
+ else if ((remmina_pref.periodic_news_last_checksum == NULL) || strcmp(news_checksum, remmina_pref.periodic_news_last_checksum) != 0) {
+ REMMINA_DEBUG("Latest news checksum: %s", news_checksum);
+ remmina_pref.periodic_news_last_checksum = g_strdup(news_checksum);
+ remmina_pref_save();
+ if (info_disable_news == 0) {
+ to_show = ShowNews;
+ }
+ }
+
+ if (to_show == ShowNone && json_tip_string != NULL && info_disable_tip == 0) {
+ to_show = ShowTip;
+ }
+ g_free(news_checksum);
+ }
+
+ if (to_show == ShowTip) {
+ RemminaInfoMessage *message = malloc(sizeof(RemminaInfoMessage));
+ if (message == NULL) {
+ return;
+ }
+ message->info_string= g_strdup(json_tip_string);
+ message->title_string= g_strdup("Tip of the Day");
+ g_idle_add(remmina_info_show_response, message);
+ }
+ else if (to_show == ShowNews) {
+ RemminaInfoMessage *message = malloc(sizeof(RemminaInfoMessage));
+ if (message == NULL) {
+ return;
+ }
+ message->info_string= g_strdup(json_news_string);
+ message->title_string= g_strdup("NEWS");
+ g_idle_add(remmina_info_show_response, message);
+ }
+
+ if (json_stats_int){
+ remmina_info_stats_collector();
+ }
+
+ g_object_unref(parser);
+ }
+
+}
+
+
+// Allocate memory to hold response from the server in msg->response
+size_t remmina_curl_process_response(void *buffer, size_t size, size_t nmemb, void *userp){
+ size_t realsize = size * nmemb;
+ struct curl_msg *msg = (struct curl_msg *)userp;
+
+ char *ptr = realloc(msg->response, msg->size + realsize + 1);
+ if (!ptr) {
+ return 0; /* out of memory! */
+ }
+
+ msg->response = ptr;
+ memcpy(&(msg->response[msg->size]), buffer, realsize);
+ msg->size += realsize;
+ msg->response[msg->size] = 0;
+
+ return realsize;
+}
+
+
+void remmina_curl_send_message(gpointer data)
+{
+ gchar *result_message = NULL;
+ gchar *marked_up_message = NULL;
+ struct curl_msg* message = (struct curl_msg*)data;
+ message->size = 0;
+ message->response = NULL;
+ CURL* curl = curl_easy_init();
+ if (curl){
+ CURLcode res;
+ curl_easy_setopt(curl, CURLOPT_URL, message->url);
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, message->body);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, remmina_curl_process_response);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, message);
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60);
+ res = curl_easy_perform(curl);
+ if(res != CURLE_OK){
+ curl_easy_strerror(res);
+ result_message = "Failure: Transport error occurred - see debug or logs for more information";
+ marked_up_message = RED_TEXT(result_message);
+ }
+ else{
+ handle_resp(message);
+ result_message = "Success: Message sent successfully, please wait for report to automatically open a new issue. This may take a little while.";
+ marked_up_message = GREEN_TEXT(result_message);
+
+ }
+ if (message->user_data != NULL){
+ gtk_label_set_markup(GTK_LABEL(message->user_data), marked_up_message);
+
+ }
+ if (message->body){
+ g_free(message->body);
+ }
+ if (message->response){
+ g_free(message->response);
+ }
+ g_free(message);
+ g_free(marked_up_message);
+ curl_easy_cleanup(curl);
+ }
+}
+
+
+
+void remmina_curl_compose_message(gchar* body, char* type, char* url, gpointer data)
+{
+ if (!imode){
+ struct curl_msg* message = (struct curl_msg*)malloc(sizeof(struct curl_msg));
+ if (message == NULL) {
+ return;
+ }
+ message->body = body;
+ message->url = url;
+ message->user_data = data;
+ g_thread_new("send_curl_message", (GThreadFunc)remmina_curl_send_message, message);
+ }
+
+}
diff --git a/src/remmina_curl_connector.h b/src/remmina_curl_connector.h
new file mode 100644
index 0000000..5ce33a8
--- /dev/null
+++ b/src/remmina_curl_connector.h
@@ -0,0 +1,54 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-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.
+ *
+ */
+#pragma once
+
+#include <glib.h>
+#include "json-glib/json-glib.h"
+
+G_BEGIN_DECLS
+
+#define DOWNLOAD_URL "https://plugins.remmina.org/plugins/plugin_download"
+#define LIST_URL "https://plugins.remmina.org/plugins/get_list"
+#define PERIODIC_UPLOAD_URL "https://info.remmina.org/info/upload_stats"
+#define INFO_REQUEST_URL "https://info.remmina.org/info/handshake"
+
+#define RED_TEXT(str) g_markup_printf_escaped("<span color=\"red\">%s</span>", str)
+#define GREEN_TEXT(str) g_markup_printf_escaped("<span color=\"green\">%s</span>", str)
+#define GRAY_TEXT(str) g_markup_printf_escaped("<span color=\"gray\">%s</span>", str)
+
+void remmina_curl_handshake(gpointer data);
+void remmina_curl_compose_message(gchar* body, char* type, char* url, gpointer data);
+
+G_END_DECLS
diff --git a/src/remmina_exec.c b/src/remmina_exec.c
new file mode 100644
index 0000000..cf76b58
--- /dev/null
+++ b/src/remmina_exec.c
@@ -0,0 +1,544 @@
+/*
+ * 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 "config.h"
+#include "buildflags.h"
+#include <glib/gi18n.h>
+#include <stdlib.h>
+#include "remmina.h"
+#include "remmina_main.h"
+#include "remmina_log.h"
+#include "remmina_pref.h"
+#include "remmina_widget_pool.h"
+#include "remmina_unlock.h"
+#include "remmina_pref_dialog.h"
+#include "remmina_file.h"
+#include "remmina_file_manager.h"
+#include "remmina_file_editor.h"
+#include "rcw.h"
+#include "remmina_about.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_exec.h"
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_file_manager.h"
+#include "remmina_crypt.h"
+
+#include "remmina_icon.h"
+
+#ifdef SNAP_BUILD
+# define ISSNAP "- SNAP Build -"
+#else
+# define ISSNAP "-"
+#endif
+
+static gboolean cb_closewidget(GtkWidget *widget, gpointer data)
+{
+ TRACE_CALL(__func__);
+ /* The correct way to close a rcw is to send
+ * it a "delete-event" signal. Simply destroying it will not close
+ * all network connections */
+ if (REMMINA_IS_CONNECTION_WINDOW(widget))
+ return rcw_delete(RCW(widget));
+ return TRUE;
+}
+
+const gchar* remmina_exec_get_build_config(void)
+{
+ static const gchar build_config[] =
+ "Build configuration: " BUILD_CONFIG "\n"
+ "Build type: " BUILD_TYPE "\n"
+ "CFLAGS: " CFLAGS "\n"
+ "Compiler: " COMPILER_ID ", " COMPILER_VERSION "\n"
+ "Target architecture: " TARGET_ARCH "\n";
+ return build_config;
+}
+
+void remmina_exec_exitremmina()
+{
+ TRACE_CALL(__func__);
+
+ /* Save main window state/position */
+ remmina_main_save_before_destroy();
+
+ /* Delete all widgets, main window not included */
+ remmina_widget_pool_foreach(cb_closewidget, NULL);
+
+#ifdef HAVE_LIBAPPINDICATOR
+ /* Remove systray menu */
+ remmina_icon_destroy();
+#endif
+
+ /* close/destroy main window struct and window */
+ remmina_main_destroy();
+
+ /* Exit from Remmina */
+ g_application_quit(g_application_get_default());
+}
+
+static gboolean disable_rcw_delete_confirm_cb(GtkWidget *widget, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaConnectionWindow *rcw;
+
+ if (REMMINA_IS_CONNECTION_WINDOW(widget)) {
+ rcw = (RemminaConnectionWindow*)widget;
+ rcw_set_delete_confirm_mode(rcw, RCW_ONDELETE_NOCONFIRM);
+ }
+ return TRUE;
+}
+
+void remmina_exec_exitremmina_one_confirm()
+{
+ TRACE_CALL(__func__);
+ GtkWidget* dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_YES_NO,
+ _("Are you sure you want to fully quit Remmina?\n This will close any active connections."));
+ int response = gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ if (response != GTK_RESPONSE_YES)
+ return;
+ remmina_widget_pool_foreach(disable_rcw_delete_confirm_cb, NULL);
+ remmina_exec_exitremmina();
+}
+
+void remmina_application_condexit(RemminaCondExitType why)
+{
+ TRACE_CALL(__func__);
+
+ /* Exit remmina only if there are no interesting windows left:
+ * no main window, no systray menu, no connection window.
+ * This function is usually called after a disconnection */
+
+ switch (why) {
+ case REMMINA_CONDEXIT_ONDISCONNECT:
+ // A connection has disconnected, should we exit remmina ?
+ if (remmina_widget_pool_count() < 1 && !remmina_main_get_window() && !remmina_icon_is_available())
+ remmina_exec_exitremmina();
+ break;
+ case REMMINA_CONDEXIT_ONMAINWINDELETE:
+ /* If we are in Kiosk mode, we just exit */
+ if (kioskmode && kioskmode == TRUE)
+ remmina_exec_exitremmina();
+ // Main window has been deleted
+ if (remmina_widget_pool_count() < 1 && !remmina_icon_is_available())
+ remmina_exec_exitremmina();
+ break;
+ case REMMINA_CONDEXIT_ONQUIT:
+ // Quit command has been sent from main window or appindicator/systray menu
+ // quit means QUIT.
+ remmina_widget_pool_foreach(disable_rcw_delete_confirm_cb, NULL);
+ remmina_exec_exitremmina();
+ break;
+ }
+}
+
+
+static void newline_remove(char *s)
+{
+ char c;
+ while((c = *s) != 0 && c != '\r' && c != '\n')
+ s++;
+ *s = 0;
+}
+
+/* used for commandline parameter --update-profile X --set-option Y --set-option Z
+ * return a status code for exit()
+ */
+int remmina_exec_set_setting(gchar *profilefilename, gchar **settings)
+{
+ RemminaFile *remminafile;
+ int i;
+ gchar **tk, *value = NULL;
+ char *line = NULL;
+ size_t len = 0;
+ ssize_t read;
+ gboolean abort = FALSE;
+
+ remminafile = remmina_file_manager_load_file(profilefilename);
+
+ if (!remminafile) {
+ g_print("Unable to open profile file %s\n", profilefilename);
+ return 2;
+ }
+
+ for(i = 0; settings[i] != NULL && !abort; i++) {
+ if (strlen(settings[i]) > 0) {
+ tk = g_strsplit(settings[i], "=", 2);
+ if (tk[1] == NULL) {
+ read = getline(&line, &len, stdin);
+ if (read > 0) {
+ newline_remove(line);
+ value = line;
+ } else {
+ g_print("Error: an extra line of standard input is needed\n");
+ abort = TRUE;
+ }
+ } else
+ value = tk[1];
+ remmina_file_set_string(remminafile, tk[0], value);
+ g_strfreev(tk);
+ }
+ }
+
+ if (line) free(line);
+
+ if (!abort) remmina_file_save(remminafile);
+
+ return 0;
+
+}
+
+static void remmina_exec_autostart_cb(RemminaFile *remminafile, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ if (remmina_file_get_int(remminafile, "enable-autostart", FALSE)) {
+ REMMINA_DEBUG ("Profile %s is set to autostart", remminafile->filename);
+ rcw_open_from_filename(remminafile->filename);
+ }
+
+}
+
+static void remmina_exec_connect(const gchar *data)
+{
+ TRACE_CALL(__func__);
+
+ gchar *protocol;
+ gchar **protocolserver;
+ gchar *server;
+ RemminaFile *remminafile;
+ gchar **userat;
+ gchar **userpass;
+ gchar *user;
+ gchar *password;
+ gchar **domainuser;
+ gchar **serverquery;
+ gchar **querystring;
+ gchar **querystringpart;
+ gchar **querystringpartkv;
+ gchar *value;
+ gchar *temp;
+ GError *error = NULL;
+
+ protocol = NULL;
+ if (strncmp("rdp://", data, 6) == 0 || strncmp("RDP://", data, 6) == 0)
+ protocol = "RDP";
+ else if (strncmp("vnc://", data, 6) == 0 || strncmp("VNC://", data, 6) == 0)
+ protocol = "VNC";
+ else if (strncmp("ssh://", data, 6) == 0 || strncmp("SSH://", data, 6) == 0)
+ protocol = "SSH";
+ else if (strncmp("spice://", data, 8) == 0 || strncmp("SPICE://", data, 8) == 0)
+ protocol = "SPICE";
+
+ if (strncmp("file://", data, 6) == 0) {
+ gchar *filename = g_filename_from_uri (data, NULL, &error);
+ if (filename != NULL) {
+ rcw_open_from_filename(filename);
+ } else
+ REMMINA_DEBUG ("Opening URI %s failed with error %s", data, error->message);
+ g_error_free(error);
+ return;
+ }
+
+ if (protocol == NULL) {
+ rcw_open_from_filename(data);
+ return;
+ }
+
+ protocolserver = g_strsplit(data, "://", 2);
+ server = g_strdup(protocolserver[1]);
+
+ // Support loading .remmina files using handler
+ if ((temp = strrchr(server, '.')) != NULL && g_strcmp0(temp + 1, "remmina") == 0) {
+ g_strfreev(protocolserver);
+ temp = g_uri_unescape_string(server, NULL);
+ g_free(server);
+ server = temp;
+ rcw_open_from_filename(server);
+ return;
+ }
+
+ remminafile = remmina_file_new();
+
+ // Check for username@server
+ if ((strcmp(protocol, "RDP") == 0 || strcmp(protocol, "VNC") == 0 || strcmp(protocol, "SSH") == 0) && strstr(server, "@") != NULL) {
+ userat = g_strsplit(server, "@", 2);
+
+ // Check for username:password
+ if (strstr(userat[0], ":") != NULL) {
+ userpass = g_strsplit(userat[0], ":", 2);
+ user = g_uri_unescape_string(userpass[0], NULL);
+ password = g_uri_unescape_string(userpass[1], NULL);
+
+ // Try to decrypt the password field if it contains =
+ temp = password != NULL && strrchr(password, '=') != NULL ? remmina_crypt_decrypt(password) : NULL;
+ if (temp != NULL) {
+ g_free(password);
+ password = temp;
+ }
+ remmina_file_set_string(remminafile, "password", password);
+ g_free(password);
+ g_strfreev(userpass);
+ } else {
+ user = g_uri_unescape_string(userat[0], NULL);
+ }
+
+ // Check for domain\user for RDP connections
+ if (strcmp(protocol, "RDP") == 0 && strstr(user, "\\") != NULL) {
+ domainuser = g_strsplit(user, "\\", 2);
+ remmina_file_set_string(remminafile, "domain", domainuser[0]);
+ g_free(user);
+ user = g_strdup(domainuser[1]);
+ }
+
+ remmina_file_set_string(remminafile, "username", user);
+ g_free(user);
+ g_free(server);
+ server = g_strdup(userat[1]);
+ g_strfreev(userat);
+ }
+
+ if (strcmp(protocol, "VNC") == 0 && strstr(server, "?") != NULL) {
+ // https://tools.ietf.org/html/rfc7869
+ // VncUsername, VncPassword and ColorLevel supported for vnc-params
+
+ // Check for query string parameters
+ serverquery = g_strsplit(server, "?", 2);
+ querystring = g_strsplit(serverquery[1], "&", -1);
+ for (querystringpart = querystring; *querystringpart; querystringpart++) {
+ if (strstr(*querystringpart, "=") == NULL)
+ continue;
+ querystringpartkv = g_strsplit(*querystringpart, "=", 2);
+ value = g_uri_unescape_string(querystringpartkv[1], NULL);
+ if (strcmp(querystringpartkv[0], "VncPassword") == 0) {
+ // Try to decrypt password field if it contains =
+ temp = value != NULL && strrchr(value, '=') != NULL ? remmina_crypt_decrypt(value) : NULL;
+ if (temp != NULL) {
+ g_free(value);
+ value = temp;
+ }
+ remmina_file_set_string(remminafile, "password", value);
+ } else if (strcmp(querystringpartkv[0], "VncUsername") == 0) {
+ remmina_file_set_string(remminafile, "username", value);
+ } else if (strcmp(querystringpartkv[0], "ColorLevel") == 0) {
+ remmina_file_set_string(remminafile, "colordepth", value);
+ }
+ g_free(value);
+ g_strfreev(querystringpartkv);
+ }
+ g_strfreev(querystring);
+ g_free(server);
+ server = g_strdup(serverquery[0]);
+ g_strfreev(serverquery);
+ }
+
+ // Unescape server
+ temp = g_uri_unescape_string(server, NULL);
+ g_free(server);
+ server = temp;
+
+ remmina_file_set_string(remminafile, "server", server);
+ remmina_file_set_string(remminafile, "name", server);
+ remmina_file_set_string(remminafile, "sound", "off");
+ remmina_file_set_string(remminafile, "protocol", protocol);
+ g_free(server);
+ g_strfreev(protocolserver);
+ rcw_open_from_file(remminafile);
+}
+
+void remmina_exec_command(RemminaCommandType command, const gchar* data)
+{
+ TRACE_CALL(__func__);
+ gchar *s1;
+ gchar *s2;
+ gchar *temp;
+ GtkWidget *widget;
+ GtkWindow *mainwindow;
+ GtkWidget *prefdialog;
+ RemminaEntryPlugin *plugin;
+ int i;
+ int ch;
+ mainwindow = remmina_main_get_window();
+
+ switch (command) {
+ case REMMINA_COMMAND_AUTOSTART:
+ remmina_file_manager_iterate((GFunc)remmina_exec_autostart_cb, NULL);
+ break;
+
+ case REMMINA_COMMAND_MAIN:
+ if (mainwindow) {
+ gtk_window_present(mainwindow);
+ gtk_window_deiconify(GTK_WINDOW(mainwindow));
+ }else {
+ widget = remmina_main_new();
+ gtk_widget_show(widget);
+ }
+ break;
+
+ case REMMINA_COMMAND_PREF:
+ if (remmina_pref_get_boolean("use_primary_password")
+ && remmina_unlock_new(mainwindow) == 0)
+ break;
+ prefdialog = remmina_pref_dialog_get_dialog();
+ if (prefdialog) {
+ gtk_window_present(GTK_WINDOW(prefdialog));
+ gtk_window_deiconify(GTK_WINDOW(prefdialog));
+ }else {
+ /* Create a new preference dialog */
+ widget = remmina_pref_dialog_new(atoi(data), NULL);
+ gtk_widget_show(widget);
+ }
+ break;
+
+ case REMMINA_COMMAND_NEW:
+ if (remmina_pref_get_boolean("lock_edit")
+ && remmina_pref_get_boolean("use_primary_password"))
+ if (remmina_unlock_new(mainwindow) == 0)
+ break;
+ s1 = (data ? strchr(data, ',') : NULL);
+ if (s1) {
+ s1 = g_strdup(data);
+ s2 = strchr(s1, ',');
+ *s2++ = '\0';
+ widget = remmina_file_editor_new_full(s2, s1);
+ g_free(s1);
+ }else {
+ widget = remmina_file_editor_new_full(NULL, data);
+ }
+ gtk_widget_show(widget);
+ break;
+
+ case REMMINA_COMMAND_CONNECT:
+ REMMINA_DEBUG ("Initiating connection");
+ /** @todo This should be a G_OPTION_ARG_FILENAME_ARRAY (^aay) so that
+ * we can implement multi profile connection:
+ * https://gitlab.com/Remmina/Remmina/issues/915
+ */
+ if (remmina_pref_get_boolean("lock_connect")
+ && remmina_pref_get_boolean("use_primary_password"))
+ if (remmina_unlock_new(mainwindow) == 0)
+ break;
+ remmina_exec_connect(data);
+ break;
+
+ case REMMINA_COMMAND_EDIT:
+ if (remmina_pref_get_boolean("lock_edit")
+ && remmina_pref_get_boolean("use_primary_password"))
+ if (remmina_unlock_new(mainwindow) == 0)
+ break;
+ widget = remmina_file_editor_new_from_filename(data);
+ if (widget)
+ gtk_widget_show(widget);
+ break;
+
+ case REMMINA_COMMAND_ABOUT:
+ remmina_about_open(NULL);
+ break;
+
+ case REMMINA_COMMAND_VERSION:
+ mainwindow = remmina_main_get_window();
+ if (mainwindow) {
+ remmina_about_open(NULL);
+ }else {
+ g_print("%s %s %s (git %s)\n", g_get_application_name(), ISSNAP, VERSION, REMMINA_GIT_REVISION);
+ /* As we do not use the "handle-local-options" signal, we have to exit Remmina */
+ remmina_exec_command(REMMINA_COMMAND_EXIT, NULL);
+ }
+
+ break;
+
+ case REMMINA_COMMAND_FULL_VERSION:
+ mainwindow = remmina_main_get_window();
+ if (mainwindow) {
+ /* Show th widget with the list of plugins and versions */
+ remmina_plugin_manager_show(mainwindow);
+ }else {
+ g_print("\n%s %s %s (git %s)\n\n", g_get_application_name(), ISSNAP, VERSION, REMMINA_GIT_REVISION);
+
+ remmina_plugin_manager_show_stdout();
+ g_print("\n%s\n", remmina_exec_get_build_config());
+ remmina_exec_command(REMMINA_COMMAND_EXIT, NULL);
+ }
+
+ break;
+
+
+ case REMMINA_COMMAND_PLUGIN:
+ plugin = (RemminaEntryPlugin*)remmina_plugin_manager_get_plugin(REMMINA_PLUGIN_TYPE_ENTRY, data);
+ if (plugin) {
+ plugin->entry_func(plugin);
+ }else {
+ widget = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
+ _("Plugin %s is not registered."), data);
+ g_signal_connect(G_OBJECT(widget), "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_widget_show(widget);
+ remmina_widget_pool_register(widget);
+ }
+ break;
+
+ case REMMINA_COMMAND_ENCRYPT_PASSWORD:
+ i = 0;
+ g_print("Enter the password you want to encrypt: ");
+ temp = (char *)g_malloc(255 * sizeof(char));
+ while ((ch = getchar()) != EOF && ch != '\n') {
+ if (i < 254) {
+ temp[i] = ch;
+ i++;
+ }
+ }
+ temp[i] = '\0';
+ s1 = remmina_crypt_encrypt(temp);
+ s2 = g_uri_escape_string(s1, NULL, TRUE);
+ g_print("\nEncrypted password: %s\n\n", s1);
+ g_print("Usage:\n");
+ g_print("rdp://username:%s@server\n", s1);
+ g_print("vnc://username:%s@server\n", s1);
+ g_print("vnc://server?VncUsername=user\\&VncPassword=%s\n", s2);
+ g_free(s1);
+ g_free(s2);
+ g_free(temp);
+ remmina_exec_exitremmina();
+ break;
+
+ case REMMINA_COMMAND_EXIT:
+ remmina_widget_pool_foreach(disable_rcw_delete_confirm_cb, NULL);
+ remmina_exec_exitremmina();
+ break;
+
+ default:
+ break;
+ }
+}
diff --git a/src/remmina_exec.h b/src/remmina_exec.h
new file mode 100644
index 0000000..3cadec0
--- /dev/null
+++ b/src/remmina_exec.h
@@ -0,0 +1,71 @@
+/*
+ * 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
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+ REMMINA_COMMAND_NONE = 0,
+ REMMINA_COMMAND_MAIN = 1,
+ REMMINA_COMMAND_PREF = 2,
+ REMMINA_COMMAND_NEW = 3,
+ REMMINA_COMMAND_CONNECT = 4,
+ REMMINA_COMMAND_EDIT = 5,
+ REMMINA_COMMAND_ABOUT = 6,
+ REMMINA_COMMAND_VERSION = 7,
+ REMMINA_COMMAND_FULL_VERSION = 8,
+ REMMINA_COMMAND_PLUGIN = 9,
+ REMMINA_COMMAND_EXIT = 10,
+ REMMINA_COMMAND_AUTOSTART = 11,
+ REMMINA_COMMAND_ENCRYPT_PASSWORD = 12
+} RemminaCommandType;
+
+typedef enum {
+ REMMINA_CONDEXIT_ONDISCONNECT = 0,
+ REMMINA_CONDEXIT_ONQUIT = 1,
+ REMMINA_CONDEXIT_ONMAINWINDELETE = 2
+} RemminaCondExitType;
+
+void remmina_exec_command(RemminaCommandType command, const gchar *data);
+void remmina_exec_exitremmina(void);
+void remmina_exec_exitremmina_one_confirm(void);
+void remmina_application_condexit(RemminaCondExitType why);
+
+int remmina_exec_set_setting(gchar *profilefilename, gchar **settings);
+
+G_END_DECLS
diff --git a/src/remmina_ext_exec.c b/src/remmina_ext_exec.c
new file mode 100644
index 0000000..38573f4
--- /dev/null
+++ b/src/remmina_ext_exec.c
@@ -0,0 +1,133 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-2010 Vic Lee
+ * 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.
+ *
+ */
+
+#include <gdk/gdkkeysyms.h>
+#include <glib/gi18n.h>
+#include <glib.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include "remmina_utils.h"
+#include "remmina_ext_exec.h"
+#include "remmina_public.h"
+#include "remmina/remmina_trace_calls.h"
+
+#define SPAWN_TIMEOUT 10
+
+#define GET_OBJECT(object_name) gtk_builder_get_object(builder, object_name)
+
+static void wait_for_child(GPid pid, gint script_retval, gpointer data)
+{
+ PCon_Spinner *pcspinner = (PCon_Spinner*)data;
+
+ gtk_spinner_stop(GTK_SPINNER(pcspinner->spinner));
+ gtk_widget_destroy(GTK_WIDGET(pcspinner->dialog));
+ g_spawn_close_pid(pid);
+ /* TODO At the moment background processes will fail to start before the
+ * remmina connection.
+ * Adding a delay here could be a (not good) solution, or we should
+ * monitor each child opened, but it could be quit tricky and messy */
+}
+
+GtkDialog* remmina_ext_exec_new(RemminaFile* remminafile, const char *remmina_ext_exec_type)
+{
+ TRACE_CALL(__func__);
+ GtkBuilder *builder;
+ PCon_Spinner *pcspinner;
+ GError *error = NULL;
+ char **argv;
+ gchar *cmd = NULL;
+ gchar pre[11];
+ gchar post[12];
+ GPid child_pid;
+
+ strcpy(pre, "precommand");
+ strcpy(post, "postcommand");
+
+ if (remmina_ext_exec_type != NULL && (
+ strcmp(remmina_ext_exec_type, pre) |
+ strcmp(remmina_ext_exec_type, post) )) {
+ cmd = g_strdup(remmina_file_get_string(remminafile, remmina_ext_exec_type));
+ g_debug("[%s] %s", remmina_ext_exec_type, cmd);
+ } else
+ return FALSE;
+
+ cmd = remmina_file_format_properties(remminafile, cmd);
+ g_debug("[%s] updated to: %s", remmina_ext_exec_type, cmd);
+ if (*cmd != 0) {
+
+ pcspinner = g_new(PCon_Spinner, 1);
+ builder = remmina_public_gtk_builder_new_from_resource("/org/remmina/Remmina/src/../data/ui/remmina_spinner.glade");
+ pcspinner->dialog = GTK_DIALOG(gtk_builder_get_object(builder, "DialogSpinner"));
+ pcspinner->label_pleasewait = GTK_LABEL(GET_OBJECT("label_pleasewait"));
+ pcspinner->spinner = GTK_WIDGET(GET_OBJECT("spinner"));
+ pcspinner->button_cancel = GTK_BUTTON(GET_OBJECT("button_cancel"));
+ /* Connect signals */
+ gtk_builder_connect_signals(builder, NULL);
+
+ /* Exec a predefined command */
+ g_shell_parse_argv(cmd, NULL, &argv, &error);
+
+ if (error) {
+ g_warning("%s\n", error->message);
+ g_error_free(error);
+ }
+
+ /* Consider using G_SPAWN_SEARCH_PATH_FROM_ENVP (from glib 2.38)*/
+ g_spawn_async( NULL, // cwd
+ argv, // argv
+ NULL, // envp
+ G_SPAWN_SEARCH_PATH |
+ G_SPAWN_SEARCH_PATH_FROM_ENVP |
+ G_SPAWN_DO_NOT_REAP_CHILD, // flags
+ NULL, // child_setup
+ NULL, // child_setup user data
+ &child_pid, // pid location
+ &error); // error
+ if (!error) {
+ gtk_spinner_start(GTK_SPINNER(pcspinner->spinner));
+ g_child_watch_add(child_pid, wait_for_child, (gpointer)pcspinner);
+ gtk_dialog_run(pcspinner->dialog);
+ }else {
+ g_warning("Command %s exited with error: %s\n", cmd, error->message);
+ g_error_free(error);
+ }
+ g_strfreev(argv);
+ g_free(cmd);
+ return (pcspinner->dialog);
+ }
+ g_free(cmd);
+ return FALSE;
+}
diff --git a/src/remmina_ext_exec.h b/src/remmina_ext_exec.h
new file mode 100644
index 0000000..b2aa461
--- /dev/null
+++ b/src/remmina_ext_exec.h
@@ -0,0 +1,52 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-2010 Vic Lee
+ * 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.
+ *
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include "remmina_file.h"
+
+G_BEGIN_DECLS
+
+typedef struct {
+ GtkDialog * dialog;
+ GtkLabel * label_pleasewait;
+ GtkButton * button_cancel;
+ GtkWidget * spinner;
+} PCon_Spinner;
+
+GtkDialog *remmina_ext_exec_new(RemminaFile *remminafile, const char *remmina_ext_exec_type);
+
+G_END_DECLS
diff --git a/src/remmina_external_tools.c b/src/remmina_external_tools.c
new file mode 100644
index 0000000..97543ca
--- /dev/null
+++ b/src/remmina_external_tools.c
@@ -0,0 +1,157 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2011 Marc-Andre Moreau
+ *
+ * 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 <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <stdlib.h>
+#include "remmina_file.h"
+#include "remmina/types.h"
+#include "remmina_public.h"
+#include "remmina_external_tools.h"
+#include "remmina/remmina_trace_calls.h"
+
+static gboolean remmina_external_tools_launcher(const gchar* filename, const gchar* scriptname, const gchar* shortname);
+
+static void view_popup_menu_onDoSomething(GtkWidget *menuitem, gpointer userdata)
+{
+ TRACE_CALL(__func__);
+ gchar *remminafilename = g_object_get_data(G_OBJECT(menuitem), "remminafilename");
+ gchar *scriptfilename = g_object_get_data(G_OBJECT(menuitem), "scriptfilename");
+ gchar *scriptshortname = g_object_get_data(G_OBJECT(menuitem), "scriptshortname");
+
+ remmina_external_tools_launcher(remminafilename, scriptfilename, scriptshortname);
+}
+
+gboolean remmina_external_tools_from_filename(RemminaMain *remminamain, gchar* remminafilename)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *menu, *menuitem;
+ gchar dirname[MAX_PATH_LEN];
+ gchar filename[MAX_PATH_LEN];
+ GDir* dir;
+ const gchar* name;
+
+ strcpy(dirname, REMMINA_RUNTIME_EXTERNAL_TOOLS_DIR);
+ dir = g_dir_open(dirname, 0, NULL);
+
+ if (dir == NULL)
+ return FALSE;
+
+ menu = gtk_menu_new();
+
+ while ((name = g_dir_read_name(dir)) != NULL) {
+ if (!g_str_has_prefix(name, "remmina_"))
+ continue;
+ g_snprintf(filename, MAX_PATH_LEN, "%s/%s", dirname, name);
+
+ menuitem = gtk_menu_item_new_with_label(name + 8);
+ g_object_set_data_full(G_OBJECT(menuitem), "remminafilename", g_strdup(remminafilename), g_free);
+ g_object_set_data_full(G_OBJECT(menuitem), "scriptfilename", g_strdup(filename), g_free);
+ g_object_set_data_full(G_OBJECT(menuitem), "scriptshortname", g_strdup(name), g_free);
+ g_signal_connect(menuitem, "activate", (GCallback)view_popup_menu_onDoSomething, NULL);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ }
+ g_dir_close(dir);
+
+ gtk_widget_show_all(menu);
+
+ /* Note: event can be NULL here when called from view_onPopupMenu;
+ * gdk_event_get_time() accepts a NULL argument
+ */
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL);
+#else
+ gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, 0, 0);
+#endif
+
+ return TRUE;
+}
+
+static gboolean remmina_external_tools_launcher(const gchar* filename, const gchar* scriptname, const gchar* shortname)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+ const char *env_format = "%s=%s";
+ char *env;
+ size_t envstrlen;
+ gchar launcher[MAX_PATH_LEN];
+
+ g_snprintf(launcher, MAX_PATH_LEN, "%s/launcher.sh", REMMINA_RUNTIME_EXTERNAL_TOOLS_DIR);
+
+ remminafile = remmina_file_load(filename);
+ if (!remminafile)
+ return FALSE;
+ GHashTableIter iter;
+ const gchar *key, *value;
+ g_hash_table_iter_init(&iter, remminafile->settings);
+ while (g_hash_table_iter_next(&iter, (gpointer*)&key, (gpointer*)&value)) {
+ envstrlen = strlen(key) + strlen(value) + strlen(env_format) + 1;
+ env = (char*)malloc(envstrlen);
+ if (env == NULL) {
+ return -1;
+ }
+
+ int retval = snprintf(env, envstrlen, env_format, key, value);
+ if (retval > 0 && (size_t)retval <= envstrlen) {
+ if (putenv(env) != 0) {
+ /* If putenv fails, we must free the unused space */
+ free(env);
+ }
+ }
+ }
+ /* Adds the window title for the terminal window */
+ const char *term_title_key = "remmina_term_title";
+ const char *term_title_val_prefix = "Remmina external tool";
+ envstrlen = strlen(term_title_key) + strlen(term_title_val_prefix) + strlen(shortname) + 7;
+ env = (char*)malloc(envstrlen);
+ if (env != NULL) {
+ if (snprintf(env, envstrlen, "%s=%s: %s", term_title_key, term_title_val_prefix, shortname) ) {
+ if (putenv(env) != 0) {
+ /* If putenv fails, we must free the unused space */
+ free(env);
+ }
+ }
+ }
+
+ const size_t cmdlen = strlen(launcher) + strlen(scriptname) + 2;
+ gchar *cmd = (gchar*)malloc(cmdlen);
+ g_snprintf(cmd, cmdlen, "%s %s", launcher, scriptname);
+ system(cmd);
+ free(cmd);
+
+ remmina_file_free(remminafile);
+
+ return TRUE;
+}
diff --git a/src/remmina_external_tools.h b/src/remmina_external_tools.h
new file mode 100644
index 0000000..ea5abb9
--- /dev/null
+++ b/src/remmina_external_tools.h
@@ -0,0 +1,46 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <gtk/gtk.h>
+#include "remmina_main.h"
+
+G_BEGIN_DECLS
+
+/* Open a new connection window for a .remmina file */
+gboolean remmina_external_tools_from_filename(RemminaMain *remminamain, gchar *remminafilename);
+
+G_END_DECLS
diff --git a/src/remmina_file.c b/src/remmina_file.c
new file mode 100644
index 0000000..2e7bdc3
--- /dev/null
+++ b/src/remmina_file.c
@@ -0,0 +1,1134 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-2011 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-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.
+ *
+ */
+
+#include "config.h"
+
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <locale.h>
+#include <langinfo.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <utime.h>
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_crypt.h"
+#include "remmina_file_manager.h"
+#include "remmina_log.h"
+#include "remmina_main.h"
+#include "remmina_masterthread_exec.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_pref.h"
+#include "remmina_public.h"
+#include "remmina_sodium.h"
+#include "remmina_utils.h"
+
+#define MIN_WINDOW_WIDTH 10
+#define MIN_WINDOW_HEIGHT 10
+
+#define KEYFILE_GROUP_REMMINA "remmina"
+#define KEYFILE_GROUP_STATE "Remmina Connection States"
+
+static struct timespec times[2];
+
+static RemminaFile *
+remmina_file_new_empty(void)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+
+ remminafile = g_new0(RemminaFile, 1);
+ remminafile->settings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+ remminafile->states = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+ /* spsettings contains settings that are loaded from the secure_plugin.
+ * it’s used by remmina_file_store_secret_plugin_password() to know
+ * where to change */
+ remminafile->spsettings = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+ remminafile->prevent_saving = FALSE;
+ return remminafile;
+}
+
+RemminaFile *
+remmina_file_new(void)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+
+ /* Try to load from the preference file for default settings first */
+ remminafile = remmina_file_load(remmina_pref_file);
+
+ if (remminafile) {
+ g_free(remminafile->filename);
+ remminafile->filename = NULL;
+ } else {
+ remminafile = remmina_file_new_empty();
+ }
+
+ return remminafile;
+}
+
+/**
+ * Generate a new Remmina connection profile file name.
+ */
+void remmina_file_generate_filename(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+
+ /** File name restrictions:
+ * - Do not start with space.
+ * - Do not end with space or dot.
+ * - No more than 255 chars.
+ * - Do not contain \0.
+ * - Avoid % and $.
+ * - Avoid underscores and spaces for interoperabiility with everything else.
+ * - Better all lowercase.
+ */
+ gchar *invalid_chars = "\\%|/$?<>:*. \"";
+ GString *filenamestr;
+ const gchar *s;
+
+
+ /* functions we can use
+ * g_strstrip( string )
+ * Removes leading and trailing whitespace from a string
+ * g_strdelimit (str, invalid_chars, '-'))
+ * Convert each invalid_chars in a hyphen
+ * g_ascii_strdown(string)
+ * all lowercase
+ * To be safe we should remove control characters as well (but I'm lazy)
+ * https://rosettacode.org/wiki/Strip_control_codes_and_extended_characters_from_a_string#C
+ * g_utf8_strncpy (gchar *dest, const gchar *src, gsize n);
+ * copies a given number of characters instead of a given number of bytes. The src string must be valid UTF-8 encoded text.
+ * g_utf8_validate (const gchar *str, gssize max_len, const gchar **end);
+ * Validates UTF-8 encoded text.
+ */
+
+ //g_free(remminafile->filename), remminafile->filename = NULL;
+
+ filenamestr = g_string_new(g_strdup_printf("%s",
+ remmina_pref.remmina_file_name));
+ if ((s = remmina_file_get_string(remminafile, "name")) == NULL) s = "name";
+ if (g_strstr_len(filenamestr->str, -1, "%N") != NULL)
+ remmina_utils_string_replace_all(filenamestr, "%N", s);
+
+ if ((s = remmina_file_get_string(remminafile, "group")) == NULL) s = "group";
+ if (g_strstr_len(filenamestr->str, -1, "%G") != NULL)
+ remmina_utils_string_replace_all(filenamestr, "%G", s);
+
+ if ((s = remmina_file_get_string(remminafile, "protocol")) == NULL) s = "proto";
+ if (g_strstr_len(filenamestr->str, -1, "%P") != NULL)
+ remmina_utils_string_replace_all(filenamestr, "%P", s);
+
+ if ((s = remmina_file_get_string(remminafile, "server")) == NULL) s = "host";
+ if (g_strstr_len(filenamestr->str, -1, "%h") != NULL)
+ remmina_utils_string_replace_all(filenamestr, "%h", s);
+
+ s = NULL;
+
+ g_autofree gchar *filename = g_strdelimit(g_ascii_strdown(g_strstrip(g_string_free(filenamestr, FALSE)), -1),
+ invalid_chars, '-');
+
+ GDir *dir = g_dir_open(remmina_file_get_datadir(), 0, NULL);
+
+ if (dir != NULL)
+ remminafile->filename = g_strdup_printf("%s/%s.remmina", remmina_file_get_datadir(), filename);
+ else
+ remminafile->filename = NULL;
+ g_dir_close(dir);
+
+}
+
+void remmina_file_set_filename(RemminaFile *remminafile, const gchar *filename)
+{
+ TRACE_CALL(__func__);
+ g_free(remminafile->filename);
+ remminafile->filename = g_strdup(filename);
+}
+
+void remmina_file_set_statefile(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+
+ if (!remminafile)
+ return;
+ else
+ g_free(remminafile->statefile);
+
+ gchar *basename = g_path_get_basename(remminafile->filename);
+ gchar *cachedir = g_build_path("/", g_get_user_cache_dir(), "remmina", NULL);
+ GString *fname = g_string_new(basename);
+
+ remminafile->statefile = g_strdup_printf("%s/%s.state", cachedir, fname->str);
+
+ g_free(cachedir);
+ g_string_free(fname, TRUE);
+ g_free(basename);
+}
+
+const gchar *
+remmina_file_get_filename(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ return remminafile->filename;
+}
+
+RemminaFile *
+remmina_file_copy(const gchar *filename)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+ gchar *buf;
+
+ remminafile = remmina_file_load(filename);
+ buf = g_strdup_printf( "COPY %s",
+ remmina_file_get_string(remminafile, "name"));
+ remmina_file_set_string(remminafile, "name", buf);
+ g_free(buf);
+
+ if (remminafile)
+ remmina_file_generate_filename(remminafile);
+
+ return remminafile;
+}
+
+const RemminaProtocolSetting *find_protocol_setting(const gchar *name, RemminaProtocolPlugin *protocol_plugin)
+{
+ TRACE_CALL(__func__);
+ const RemminaProtocolSetting *setting_iter;
+
+ if (protocol_plugin == NULL)
+ return NULL;
+
+ setting_iter = protocol_plugin->basic_settings;
+ if (setting_iter) {
+ while (setting_iter->type != REMMINA_PROTOCOL_SETTING_TYPE_END) {
+ if (strcmp(name, remmina_plugin_manager_get_canonical_setting_name(setting_iter)) == 0)
+ return setting_iter;
+ setting_iter++;
+ }
+ }
+
+ setting_iter = protocol_plugin->advanced_settings;
+ if (setting_iter) {
+ while (setting_iter->type != REMMINA_PROTOCOL_SETTING_TYPE_END) {
+ if (strcmp(name, remmina_plugin_manager_get_canonical_setting_name(setting_iter)) == 0)
+ return setting_iter;
+ setting_iter++;
+ }
+ }
+
+ return NULL;
+}
+
+
+static void upgrade_sshkeys_202001_mig_common_setting(RemminaFile *remminafile, gboolean protocol_is_ssh, gboolean ssh_enabled, gchar *suffix)
+{
+ gchar *src_key;
+ gchar *dst_key;
+ const gchar *val;
+
+ src_key = g_strdup_printf("ssh_%s", suffix);
+ dst_key = g_strdup_printf("ssh_tunnel_%s", suffix);
+
+ val = remmina_file_get_string(remminafile, src_key);
+ if (!val) {
+ g_free(dst_key);
+ g_free(src_key);
+ return;
+ }
+
+ if (ssh_enabled && val && val[0] != 0)
+ remmina_file_set_string(remminafile, dst_key, val);
+
+ if (!protocol_is_ssh)
+ remmina_file_set_string(remminafile, src_key, NULL);
+
+ g_free(dst_key);
+ g_free(src_key);
+}
+
+static void upgrade_sshkeys_202001(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+
+ gboolean protocol_is_ssh;
+ gboolean ssh_enabled;
+ const gchar *val;
+
+ if (remmina_file_get_string(remminafile, "ssh_enabled")) {
+ /* Upgrade ssh params from remmina pre 1.4 */
+
+ ssh_enabled = remmina_file_get_int(remminafile, "ssh_enabled", 0);
+ val = remmina_file_get_string(remminafile, "protocol");
+ protocol_is_ssh = (strcmp(val, "SSH") == 0);
+
+ upgrade_sshkeys_202001_mig_common_setting(remminafile, protocol_is_ssh, ssh_enabled, "stricthostkeycheck");
+ upgrade_sshkeys_202001_mig_common_setting(remminafile, protocol_is_ssh, ssh_enabled, "kex_algorithms");
+ upgrade_sshkeys_202001_mig_common_setting(remminafile, protocol_is_ssh, ssh_enabled, "hostkeytypes");
+ upgrade_sshkeys_202001_mig_common_setting(remminafile, protocol_is_ssh, ssh_enabled, "ciphers");
+ upgrade_sshkeys_202001_mig_common_setting(remminafile, protocol_is_ssh, ssh_enabled, "proxycommand");
+ upgrade_sshkeys_202001_mig_common_setting(remminafile, protocol_is_ssh, ssh_enabled, "passphrase");
+ upgrade_sshkeys_202001_mig_common_setting(remminafile, protocol_is_ssh, ssh_enabled, "auth");
+ upgrade_sshkeys_202001_mig_common_setting(remminafile, protocol_is_ssh, ssh_enabled, "privatekey");
+
+ val = remmina_file_get_string(remminafile, "ssh_loopback");
+ if (val) {
+ remmina_file_set_string(remminafile, "ssh_tunnel_loopback", val);
+ remmina_file_set_string(remminafile, "ssh_loopback", NULL);
+ }
+
+ val = remmina_file_get_string(remminafile, "ssh_username");
+ if (val) {
+ remmina_file_set_string(remminafile, "ssh_tunnel_username", val);
+ if (protocol_is_ssh)
+ remmina_file_set_string(remminafile, "username", val);
+ remmina_file_set_string(remminafile, "ssh_username", NULL);
+ }
+
+ val = remmina_file_get_string(remminafile, "ssh_password");
+ if (val) {
+ remmina_file_set_string(remminafile, "ssh_tunnel_password", val);
+ if (protocol_is_ssh)
+ remmina_file_set_string(remminafile, "password", val);
+ remmina_file_set_string(remminafile, "ssh_password", NULL);
+ }
+
+ val = remmina_file_get_string(remminafile, "ssh_server");
+ if (val) {
+ remmina_file_set_string(remminafile, "ssh_tunnel_server", val);
+ remmina_file_set_string(remminafile, "ssh_server", NULL);
+ }
+
+ /* Real key removal will be done by remmina_file_save() */
+
+ remmina_file_set_int(remminafile, "ssh_tunnel_enabled", ssh_enabled);
+ }
+}
+
+RemminaFile *
+remmina_file_load(const gchar *filename)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+ RemminaFile *remminafile;
+ gchar *key;
+ gchar *s;
+ RemminaProtocolPlugin *protocol_plugin;
+ RemminaSecretPlugin *secret_plugin;
+ gboolean secret_service_available;
+ int w, h;
+
+ gkeyfile = g_key_file_new();
+
+ if (g_file_test(filename, G_FILE_TEST_IS_REGULAR | G_FILE_TEST_EXISTS)) {
+ if (!g_key_file_load_from_file(gkeyfile, filename, G_KEY_FILE_NONE, NULL)) {
+ g_key_file_free(gkeyfile);
+ REMMINA_DEBUG("Unable to load remmina profile file %s: g_key_file_load_from_file() returned NULL.\n", filename);
+ return NULL;
+ }
+ }
+
+ if (!g_key_file_has_key(gkeyfile, KEYFILE_GROUP_REMMINA, "name", NULL)) {
+
+ REMMINA_DEBUG("Unable to load remmina profile file %s: cannot find key name= in section remmina.\n", filename);
+ remminafile = NULL;
+ remmina_file_set_statefile(remminafile);
+
+ g_key_file_free(gkeyfile);
+
+ return remminafile;
+ }
+ remminafile = remmina_file_new_empty();
+
+ protocol_plugin = NULL;
+
+ /* Identify the protocol plugin and get pointers to its RemminaProtocolSetting structs */
+ gchar *proto = g_key_file_get_string(gkeyfile, KEYFILE_GROUP_REMMINA, "protocol", NULL);
+ if (proto) {
+ protocol_plugin = (RemminaProtocolPlugin *)remmina_plugin_manager_get_plugin(REMMINA_PLUGIN_TYPE_PROTOCOL, proto);
+ g_free(proto);
+ }
+
+ secret_plugin = remmina_plugin_manager_get_secret_plugin();
+ secret_service_available = secret_plugin && secret_plugin->is_service_available(secret_plugin);
+
+ remminafile->filename = g_strdup(filename);
+ gsize nkeys = 0;
+ gint keyindex;
+ GError *err = NULL;
+ gchar **keys = g_key_file_get_keys(gkeyfile, KEYFILE_GROUP_REMMINA, &nkeys, &err);
+ if (keys == NULL) {
+ g_clear_error(&err);
+ }
+ for (keyindex = 0; keyindex < nkeys; ++keyindex) {
+ key = keys[keyindex];
+ /* It may contain an encrypted password
+ * - password = . // secret_service
+ * - password = $argon2id$v=19$m=262144,t=3,p=… // libsodium
+ */
+ if (protocol_plugin && remmina_plugin_manager_is_encrypted_setting(protocol_plugin, key)) {
+ s = g_key_file_get_string(gkeyfile, KEYFILE_GROUP_REMMINA, key, NULL);
+#if 0
+ switch (remmina_pref.enc_mode) {
+ case RM_ENC_MODE_SODIUM_INTERACTIVE:
+ case RM_ENC_MODE_SODIUM_MODERATE:
+ case RM_ENC_MODE_SODIUM_SENSITIVE:
+#if SODIUM_VERSION_INT >= 90200
+#endif
+ break;
+ case RM_ENC_MODE_GCRYPT:
+ break;
+ case RM_ENC_MODE_SECRET:
+ default:
+ break;
+ }
+#endif
+ if ((g_strcmp0(s, ".") == 0) && (secret_service_available)) {
+ gchar *sec = secret_plugin->get_password(secret_plugin, remminafile, key);
+ remmina_file_set_string(remminafile, key, sec);
+ /* Annotate in spsettings that this value comes from secret_plugin */
+ g_hash_table_insert(remminafile->spsettings, g_strdup(key), NULL);
+ g_free(sec);
+ } else {
+ gchar *decrypted;
+ decrypted = remmina_crypt_decrypt(s);
+ remmina_file_set_string(remminafile, key, decrypted);
+ g_free(decrypted);
+ }
+ g_free(s), s = NULL;
+ } else {
+ /* If we find "resolution", then we split it in two */
+ if (strcmp(key, "resolution") == 0) {
+ gchar *resolution_str = g_key_file_get_string(gkeyfile, KEYFILE_GROUP_REMMINA, key, NULL);
+ if (remmina_public_split_resolution_string(resolution_str, &w, &h)) {
+ gchar *buf;
+ buf = g_strdup_printf("%i", w); remmina_file_set_string(remminafile, "resolution_width", buf); g_free(buf);
+ buf = g_strdup_printf("%i", h); remmina_file_set_string(remminafile, "resolution_height", buf); g_free(buf);
+ } else {
+ remmina_file_set_string(remminafile, "resolution_width", NULL);
+ remmina_file_set_string(remminafile, "resolution_height", NULL);
+ }
+ g_free(resolution_str);
+ } else {
+ gchar *value;
+ value = g_key_file_get_string(gkeyfile, KEYFILE_GROUP_REMMINA, key, NULL);
+ remmina_file_set_string(remminafile, key, value);
+ g_free(value);
+ }
+ }
+ }
+
+ upgrade_sshkeys_202001(remminafile);
+ g_strfreev(keys);
+ remmina_file_set_statefile(remminafile);
+ g_key_file_free(gkeyfile);
+ return remminafile;
+}
+
+void remmina_file_set_string(RemminaFile *remminafile, const gchar *setting, const gchar *value)
+{
+ TRACE_CALL(__func__);
+
+ /* Note: setting and value are copied on the heap, so it is responsibility of the caller
+ * to deallocate them when returning from remmina_file_set_string() if needed */
+
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread
+ * (plugins needs it to have user credentials)*/
+ RemminaMTExecData *d;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_FILE_SET_STRING;
+ d->p.file_set_string.remminafile = remminafile;
+ d->p.file_set_string.setting = setting;
+ d->p.file_set_string.value = value;
+ remmina_masterthread_exec_and_wait(d);
+ g_free(d);
+ return;
+ }
+
+ if (value) {
+ /* We refuse to accept to set the "resolution" field */
+ if (strcmp(setting, "resolution") == 0) {
+ // TRANSLATORS: This is a message that pops up when an external Remmina plugin tries to set the window resolution using a legacy parameter.
+ const gchar *message = _("Using the «resolution» parameter in the Remmina preferences file is deprecated.\n");
+ REMMINA_CRITICAL(message);
+ remmina_main_show_warning_dialog(message);
+ return;
+ }
+ g_hash_table_insert(remminafile->settings, g_strdup(setting), g_strdup(value));
+ } else {
+ g_hash_table_insert(remminafile->settings, g_strdup(setting), g_strdup(""));
+ }
+}
+
+void remmina_file_set_state(RemminaFile *remminafile, const gchar *setting, const gchar *value)
+{
+ TRACE_CALL(__func__);
+
+ if (value && value[0] != 0)
+ g_hash_table_insert(remminafile->states, g_strdup(setting), g_strdup(value));
+ else
+ g_hash_table_insert(remminafile->states, g_strdup(setting), g_strdup(""));
+}
+
+const gchar *
+remmina_file_get_string(RemminaFile *remminafile, const gchar *setting)
+{
+ TRACE_CALL(__func__);
+ gchar *value;
+
+ /* Returned value is a pointer to the string stored on the hash table,
+ * please do not free it or the hash table will contain invalid pointer */
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread
+ * (plugins needs it to have user credentials)*/
+ RemminaMTExecData *d;
+ const gchar *retval;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_FILE_GET_STRING;
+ d->p.file_get_string.remminafile = remminafile;
+ d->p.file_get_string.setting = setting;
+ remmina_masterthread_exec_and_wait(d);
+ retval = d->p.file_get_string.retval;
+ g_free(d);
+ return retval;
+ }
+
+ if (strcmp(setting, "resolution") == 0) {
+ // TRANSLATORS: This is a message that pop-up when an external Remmina plugin tries to set the windows resolution using a legacy parameter.
+ const gchar *message = _("Using the «resolution» parameter in the Remmina preferences file is deprecated.\n");
+ REMMINA_CRITICAL(message);
+ remmina_main_show_warning_dialog(message);
+ return NULL;
+ }
+
+ value = (gchar *)g_hash_table_lookup(remminafile->settings, setting);
+ return value && value[0] ? value : NULL;
+}
+
+gchar *
+remmina_file_get_secret(RemminaFile *remminafile, const gchar *setting)
+{
+ TRACE_CALL(__func__);
+
+ /* This function is in the RemminaPluginService table, we cannot remove it
+ * without breaking plugin API */
+ g_warning("remmina_file_get_secret(remminafile,“%s”) is deprecated and must not be called. Use remmina_file_get_string() and do not deallocate returned memory.\n", setting);
+ return g_strdup(remmina_file_get_string(remminafile, setting));
+}
+
+gchar *remmina_file_format_properties(RemminaFile *remminafile, const gchar *setting)
+{
+ gchar *res = NULL;
+ GString *fmt_str;
+ GDateTime *now;
+ gchar *date_str = NULL;
+
+ fmt_str = g_string_new(setting);
+ remmina_utils_string_replace_all(fmt_str, "%h", remmina_file_get_string(remminafile, "server"));
+ remmina_utils_string_replace_all(fmt_str, "%t", remmina_file_get_string(remminafile, "ssh_tunnel_server"));
+ remmina_utils_string_replace_all(fmt_str, "%u", remmina_file_get_string(remminafile, "username"));
+ remmina_utils_string_replace_all(fmt_str, "%U", remmina_file_get_string(remminafile, "ssh_tunnel_username"));
+ remmina_utils_string_replace_all(fmt_str, "%p", remmina_file_get_string(remminafile, "name"));
+ remmina_utils_string_replace_all(fmt_str, "%g", remmina_file_get_string(remminafile, "group"));
+
+ now = g_date_time_new_now_local();
+ date_str = g_date_time_format(now, "%FT%TZ");
+ remmina_utils_string_replace_all(fmt_str, "%d", date_str);
+ g_free(date_str);
+
+ res = g_string_free(fmt_str, FALSE);
+ return res;
+}
+
+void remmina_file_set_int(RemminaFile *remminafile, const gchar *setting, gint value)
+{
+ TRACE_CALL(__func__);
+ if (remminafile)
+ g_hash_table_insert(remminafile->settings,
+ g_strdup(setting),
+ g_strdup_printf("%i", value));
+}
+
+void remmina_file_set_state_int(RemminaFile *remminafile, const gchar *setting, gint value)
+{
+ TRACE_CALL(__func__);
+ if (remminafile)
+ g_hash_table_insert(remminafile->states,
+ g_strdup(setting),
+ g_strdup_printf("%i", value));
+}
+
+gint remmina_file_get_int(RemminaFile *remminafile, const gchar *setting, gint default_value)
+{
+ TRACE_CALL(__func__);
+ gchar *value;
+ gint r;
+
+ value = g_hash_table_lookup(remminafile->settings, setting);
+ r = value == NULL ? default_value : (value[0] == 't' ? TRUE : atoi(value));
+ // TOO verbose: REMMINA_DEBUG ("Integer value is: %d", r);
+ return r;
+}
+
+gint remmina_file_get_state_int(RemminaFile *remminafile, const gchar *setting, gint default_value)
+{
+ TRACE_CALL(__func__);
+ gchar *value;
+ gint r;
+
+ value = g_hash_table_lookup(remminafile->states, setting);
+ r = value == NULL ? default_value : (value[0] == 't' ? TRUE : atoi(value));
+ // TOO verbose: REMMINA_DEBUG ("Integer value is: %d", r);
+ return r;
+}
+
+// sscanf uses the set language to convert the float.
+// therefore '.' and ',' cannot be used interchangeably.
+gdouble remmina_file_get_double(RemminaFile * remminafile,
+ const gchar * setting,
+ gdouble default_value)
+{
+ TRACE_CALL(__func__);
+ gchar *value;
+
+ value = g_hash_table_lookup(remminafile->settings, setting);
+ if (!value)
+ return default_value;
+
+ // str to double.
+ // https://stackoverflow.com/questions/10075294/converting-string-to-a-double-variable-in-c
+ gdouble d;
+ gint ret = sscanf(value, "%lf", &d);
+
+ if (ret != 1)
+ // failed.
+ d = default_value;
+
+ // TOO VERBOSE: REMMINA_DEBUG("Double value is: %lf", d);
+ return d;
+}
+
+// sscanf uses the set language to convert the float.
+// therefore '.' and ',' cannot be used interchangeably.
+gdouble remmina_file_get_state_double(RemminaFile * remminafile,
+ const gchar * setting,
+ gdouble default_value)
+{
+ TRACE_CALL(__func__);
+ gchar *value;
+
+ value = g_hash_table_lookup(remminafile->states, setting);
+ if (!value)
+ return default_value;
+
+ // str to double.
+ // https://stackoverflow.com/questions/10075294/converting-string-to-a-double-variable-in-c
+ gdouble d;
+ gint ret = sscanf(value, "%lf", &d);
+
+ if (ret != 1)
+ // failed.
+ d = default_value;
+
+ // TOO VERBOSE: REMMINA_DEBUG("Double value is: %lf", d);
+ return d;
+}
+
+static GKeyFile *
+remmina_file_get_keyfile(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+
+ if (remminafile->filename == NULL)
+ return NULL;
+ gkeyfile = g_key_file_new();
+ if (!g_key_file_load_from_file(gkeyfile, remminafile->filename, G_KEY_FILE_NONE, NULL)) {
+ /* it will fail if it’s a new file, but shouldn’t matter. */
+ }
+ return gkeyfile;
+}
+
+static GKeyFile *
+remmina_file_get_keystate(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+
+ if (remminafile->statefile == NULL)
+ return NULL;
+ gkeyfile = g_key_file_new();
+ if (!g_key_file_load_from_file(gkeyfile, remminafile->statefile, G_KEY_FILE_NONE, NULL)) {
+ /* it will fail if it’s a new file, but shouldn’t matter. */
+ }
+ return gkeyfile;
+}
+
+void remmina_file_free(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ if (remminafile == NULL)
+ return;
+
+ if (remminafile->filename)
+ g_free(remminafile->filename);
+ if (remminafile->statefile)
+ g_free(remminafile->statefile);
+ if (remminafile->settings)
+ g_hash_table_destroy(remminafile->settings);
+ if (remminafile->spsettings)
+ g_hash_table_destroy(remminafile->spsettings);
+ if (remminafile->states)
+ g_hash_table_destroy(remminafile->states);
+
+ g_free(remminafile);
+}
+
+
+void remmina_file_save(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ RemminaSecretPlugin *secret_plugin;
+ gboolean secret_service_available;
+ RemminaProtocolPlugin *protocol_plugin;
+ GHashTableIter iter;
+ const gchar *key, *value;
+ gchar *s, *proto, *content;
+ gint nopasswdsave;
+ GKeyFile *gkeyfile;
+ GKeyFile *gkeystate;
+ gsize length = 0;
+ GError *err = NULL;
+
+ if (remminafile->prevent_saving)
+ return;
+
+ if ((gkeyfile = remmina_file_get_keyfile(remminafile)) == NULL)
+ return;
+
+ if ((gkeystate = remmina_file_get_keystate(remminafile)) == NULL)
+ return;
+
+ REMMINA_DEBUG("Saving profile");
+ /* get disablepasswordstoring */
+ nopasswdsave = remmina_file_get_int(remminafile, "disablepasswordstoring", 0);
+ /* Identify the protocol plugin and get pointers to its RemminaProtocolSetting structs */
+ proto = (gchar *)g_hash_table_lookup(remminafile->settings, "protocol");
+ if (proto) {
+ protocol_plugin = (RemminaProtocolPlugin *)remmina_plugin_manager_get_plugin(REMMINA_PLUGIN_TYPE_PROTOCOL, proto);
+ } else {
+ REMMINA_CRITICAL("Saving settings for unknown protocol:", proto);
+ protocol_plugin = NULL;
+ }
+
+ secret_plugin = remmina_plugin_manager_get_secret_plugin();
+ secret_service_available = secret_plugin && secret_plugin->is_service_available(secret_plugin);
+
+ g_hash_table_iter_init(&iter, remminafile->settings);
+ while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value)) {
+ if (remmina_plugin_manager_is_encrypted_setting(protocol_plugin, key)) {
+ if (remminafile->filename && g_strcmp0(remminafile->filename, remmina_pref_file)) {
+ if (secret_service_available && nopasswdsave == 0) {
+ REMMINA_DEBUG("We have a secret and disablepasswordstoring=0");
+ if (value && value[0]) {
+ if (g_strcmp0(value, ".") != 0)
+ secret_plugin->store_password(secret_plugin, remminafile, key, value);
+ g_key_file_set_string(gkeyfile, KEYFILE_GROUP_REMMINA, key, ".");
+ } else {
+ g_key_file_set_string(gkeyfile, KEYFILE_GROUP_REMMINA, key, "");
+ secret_plugin->delete_password(secret_plugin, remminafile, key);
+ }
+ } else {
+ REMMINA_DEBUG("We have a password and disablepasswordstoring=0");
+ if (value && value[0] && nopasswdsave == 0) {
+ s = remmina_crypt_encrypt(value);
+ g_key_file_set_string(gkeyfile, KEYFILE_GROUP_REMMINA, key, s);
+ g_free(s);
+ } else {
+ g_key_file_set_string(gkeyfile, KEYFILE_GROUP_REMMINA, key, "");
+ }
+ }
+ if (secret_service_available && nopasswdsave == 1) {
+ if (value && value[0]) {
+ if (g_strcmp0(value, ".") != 0) {
+ REMMINA_DEBUG("Deleting the secret in the keyring as disablepasswordstoring=1");
+ secret_plugin->delete_password(secret_plugin, remminafile, key);
+ g_key_file_set_string(gkeyfile, KEYFILE_GROUP_REMMINA, key, ".");
+ }
+ }
+ }
+ }
+ } else {
+ g_key_file_set_string(gkeyfile, KEYFILE_GROUP_REMMINA, key, value);
+ }
+ }
+
+ /* Avoid storing redundant and deprecated "resolution" field */
+ g_key_file_remove_key(gkeyfile, KEYFILE_GROUP_REMMINA, "resolution", NULL);
+
+ /* Delete old pre-1.4 ssh keys */
+ g_key_file_remove_key(gkeyfile, KEYFILE_GROUP_REMMINA, "ssh_enabled", NULL);
+ g_key_file_remove_key(gkeyfile, KEYFILE_GROUP_REMMINA, "save_ssh_server", NULL);
+ g_key_file_remove_key(gkeyfile, KEYFILE_GROUP_REMMINA, "save_ssh_username", NULL);
+
+ /* Store gkeyfile to disk (password are already sent to keyring) */
+ content = g_key_file_to_data(gkeyfile, &length, NULL);
+
+ if (g_file_set_contents(remminafile->filename, content, length, &err))
+ REMMINA_DEBUG("Profile saved");
+ else
+ REMMINA_WARNING("Remmina connection profile cannot be saved, with error %d (%s)", err->code, err->message);
+ if (err != NULL)
+ g_error_free(err);
+
+ g_free(content), content = NULL;
+ /* Saving states */
+ g_hash_table_iter_init(&iter, remminafile->states);
+ while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value))
+ g_key_file_set_string(gkeyfile, KEYFILE_GROUP_STATE, key, value);
+ content = g_key_file_to_data(gkeystate, &length, NULL);
+ if (g_file_set_contents(remminafile->statefile, content, length, &err))
+ REMMINA_DEBUG("Connection profile states saved");
+ else
+ REMMINA_WARNING("Remmina connection profile cannot be saved, with error %d (%s)", err->code, err->message);
+ if (err != NULL)
+ g_error_free(err);
+ g_free(content), content = NULL;
+ g_key_file_free(gkeyfile);
+ g_key_file_free(gkeystate);
+
+ if (!remmina_pref.list_refresh_workaround)
+ remmina_main_update_file_datetime(remminafile);
+}
+
+void remmina_file_store_secret_plugin_password(RemminaFile *remminafile, const gchar *key, const gchar *value)
+{
+ TRACE_CALL(__func__);
+
+ /* Only change the password in the keyring. This function
+ * is a shortcut which avoids updating of date/time of .pref file
+ * when possible, and is used by the mpchanger */
+ RemminaSecretPlugin *plugin;
+
+ if (g_hash_table_lookup_extended(remminafile->spsettings, g_strdup(key), NULL, NULL)) {
+ plugin = remmina_plugin_manager_get_secret_plugin();
+ plugin->store_password(plugin, remminafile, key, value);
+ } else {
+ remmina_file_set_string(remminafile, key, value);
+ remmina_file_save(remminafile);
+ }
+}
+
+RemminaFile *
+remmina_file_dup(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *dupfile;
+ GHashTableIter iter;
+ const gchar *key, *value;
+
+ dupfile = remmina_file_new_empty();
+ dupfile->filename = g_strdup(remminafile->filename);
+
+ g_hash_table_iter_init(&iter, remminafile->settings);
+ while (g_hash_table_iter_next(&iter, (gpointer *)&key, (gpointer *)&value))
+ remmina_file_set_string(dupfile, key, value);
+
+ remmina_file_set_statefile(dupfile);
+ remmina_file_touch(dupfile);
+ return dupfile;
+}
+
+const gchar *
+remmina_file_get_icon_name(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolPlugin *plugin;
+
+ plugin = (RemminaProtocolPlugin *)remmina_plugin_manager_get_plugin(REMMINA_PLUGIN_TYPE_PROTOCOL,
+ remmina_file_get_string(remminafile, "protocol"));
+ if (!plugin)
+ return g_strconcat(REMMINA_APP_ID, "-symbolic", NULL);
+
+ return remmina_file_get_int(remminafile, "ssh_tunnel_enabled", FALSE) ? plugin->icon_name_ssh : plugin->icon_name;
+}
+
+RemminaFile *
+remmina_file_dup_temp_protocol(RemminaFile *remminafile, const gchar *new_protocol)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *tmp;
+
+ tmp = remmina_file_dup(remminafile);
+ g_free(tmp->filename);
+ tmp->filename = NULL;
+ remmina_file_set_string(tmp, "protocol", new_protocol);
+ return tmp;
+}
+
+void remmina_file_delete(const gchar *filename)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+
+ remminafile = remmina_file_load(filename);
+ if (remminafile) {
+ remmina_file_unsave_passwords(remminafile);
+ remmina_file_free(remminafile);
+ }
+ g_unlink(filename);
+}
+
+const gchar *
+remmina_file_get_state(RemminaFile *remminafile, const gchar *setting)
+{
+ TRACE_CALL(__func__);
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GKeyFile) key_file = g_key_file_new();
+
+ if (!g_key_file_load_from_file(key_file, remminafile->statefile, G_KEY_FILE_NONE, &error)) {
+ if (!g_error_matches(error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ REMMINA_CRITICAL("Could not load the state file. %s", error->message);
+ return NULL;
+ }
+
+ g_autofree gchar *val = g_key_file_get_string(key_file, KEYFILE_GROUP_STATE, setting, &error);
+
+ if (val == NULL &&
+ !g_error_matches(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) {
+ REMMINA_CRITICAL("Could not find \"%s\" in the \"%s\" state file. %s",
+ setting, remminafile->statefile, error->message);
+ return NULL;
+ }
+ return val && val[0] ? val : NULL;
+}
+
+void remmina_file_state_last_success(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+
+ g_autoptr(GKeyFile) key_statefile = g_key_file_new();
+ g_autoptr(GKeyFile) key_remminafile = g_key_file_new();
+ GError *error = NULL;
+
+ const gchar *date = NULL;
+ GDateTime *d = g_date_time_new_now_utc();
+
+ date = g_strdup_printf("%d%02d%02d",
+ g_date_time_get_year(d),
+ g_date_time_get_month(d),
+ g_date_time_get_day_of_month(d));
+
+ g_key_file_set_string(key_statefile, KEYFILE_GROUP_STATE, "last_success", date);
+
+ REMMINA_DEBUG("State file %s.", remminafile->statefile);
+ if (!g_key_file_save_to_file(key_statefile, remminafile->statefile, &error)) {
+ REMMINA_CRITICAL("Could not save the key file. %s", error->message);
+ g_error_free(error);
+ error = NULL;
+ return;
+ }
+ /* Delete old pre-1.5 keys */
+ g_key_file_remove_key(key_remminafile, KEYFILE_GROUP_REMMINA, "last_success", NULL);
+ REMMINA_DEBUG("Last connection made on %s.", date);
+}
+
+void remmina_file_unsave_passwords(RemminaFile *remminafile)
+{
+ /* Delete all saved secrets for this profile */
+
+ TRACE_CALL(__func__);
+ const RemminaProtocolSetting *setting_iter;
+ RemminaProtocolPlugin *protocol_plugin;
+ gchar *proto;
+
+ protocol_plugin = NULL;
+
+ remmina_file_set_string(remminafile, "password", NULL);
+
+ proto = (gchar *)g_hash_table_lookup(remminafile->settings, "protocol");
+ if (proto) {
+ protocol_plugin = (RemminaProtocolPlugin *)remmina_plugin_manager_get_plugin(REMMINA_PLUGIN_TYPE_PROTOCOL, proto);
+ if (protocol_plugin) {
+ setting_iter = protocol_plugin->basic_settings;
+ if (setting_iter) {
+ while (setting_iter->type != REMMINA_PROTOCOL_SETTING_TYPE_END) {
+ // TOO VERBOSE: g_debug("setting name: %s", setting_iter->name);
+ if (setting_iter->name == NULL)
+ g_error("Internal error: a setting name in protocol plugin %s is null. Please fix RemminaProtocolSetting struct content.", proto);
+ else
+ if (remmina_plugin_manager_is_encrypted_setting(protocol_plugin, setting_iter->name))
+ remmina_file_set_string(remminafile, remmina_plugin_manager_get_canonical_setting_name(setting_iter), NULL);
+ setting_iter++;
+ }
+ }
+ setting_iter = protocol_plugin->advanced_settings;
+ if (setting_iter) {
+ while (setting_iter->type != REMMINA_PROTOCOL_SETTING_TYPE_END) {
+ if (remmina_plugin_manager_is_encrypted_setting(protocol_plugin, setting_iter->name))
+ remmina_file_set_string(remminafile, remmina_plugin_manager_get_canonical_setting_name(setting_iter), NULL);
+ setting_iter++;
+ }
+ }
+ remmina_file_save(remminafile);
+ }
+ }
+}
+
+/**
+ * Return the string date of the last time a Remmina state file has been modified.
+ *
+ * This is used to return the modification date of a file and it’s used
+ * to return the modification date and time of a given Remmina file.
+ * If it fails it will return "Fri, 16 Oct 2009 07:04:46 GMT", that is just a date to don't
+ * return an empty string (challenge: what was happened that day at that time?).
+ * @return A date string in the form "%d/%m/%Y %H:%M:%S".
+ * @todo This should be moved to remmina_utils.c
+ */
+gchar *
+remmina_file_get_datetime(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+
+ GFile *file;
+ GFileInfo *info;
+
+ struct timeval tv;
+ struct tm *ptm;
+ char time_string[256];
+ gchar *tmps;
+
+ guint64 mtime;
+
+ if (remminafile->statefile)
+ //REMMINA_DEBUG ("remminafile->statefile: %s", remminafile->statefile);
+ file = g_file_new_for_path(remminafile->statefile);
+ else
+ file = g_file_new_for_path(remminafile->filename);
+
+ info = g_file_query_info(file,
+ G_FILE_ATTRIBUTE_TIME_MODIFIED,
+ G_FILE_QUERY_INFO_NONE,
+ NULL,
+ NULL);
+
+ g_object_unref(file);
+
+ if (info == NULL) {
+ //REMMINA_DEBUG("could not get time info");
+
+ // The BDAY "Fri, 16 Oct 2009 07:04:46 GMT"
+ mtime = 1255676686;
+ const gchar *last_success = remmina_file_get_string(remminafile, "last_success");
+ if (last_success) {
+ //REMMINA_DEBUG ("Last success is %s", last_success);
+ GDateTime *dt;
+ tmps = g_strconcat(last_success, "T00:00:00Z", NULL);
+ dt = g_date_time_new_from_iso8601(tmps, NULL);
+ g_free(tmps);
+ if (dt) {
+ //REMMINA_DEBUG("Converting last_success");
+ tmps = g_date_time_format(dt, "%s");
+ mtime = g_ascii_strtoull(tmps, NULL, 10);
+ g_free(tmps);
+ g_date_time_unref(dt);
+ } else {
+ //REMMINA_DEBUG("dt was null");
+ mtime = 191543400;
+ }
+ }
+ } else {
+ mtime = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
+ g_object_unref(info);
+ }
+
+ tv.tv_sec = mtime;
+
+ ptm = localtime(&tv.tv_sec);
+ strftime(time_string, sizeof(time_string), "%F - %T", ptm);
+
+ gchar *modtime_string = g_locale_to_utf8(time_string, -1, NULL, NULL, NULL);
+
+ return modtime_string;
+}
+
+/**
+ * Update the atime and mtime of a given filename.
+ * Function used to update the atime and mtime of a given remmina file, partially
+ * taken from suckless sbase
+ * @see https://git.suckless.org/sbase/tree/touch.c
+ * @todo This should be moved to remmina_utils.c
+ */
+void
+remmina_file_touch(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ int fd;
+ struct stat st;
+ int r;
+
+ if ((r = stat(remminafile->statefile, &st)) < 0) {
+ if (errno != ENOENT)
+ REMMINA_DEBUG("stat %s:", remminafile->statefile);
+ } else if (!r) {
+#ifdef __APPLE__
+ times[0] = st.st_atimespec;
+ times[1] = st.st_mtimespec;
+#else
+ times[0] = st.st_atim;
+ times[1] = st.st_mtim;
+#endif
+ if (utimensat(AT_FDCWD, remminafile->statefile, times, 0) < 0)
+ REMMINA_DEBUG("utimensat %s:", remminafile->statefile);
+ return;
+ }
+
+ if ((fd = open(remminafile->statefile, O_CREAT | O_EXCL, 0644)) < 0)
+ REMMINA_DEBUG("open %s:", remminafile->statefile);
+ close(fd);
+
+ remmina_file_touch(remminafile);
+}
diff --git a/src/remmina_file.h b/src/remmina_file.h
new file mode 100644
index 0000000..9696441
--- /dev/null
+++ b/src/remmina_file.h
@@ -0,0 +1,126 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 <glib-object.h>
+#include <gobject/gvaluecollector.h>
+
+#include "remmina/types.h"
+
+#pragma once
+
+G_BEGIN_DECLS
+
+struct _RemminaFile {
+ gchar * filename;
+ // @todo Add a cache file with content remminafile->filename = last_success
+ gchar * statefile;
+ GHashTable * settings;
+ GHashTable * states;
+ GHashTable * spsettings;
+ gboolean prevent_saving;
+};
+
+/**
+ * used in remmina_ssh.c and remmina_ssh_plugin.c
+ *
+ * #define SSH_AUTH_METHOD_UNKNOWN 0x0000u
+ * #define SSH_AUTH_METHOD_NONE 0x0001u
+ * #define SSH_AUTH_METHOD_PASSWORD 0x0002u
+ * #define SSH_AUTH_METHOD_PUBLICKEY 0x0004u
+ * #define SSH_AUTH_METHOD_HOSTBASED 0x0008u
+ * #define SSH_AUTH_METHOD_INTERACTIVE 0x0010u
+ * #define SSH_AUTH_METHOD_GSSAPI_MIC 0x0020u
+ */
+enum {
+ SSH_AUTH_PASSWORD,
+ SSH_AUTH_PUBLICKEY,
+ SSH_AUTH_AGENT,
+ SSH_AUTH_AUTO_PUBLICKEY,
+ SSH_AUTH_GSSAPI,
+ SSH_AUTH_KBDINTERACTIVE
+};
+
+
+#define TOOLBAR_OPACITY_LEVEL 8
+#define TOOLBAR_OPACITY_MIN 0.2
+
+/* Create a empty .remmina file */
+RemminaFile *remmina_file_new(void);
+RemminaFile *remmina_file_copy(const gchar *filename);
+void remmina_file_generate_filename(RemminaFile *remminafile);
+void remmina_file_set_filename(RemminaFile *remminafile, const gchar *filename);
+void remmina_file_set_statefile(RemminaFile *remminafile);
+void remmina_file_state_last_success(RemminaFile *remminafile);
+const gchar *remmina_file_get_filename(RemminaFile *remminafile);
+const gchar *remmina_file_get_statefile(RemminaFile *remminafile);
+/* Load a new .remmina file and return the allocated RemminaFile object */
+RemminaFile *remmina_file_load(const gchar *filename);
+/* Settings get/set functions */
+void remmina_file_set_string(RemminaFile *remminafile, const gchar *setting, const gchar *value);
+const gchar *remmina_file_get_string(RemminaFile *remminafile, const gchar *setting);
+gchar *remmina_file_get_secret(RemminaFile *remminafile, const gchar *setting);
+gchar *remmina_file_format_properties(RemminaFile *remminafile, const gchar *setting);
+void remmina_file_set_int(RemminaFile *remminafile, const gchar *setting, gint value);
+gint remmina_file_get_int(RemminaFile *remminafile, const gchar *setting, gint default_value);
+gdouble remmina_file_get_double(RemminaFile *remminafile, const gchar *setting, gdouble default_value);
+void remmina_file_store_secret_plugin_password(RemminaFile *remminafile, const gchar *key, const gchar *value);
+gboolean remmina_file_remove_key(RemminaFile *remminafile, const gchar *setting);
+void remmina_file_set_state(RemminaFile *remminafile, const gchar *setting, const gchar *value);
+const gchar *remmina_file_get_state(RemminaFile *remminafile, const gchar *setting);
+void remmina_file_set_state_int(RemminaFile *remminafile, const gchar *setting, gint value);
+gint remmina_file_get_state_int(RemminaFile *remminafile, const gchar *setting, gint default_value);
+gdouble remmina_file_get_state_double(RemminaFile *remminafile, const gchar *setting, gdouble default_value);
+/* Create or overwrite the .remmina file */
+void remmina_file_save(RemminaFile *remminafile);
+/* Free the RemminaFile object */
+void remmina_file_free(RemminaFile *remminafile);
+/* Duplicate a RemminaFile object */
+RemminaFile *remmina_file_dup(RemminaFile *remminafile);
+/* Get the protocol icon name */
+const gchar *remmina_file_get_icon_name(RemminaFile *remminafile);
+/* Duplicate a temporary RemminaFile and change the protocol */
+RemminaFile *remmina_file_dup_temp_protocol(RemminaFile *remminafile, const gchar *new_protocol);
+/* Delete a .remmina file */
+void remmina_file_delete(const gchar *filename);
+/* Delete a "password" field and save into .remmina file */
+void remmina_file_unsave_passwords(RemminaFile *remminafile);
+/* Function used to update the atime and mtime of a given remmina file, partially
+ * taken from suckless sbase */
+gchar *remmina_file_get_datetime(RemminaFile *remminafile);
+/* Function used to update the atime and mtime of a given remmina file */
+void remmina_file_touch(RemminaFile *remminafile);
+
+G_END_DECLS
diff --git a/src/remmina_file_editor.c b/src/remmina_file_editor.c
new file mode 100644
index 0000000..feca2bc
--- /dev/null
+++ b/src/remmina_file_editor.c
@@ -0,0 +1,2204 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-2011 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-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.
+ *
+ */
+
+#include <ctype.h>
+#include "config.h"
+#ifdef HAVE_LIBAVAHI_UI
+#include <avahi-ui/avahi-ui.h>
+#endif
+#include "remmina_public.h"
+#include "remmina_pref.h"
+#include "rcw.h"
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_file.h"
+#include "remmina_file_editor.h"
+#include "remmina_file_manager.h"
+#include "remmina_icon.h"
+#include "remmina_main.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_pref_dialog.h"
+#include "remmina_ssh.h"
+#include "remmina_string_list.h"
+#include "remmina_unlock.h"
+#include "remmina_widget_pool.h"
+
+G_DEFINE_TYPE(RemminaFileEditor, remmina_file_editor, GTK_TYPE_DIALOG)
+
+static const gchar *server_tips = N_("<big>"
+ "Supported formats\n"
+ "• server\n"
+ "• server[:port]\n"
+ "VNC additional formats\n"
+ "• ID:repeater ID number\n"
+ "• unix:///path/socket.sock"
+ "</big>");
+
+static const gchar *cmd_tips = N_("<big>"
+ "• command in PATH args %h\n"
+ "• /path/to/foo -options %h %u\n"
+ "• %h is substituted with the server name\n"
+ "• %t is substituted with the SSH server name\n"
+ "• %u is substituted with the username\n"
+ "• %U is substituted with the SSH username\n"
+ "• %p is substituted with Remmina profile name\n"
+ "• %g is substituted with Remmina profile group name\n"
+ "• %d is substituted with local date and time in ISO 8601 format\n"
+ "Do not run in background if you want the command to be executed before connecting.\n"
+ "</big>");
+
+#ifdef HAVE_LIBSSH
+static const gchar *server_tips2 = N_("<big>"
+ "Supported formats\n"
+ "• server\n"
+ "• server[:port]\n"
+ "• username@server[:port] (SSH protocol only)"
+ "</big>");
+#endif
+
+struct _RemminaFileEditorPriv {
+ RemminaFile * remmina_file;
+ RemminaProtocolPlugin * plugin;
+ const gchar * avahi_service_type;
+
+ GtkWidget * name_entry;
+ GtkWidget * labels_entry;
+ GtkWidget * group_combo;
+ GtkWidget * protocol_combo;
+ GtkWidget * save_button;
+
+ GtkWidget * config_box;
+ GtkWidget * config_scrollable;
+ GtkWidget * config_viewport;
+ GtkWidget * config_container;
+
+ GtkWidget * server_combo;
+ GtkWidget * resolution_iws_radio;
+ GtkWidget * resolution_auto_radio;
+ GtkWidget * resolution_custom_radio;
+ GtkWidget * resolution_custom_combo;
+ GtkWidget * keymap_combo;
+
+ GtkWidget * assistance_toggle;
+ GtkWidget * assistance_file;
+ GtkWidget * assistance_password;
+ GtkWidget * assistance_file_label;
+ GtkWidget * assistance_password_label;
+
+ GtkWidget * behavior_autostart_check;
+ GtkWidget * behavior_precommand_entry;
+ GtkWidget * behavior_postcommand_entry;
+ GtkWidget * behavior_lock_check;
+ GtkWidget * behavior_disconnect;
+
+ GtkWidget * ssh_tunnel_enabled_check;
+ GtkWidget * ssh_tunnel_loopback_check;
+ GtkWidget * ssh_tunnel_server_default_radio;
+ GtkWidget * ssh_tunnel_server_custom_radio;
+ GtkWidget * ssh_tunnel_server_entry;
+ GtkWidget * ssh_tunnel_auth_agent_radio;
+ GtkWidget * ssh_tunnel_auth_password_radio;
+ GtkWidget * ssh_tunnel_auth_password;
+ GtkWidget * ssh_tunnel_passphrase;
+ GtkWidget * ssh_tunnel_auth_publickey_radio;
+ GtkWidget * ssh_tunnel_auth_auto_publickey_radio;
+ GtkWidget * ssh_tunnel_auth_combo;
+ GtkWidget * ssh_tunnel_username_entry;
+ GtkWidget * ssh_tunnel_privatekey_chooser;
+ GtkWidget * ssh_tunnel_certfile_chooser;
+
+ GHashTable * setting_widgets;
+};
+
+static void remmina_file_editor_class_init(RemminaFileEditorClass *klass)
+{
+ TRACE_CALL(__func__);
+}
+
+/**
+ * @brief Shows a tooltip-like window which tells the user what they did wrong
+ * to trigger the validation function of a ProtocolSetting widget.
+ *
+ * @param gfe GtkWindow gfe
+ * @param failed_widget Widget which failed validation
+ * @param err Contains error message for user
+ *
+ *
+ * Mouse click and focus-loss will delete the window. \n
+ * TODO: when Remmina Editor's content is scrollable and failed_widget is not even
+ * visible anymore, the window gets shown where failed_widget would be if
+ * the Remmina Editor was big enough. \n
+ * TODO: Responsive text size and line wrap.
+ */
+static void remmina_file_editor_show_validation_error_popup(RemminaFileEditor * gfe,
+ GtkWidget * failed_widget,
+ GError * err)
+{
+ if (!err) {
+ err = NULL; // g_set_error doesn't like overwriting errors.
+ g_set_error(&err, 1, 1, _("Input is invalid."));
+ }
+
+ if (!gfe || !failed_widget) {
+ g_critical("(%s): Parameters RemminaFileEditor 'gfe' or "
+ "GtkWidget* 'failed_widget' are 'NULL'!",
+ __func__);
+ return;
+ }
+
+ gint widget_width = gtk_widget_get_allocated_width(failed_widget);
+ gint widget_height = gtk_widget_get_allocated_height(failed_widget);
+
+ GtkWidget *err_label = gtk_label_new("");
+ GtkWidget *alert_icon = NULL;
+ GtkWindow *err_window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
+ GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ GdkWindow *window = gtk_widget_get_window(failed_widget);
+
+ GtkAllocation allocation;
+ gint failed_widget_x, failed_widget_y;
+
+ gchar *markup = g_strdup_printf("<span size='large'>%s</span>", err->message);
+
+ // Setup err_window
+ gtk_window_set_decorated(err_window, FALSE);
+ gtk_window_set_type_hint(err_window, GDK_WINDOW_TYPE_HINT_TOOLTIP);
+ gtk_window_set_default_size(err_window, widget_width, widget_height);
+ gtk_window_set_title(err_window, "Error");
+ gtk_window_set_resizable(err_window, TRUE);
+
+ // Move err_window under failed_widget
+ gtk_window_set_attached_to(err_window, failed_widget);
+ gtk_window_set_transient_for(err_window, GTK_WINDOW(gfe));
+ gdk_window_get_origin(GDK_WINDOW(window), &failed_widget_x, &failed_widget_y);
+ gtk_widget_get_allocation(failed_widget, &allocation);
+ failed_widget_x += allocation.x;
+ failed_widget_y += allocation.y + allocation.height;
+ gtk_window_move(err_window, failed_widget_x, failed_widget_y);
+
+ // Setup label
+ gtk_label_set_selectable(GTK_LABEL(err_label), FALSE);
+ gtk_label_set_max_width_chars(GTK_LABEL(err_label), 1);
+ gtk_widget_set_hexpand(GTK_WIDGET(err_label), TRUE);
+ gtk_widget_set_vexpand(GTK_WIDGET(err_label), TRUE);
+ gtk_label_set_ellipsize(GTK_LABEL(err_label), PANGO_ELLIPSIZE_END);
+ gtk_label_set_line_wrap(GTK_LABEL(err_label), TRUE);
+ gtk_label_set_line_wrap_mode(GTK_LABEL(err_label), PANGO_WRAP_WORD_CHAR);
+ gtk_label_set_markup(GTK_LABEL(err_label), markup);
+
+ alert_icon = gtk_image_new_from_icon_name("dialog-warning-symbolic",
+ GTK_ICON_SIZE_DND);
+
+ // Fill icon and label into a box.
+ gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(alert_icon), FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(box), GTK_WIDGET(err_label), TRUE, TRUE, 5);
+
+ // Attach box to err_window
+ gtk_container_add(GTK_CONTAINER(err_window), GTK_WIDGET(box));
+
+ // Display everything.
+ gtk_widget_show_all(GTK_WIDGET(err_window));
+
+ // Mouse click and focus-loss will delete the err_window.
+ g_signal_connect(G_OBJECT(err_window), "focus-out-event",
+ G_CALLBACK(gtk_window_close), NULL);
+ g_signal_connect(G_OBJECT(err_window), "button-press-event",
+ G_CALLBACK(gtk_window_close), NULL);
+}
+
+#ifdef HAVE_LIBAVAHI_UI
+
+static void remmina_file_editor_browse_avahi(GtkWidget *button, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *dialog;
+ gchar *host;
+
+ dialog = aui_service_dialog_new(_("Choose a Remote Desktop Server"),
+ GTK_WINDOW(gfe),
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ _("_OK"), GTK_RESPONSE_ACCEPT,
+ NULL);
+
+ gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(gfe));
+ aui_service_dialog_set_resolve_service(AUI_SERVICE_DIALOG(dialog), TRUE);
+ aui_service_dialog_set_resolve_host_name(AUI_SERVICE_DIALOG(dialog), TRUE);
+ aui_service_dialog_set_browse_service_types(AUI_SERVICE_DIALOG(dialog),
+ gfe->priv->avahi_service_type, NULL);
+
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ host = g_strdup_printf("[%s]:%i",
+ aui_service_dialog_get_host_name(AUI_SERVICE_DIALOG(dialog)),
+ aui_service_dialog_get_port(AUI_SERVICE_DIALOG(dialog)));
+ } else {
+ host = NULL;
+ }
+ gtk_widget_destroy(dialog);
+
+ if (host) {
+ gtk_entry_set_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(gfe->priv->server_combo))), host);
+ g_free(host);
+ }
+}
+#endif
+
+static void remmina_file_editor_on_realize(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditor *gfe;
+ GtkWidget *defaultwidget;
+
+ gfe = REMMINA_FILE_EDITOR(widget);
+
+ defaultwidget = gfe->priv->server_combo;
+
+ if (defaultwidget) {
+ if (GTK_IS_EDITABLE(defaultwidget))
+ gtk_editable_select_region(GTK_EDITABLE(defaultwidget), 0, -1);
+ gtk_widget_grab_focus(defaultwidget);
+ }
+}
+
+static void remmina_file_editor_destroy(GtkWidget *widget, gpointer data)
+{
+ TRACE_CALL(__func__);
+ remmina_file_free(REMMINA_FILE_EDITOR(widget)->priv->remmina_file);
+ g_hash_table_destroy(REMMINA_FILE_EDITOR(widget)->priv->setting_widgets);
+ g_free(REMMINA_FILE_EDITOR(widget)->priv);
+}
+
+static void remmina_file_editor_button_on_toggled(GtkToggleButton *togglebutton, GtkWidget *widget)
+{
+ TRACE_CALL(__func__);
+ gtk_widget_set_sensitive(widget, gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(togglebutton)));
+}
+
+static void remmina_file_editor_create_notebook_container(RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ /* Create the notebook */
+ gfe->priv->config_container = gtk_notebook_new();
+ gfe->priv->config_viewport = gtk_viewport_new(NULL, NULL);
+ gfe->priv->config_scrollable = gtk_scrolled_window_new(NULL, NULL);
+ gtk_container_set_border_width(GTK_CONTAINER(gfe->priv->config_scrollable), 2);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(gfe->priv->config_scrollable),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
+ gtk_widget_show(gfe->priv->config_scrollable);
+
+ gtk_container_add(GTK_CONTAINER(gfe->priv->config_viewport), gfe->priv->config_container);
+ gtk_container_set_border_width(GTK_CONTAINER(gfe->priv->config_viewport), 2);
+ gtk_widget_show(gfe->priv->config_viewport);
+ gtk_container_add(GTK_CONTAINER(gfe->priv->config_scrollable), gfe->priv->config_viewport);
+ gtk_container_set_border_width(GTK_CONTAINER(gfe->priv->config_container), 2);
+ gtk_widget_show(gfe->priv->config_container);
+
+ gtk_container_add(GTK_CONTAINER(gfe->priv->config_box), gfe->priv->config_scrollable);
+}
+
+static GtkWidget *remmina_file_editor_create_notebook_tab(RemminaFileEditor *gfe,
+ const gchar *stock_id, const gchar *label, gint rows, gint cols)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *tablabel;
+ GtkWidget *tabbody;
+ GtkWidget *grid;
+ GtkWidget *widget;
+
+ tablabel = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show(tablabel);
+
+ widget = gtk_image_new_from_icon_name(stock_id, GTK_ICON_SIZE_BUTTON);
+ gtk_box_pack_start(GTK_BOX(tablabel), widget, FALSE, FALSE, 0);
+ gtk_widget_show(widget);
+
+ widget = gtk_label_new(label);
+ gtk_box_pack_start(GTK_BOX(tablabel), widget, FALSE, FALSE, 0);
+ gtk_widget_show(widget);
+
+ tabbody = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_show(tabbody);
+ gtk_notebook_append_page(GTK_NOTEBOOK(gfe->priv->config_container), tabbody, tablabel);
+
+ grid = gtk_grid_new();
+ gtk_widget_show(grid);
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
+ gtk_container_set_border_width(GTK_CONTAINER(grid), 15);
+ gtk_box_pack_start(GTK_BOX(tabbody), grid, FALSE, FALSE, 0);
+
+ return grid;
+}
+
+
+static void remmina_file_editor_assistance_enabled_check_on_toggled(GtkToggleButton *togglebutton,
+ RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ gboolean enabled = TRUE;
+
+ if (gfe->priv->assistance_toggle) {
+ enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gfe->priv->assistance_toggle));
+ if (gfe->priv->assistance_file)
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->assistance_file), enabled);
+ if (gfe->priv->assistance_password)
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->assistance_password), enabled);
+ if (gfe->priv->assistance_file_label)
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->assistance_file_label), enabled);
+ if (gfe->priv->assistance_password_label)
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->assistance_password_label), enabled);
+ }
+}
+
+#ifdef HAVE_LIBSSH
+
+static void remmina_file_editor_ssh_tunnel_server_custom_radio_on_toggled(GtkToggleButton *togglebutton, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->ssh_tunnel_server_entry),
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gfe->priv->ssh_tunnel_enabled_check)) &&
+ (gfe->priv->ssh_tunnel_server_custom_radio == NULL ||
+ gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gfe->priv->ssh_tunnel_server_custom_radio))));
+}
+
+
+static void remmina_file_editor_ssh_tunnel_enabled_check_on_toggled(GtkToggleButton *togglebutton,
+ RemminaFileEditor *gfe, RemminaProtocolSSHSetting ssh_setting)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv = gfe->priv;
+ gboolean enabled = TRUE;
+ gchar *p;
+ const gchar *cp;
+ const gchar *s = NULL;
+
+ if (gfe->priv->ssh_tunnel_enabled_check) {
+ enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gfe->priv->ssh_tunnel_enabled_check));
+ if (gfe->priv->ssh_tunnel_loopback_check)
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->ssh_tunnel_loopback_check), enabled);
+ if (gfe->priv->ssh_tunnel_server_default_radio)
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->ssh_tunnel_server_default_radio), enabled);
+ if (gfe->priv->ssh_tunnel_server_custom_radio)
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->ssh_tunnel_server_custom_radio), enabled);
+ remmina_file_editor_ssh_tunnel_server_custom_radio_on_toggled(NULL, gfe);
+ p = remmina_public_combo_get_active_text(GTK_COMBO_BOX(priv->protocol_combo));
+ // if (!(g_strcmp0(p, "SFTP") == 0 || g_strcmp0(p, "SSH") == 0)) {
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->ssh_tunnel_username_entry), enabled);
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->ssh_tunnel_auth_password), enabled);
+ gtk_widget_set_sensitive(GTK_WIDGET(gfe->priv->ssh_tunnel_auth_combo), enabled);
+ //}
+ g_free(p);
+ }
+ // remmina_file_editor_ssh_tunnel_auth_publickey_radio_on_toggled(NULL, gfe);
+ s = remmina_file_get_string(gfe->priv->remmina_file, "ssh_tunnel_privatekey");
+ if (s)
+ gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(gfe->priv->ssh_tunnel_privatekey_chooser), s);
+ s = remmina_file_get_string(gfe->priv->remmina_file, "ssh_tunnel_certfile");
+ if (s)
+ gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(gfe->priv->ssh_tunnel_certfile_chooser), s);
+
+ if (gfe->priv->ssh_tunnel_username_entry)
+ if (enabled && gtk_entry_get_text(GTK_ENTRY(gfe->priv->ssh_tunnel_username_entry))[0] == '\0') {
+ cp = remmina_file_get_string(priv->remmina_file, "ssh_tunnel_username");
+ gtk_entry_set_text(GTK_ENTRY(gfe->priv->ssh_tunnel_username_entry), cp ? cp : "");
+ }
+
+ if (gfe->priv->ssh_tunnel_auth_password) {
+ if (enabled && gtk_entry_get_text(GTK_ENTRY(gfe->priv->ssh_tunnel_auth_password))[0] == '\0') {
+ cp = remmina_file_get_string(priv->remmina_file, "ssh_tunnel_password");
+ gtk_entry_set_text(GTK_ENTRY(gfe->priv->ssh_tunnel_auth_password), cp ? cp : "");
+ }
+ }
+ if (gfe->priv->ssh_tunnel_passphrase) {
+ if (enabled && gtk_entry_get_text(GTK_ENTRY(gfe->priv->ssh_tunnel_passphrase))[0] == '\0') {
+ cp = remmina_file_get_string(priv->remmina_file, "ssh_tunnel_passphrase");
+ gtk_entry_set_text(GTK_ENTRY(gfe->priv->ssh_tunnel_passphrase), cp ? cp : "");
+ }
+ }
+}
+
+#endif
+
+static void remmina_file_editor_create_server(RemminaFileEditor *gfe, const RemminaProtocolSetting *setting, GtkWidget *grid,
+ gint row)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolPlugin *plugin = gfe->priv->plugin;
+ GtkWidget *widget;
+#ifdef HAVE_LIBAVAHI_UI
+ GtkWidget *hbox;
+#endif
+ gchar *s;
+
+ widget = gtk_label_new(_("Server"));
+ gtk_widget_show(widget);
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, row + 1);
+
+ s = remmina_pref_get_recent(plugin->name);
+ widget = remmina_public_create_combo_entry(s, remmina_file_get_string(gfe->priv->remmina_file, "server"), TRUE);
+ gtk_widget_set_hexpand(widget, TRUE);
+ gtk_widget_show(widget);
+ gtk_widget_set_tooltip_markup(widget, _(server_tips));
+ gtk_entry_set_activates_default(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(widget))), TRUE);
+ gfe->priv->server_combo = widget;
+ g_free(s);
+
+#ifdef HAVE_LIBAVAHI_UI
+ if (setting->opt1) {
+ gfe->priv->avahi_service_type = (const gchar *)setting->opt1;
+
+ hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show(hbox);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0);
+
+ widget = gtk_button_new_with_label("…");
+ s = g_strdup_printf(_("Browse the network to find a %s server"), plugin->name);
+ gtk_widget_set_tooltip_text(widget, s);
+ g_free(s);
+ gtk_widget_show(widget);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(remmina_file_editor_browse_avahi), gfe);
+
+ gtk_grid_attach(GTK_GRID(grid), hbox, 1, row, 1, 1);
+ } else
+#endif
+ {
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, row, 1, 1);
+ }
+}
+
+
+static GtkWidget *remmina_file_editor_create_password(RemminaFileEditor *gfe, GtkWidget *grid, gint row, gint col, const gchar *label, const gchar *value, gchar *setting_name)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+
+ widget = gtk_label_new(label);
+ gtk_widget_show(widget);
+#if GTK_CHECK_VERSION(3, 12, 0)
+ gtk_widget_set_margin_end(widget, 40);
+#else
+ gtk_widget_set_margin_right(widget, 40);
+#endif
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 1);
+
+ widget = gtk_entry_new();
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, row, 1, 1);
+ gtk_entry_set_max_length(GTK_ENTRY(widget), 0);
+ gtk_entry_set_visibility(GTK_ENTRY(widget), FALSE);
+ gtk_widget_set_hexpand(widget, TRUE);
+ gtk_entry_set_activates_default(GTK_ENTRY(widget), TRUE);
+ if (setting_name)
+ gtk_widget_set_name(widget, setting_name);
+
+ if (value)
+ gtk_entry_set_text(GTK_ENTRY(widget), value);
+ /* Password view Toogle*/
+ if (setting_name) {
+ gtk_entry_set_icon_from_icon_name(GTK_ENTRY(widget), GTK_ENTRY_ICON_SECONDARY, "org.remmina.Remmina-password-reveal-symbolic");
+ gtk_entry_set_icon_activatable(GTK_ENTRY(widget), GTK_ENTRY_ICON_SECONDARY, TRUE);
+ g_signal_connect(widget, "icon-press", G_CALLBACK(remmina_main_toggle_password_view), NULL);
+ }
+ return widget;
+}
+
+static void remmina_file_editor_update_resolution(GtkWidget *widget, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ gchar *res_str;
+ res_str = g_strdup_printf("%dx%d",
+ remmina_file_get_int(gfe->priv->remmina_file, "resolution_width", 0),
+ remmina_file_get_int(gfe->priv->remmina_file, "resolution_height", 0));
+ remmina_public_load_combo_text_d(gfe->priv->resolution_custom_combo, remmina_pref.resolutions,
+ res_str, NULL);
+ g_free(res_str);
+}
+
+static void remmina_file_editor_browse_resolution(GtkWidget *button, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+
+ GtkDialog *dialog = remmina_string_list_new(FALSE, NULL);
+ remmina_string_list_set_validation_func(remmina_public_resolution_validation_func);
+ remmina_string_list_set_text(remmina_pref.resolutions, TRUE);
+ remmina_string_list_set_titles(_("Resolutions"), _("Configure the available resolutions"));
+ gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(gfe));
+ gtk_dialog_run(dialog);
+ g_free(remmina_pref.resolutions);
+ remmina_pref.resolutions = remmina_string_list_get_text();
+ g_signal_connect(G_OBJECT(dialog), "destroy", G_CALLBACK(remmina_file_editor_update_resolution), gfe);
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+}
+
+static void remmina_file_editor_create_resolution(RemminaFileEditor *gfe, const RemminaProtocolSetting *setting,
+ GtkWidget *grid, gint row)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+ GtkWidget *hbox;
+ int resolution_w, resolution_h;
+ gchar *res_str;
+ RemminaProtocolWidgetResolutionMode res_mode;
+
+ res_mode = remmina_file_get_int(gfe->priv->remmina_file, "resolution_mode", RES_INVALID);
+ resolution_w = remmina_file_get_int(gfe->priv->remmina_file, "resolution_width", -1);
+ resolution_h = remmina_file_get_int(gfe->priv->remmina_file, "resolution_height", -1);
+
+ /* If resolution_mode is non-existent (-1), then we try to calculate it
+ * as we did before having resolution_mode */
+ if (res_mode == RES_INVALID) {
+ if (resolution_w <= 0 || resolution_h <= 0)
+ res_mode = RES_USE_INITIAL_WINDOW_SIZE;
+ else
+ res_mode = RES_USE_CUSTOM;
+ }
+ if (res_mode == RES_USE_CUSTOM)
+ res_str = g_strdup_printf("%dx%d", resolution_w, resolution_h);
+ else
+ res_str = NULL;
+
+ widget = gtk_label_new(_("Resolution"));
+ gtk_widget_show(widget);
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 1);
+
+ hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ widget = gtk_radio_button_new_with_label(NULL, _("Use initial window size"));
+ gtk_widget_show(widget);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0);
+ gfe->priv->resolution_iws_radio = widget;
+ widget = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(gfe->priv->resolution_iws_radio), _("Use client resolution"));
+ gtk_widget_show(widget);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0);
+ gfe->priv->resolution_auto_radio = widget;
+ gtk_grid_attach(GTK_GRID(grid), hbox, 1, row, 1, 1);
+ gtk_widget_show(hbox);
+
+ hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show(hbox);
+ gtk_grid_attach(GTK_GRID(grid), hbox, 1, row + 1, 1, 1);
+
+ widget = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(gfe->priv->resolution_iws_radio), _("Custom"));
+ gtk_widget_show(widget);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, FALSE, 0);
+ gfe->priv->resolution_custom_radio = widget;
+
+ widget = remmina_public_create_combo_text_d(remmina_pref.resolutions, res_str, NULL);
+ gtk_widget_show(widget);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0);
+ gfe->priv->resolution_custom_combo = widget;
+
+ widget = gtk_button_new_with_label("…");
+ gtk_widget_show(widget);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(remmina_file_editor_browse_resolution), gfe);
+
+ g_signal_connect(G_OBJECT(gfe->priv->resolution_custom_radio), "toggled",
+ G_CALLBACK(remmina_file_editor_button_on_toggled), gfe->priv->resolution_custom_combo);
+
+ if (res_mode == RES_USE_CUSTOM)
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gfe->priv->resolution_custom_radio), TRUE);
+ else if (res_mode == RES_USE_CLIENT)
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gfe->priv->resolution_auto_radio), TRUE);
+ else
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gfe->priv->resolution_iws_radio), TRUE);
+
+ gtk_widget_set_sensitive(gfe->priv->resolution_custom_combo, gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gfe->priv->resolution_custom_radio)));
+
+ g_free(res_str);
+}
+
+
+static void remmina_file_editor_create_assistance(RemminaFileEditor *gfe, const RemminaProtocolSetting *setting,
+ GtkWidget *grid, gint row)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+
+
+
+ widget = gtk_toggle_button_new_with_label(_("Assistance Mode"));
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 1);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), remmina_file_get_int(gfe->priv->remmina_file, "assistance_mode", 0));
+ gfe->priv->assistance_toggle = widget;
+ g_signal_connect(widget, "toggled", G_CALLBACK(remmina_file_editor_assistance_enabled_check_on_toggled), gfe);
+
+
+ widget = gtk_label_new("Assistance file");
+ gtk_widget_set_halign(widget, GTK_ALIGN_END);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row+1, 1, 1);
+ gfe->priv->assistance_file_label = widget;
+
+ widget = gtk_entry_new();
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_widget_show(widget);
+
+ if (remmina_file_get_string(gfe->priv->remmina_file, "assistance_file") != NULL) {
+ gtk_entry_set_text(GTK_ENTRY(widget), remmina_file_get_string(gfe->priv->remmina_file, "assistance_file"));
+ }
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, row+1, 1, 1);
+ gfe->priv->assistance_file = widget;
+
+ widget = gtk_label_new("Assistance Password");
+ gtk_widget_set_halign(widget, GTK_ALIGN_END);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row+2, 1, 1);
+ gfe->priv->assistance_password_label = widget;
+
+ widget = gtk_entry_new();
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_widget_show(widget);
+
+ if (remmina_file_get_string(gfe->priv->remmina_file, "assistance_pass") != NULL) {
+ gtk_entry_set_text(GTK_ENTRY(widget), remmina_file_get_string(gfe->priv->remmina_file, "assistance_pass"));
+ }
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, row+2, 1, 1);
+ gfe->priv->assistance_password = widget;
+
+ remmina_file_editor_assistance_enabled_check_on_toggled(NULL, gfe);
+
+}
+
+
+static GtkWidget *remmina_file_editor_create_text2(RemminaFileEditor *gfe, GtkWidget *grid,
+ gint row, gint col, const gchar *label, const gchar *value, gint left,
+ gint right, gchar *setting_name)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+
+ widget = gtk_label_new(label);
+ gtk_widget_show(widget);
+#if GTK_CHECK_VERSION(3, 12, 0)
+ gtk_widget_set_margin_start(widget, left);
+ gtk_widget_set_margin_end(widget, right);
+#else
+ gtk_widget_set_margin_left(widget, left);
+ gtk_widget_set_margin_right(widget, right);
+#endif
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, col, row, 1, 1);
+
+ widget = gtk_entry_new();
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, col + 1, row, 1, 1);
+ gtk_entry_set_max_length(GTK_ENTRY(widget), 300);
+ gtk_widget_set_hexpand(widget, TRUE);
+ if (setting_name)
+ gtk_widget_set_name(widget, setting_name);
+
+ if (value)
+ gtk_entry_set_text(GTK_ENTRY(widget), value);
+
+ return widget;
+}
+
+static GtkWidget *remmina_file_editor_create_text(RemminaFileEditor *gfe, GtkWidget *grid,
+ gint row, gint col, const gchar *label, const gchar *value,
+ gchar *setting_name)
+{
+ TRACE_CALL(__func__);
+ return remmina_file_editor_create_text2(gfe, grid, row, col, label, value, 0, 40,
+ setting_name);
+}
+
+static GtkWidget *remmina_file_editor_create_textarea(RemminaFileEditor *gfe, GtkWidget *grid,
+ gint row, gint col, const gchar *label, const gchar *value,
+ gchar *setting_name)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+ GtkTextView *view;
+ GtkTextBuffer *buffer;
+ GtkTextIter start;
+
+ widget = gtk_text_view_new();
+ view = GTK_TEXT_VIEW(widget);
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(view), GTK_WRAP_WORD);
+ gtk_text_view_set_top_margin(GTK_TEXT_VIEW(view), 20);
+ gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(view), 20);
+ gtk_text_view_set_left_margin(GTK_TEXT_VIEW(view), 20);
+ gtk_text_view_set_right_margin(GTK_TEXT_VIEW(view), 20);
+ gtk_text_view_set_monospace(view, TRUE);
+ if (setting_name)
+ gtk_widget_set_name(widget, setting_name);
+ if (value) {
+ buffer = gtk_text_view_get_buffer(view);
+ gtk_text_buffer_set_text(buffer, value, -1);
+ gtk_text_buffer_get_start_iter(buffer, &start);
+ gtk_text_buffer_place_cursor(buffer, &start);
+ }
+ gtk_widget_show(widget);
+ gtk_widget_set_hexpand(widget, TRUE);
+ gtk_widget_set_size_request(GTK_WIDGET(view), 320, 300);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 1);
+ return widget;
+}
+
+static GtkWidget *remmina_file_editor_create_select(RemminaFileEditor *gfe, GtkWidget *grid,
+ gint row, gint col, const gchar *label, const gpointer *list,
+ const gchar *value, gchar *setting_name)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+
+ widget = gtk_label_new(label);
+ gtk_widget_show(widget);
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ if (setting_name)
+ gtk_widget_set_name(widget, setting_name);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 1);
+
+ widget = remmina_public_create_combo_map(list, value, FALSE, gfe->priv->plugin->domain);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, row, 1, 1);
+
+ return widget;
+}
+
+static GtkWidget *remmina_file_editor_create_combo(RemminaFileEditor *gfe, GtkWidget *grid,
+ gint row, gint col, const gchar *label, const gchar *list,
+ const gchar *value, gchar *setting_name)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+
+ widget = gtk_label_new(label);
+ gtk_widget_show(widget);
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 1);
+
+ widget = remmina_public_create_combo_entry(list, value, FALSE);
+ gtk_widget_show(widget);
+ gtk_widget_set_hexpand(widget, TRUE);
+ if (setting_name)
+ gtk_widget_set_name(widget, setting_name);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, row, 1, 1);
+
+ return widget;
+}
+
+static GtkWidget *remmina_file_editor_create_check(RemminaFileEditor *gfe, GtkWidget *grid,
+ gint row, gint top, const gchar *label, gboolean value,
+ gchar *setting_name)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+ widget = gtk_check_button_new_with_label(label);
+ gtk_widget_show(widget);
+ if (setting_name)
+ gtk_widget_set_name(widget, setting_name);
+ gtk_grid_attach(GTK_GRID(grid), widget, top, row, 1, 1);
+
+ if (value)
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), TRUE);
+
+ return widget;
+}
+
+/**
+ * Create checkbox + gtk_file_chooser for open files and select folders
+ *
+ * The code is wrong, because if the checkbox is not active, the value should be set to NULL
+ * and remove it from the remmina file. The problem is that this function knows nothing about
+ * the remmina file.
+ * This should be rewritten in a more generic way
+ * Please use REMMINA_PROTOCOL_SETTING_TYPE_TEXT
+ */
+static GtkWidget *
+remmina_file_editor_create_chooser(RemminaFileEditor *gfe, GtkWidget *grid, gint row, gint col, const gchar *label,
+ const gchar *value, gint type, gchar *setting_name)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *check;
+ GtkWidget *widget;
+ GtkWidget *hbox;
+
+ widget = gtk_label_new(label);
+ gtk_widget_show(widget);
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 1);
+
+ hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show(hbox);
+ gtk_grid_attach(GTK_GRID(grid), hbox, 1, row, 1, 1);
+
+ check = gtk_check_button_new();
+ gtk_widget_show(check);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), (value && value[0] == '/'));
+ gtk_box_pack_start(GTK_BOX(hbox), check, FALSE, FALSE, 0);
+
+ widget = gtk_file_chooser_button_new(label, type);
+ if (setting_name)
+ gtk_widget_set_name(widget, setting_name);
+ gtk_widget_show(widget);
+ if (value)
+ gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(widget), value);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0);
+
+ g_signal_connect(G_OBJECT(check), "toggled", G_CALLBACK(remmina_file_editor_button_on_toggled), widget);
+ remmina_file_editor_button_on_toggled(GTK_TOGGLE_BUTTON(check), widget);
+
+ return widget;
+}
+
+// used to filter out invalid characters for REMMINA_PROTOCOL_SETTING_TYPE_INT
+void remmina_file_editor_int_setting_filter(GtkEditable *editable, const gchar *text,
+ gint length, gint *position, gpointer data)
+{
+ for (int i = 0; i < length; i++) {
+ if (!isdigit(text[i]) && text[i] != '-') {
+ g_signal_stop_emission_by_name(G_OBJECT(editable), "insert-text");
+ return;
+ }
+ }
+}
+
+// used to filter out invalid characters for REMMINA_PROTOCOL_SETTING_TYPE_DOUBLE
+// '.' and ',' can't be used interchangeably! It depends on the language setting
+// of the user.
+void remmina_file_editor_double_setting_filter(GtkEditable *editable, const gchar *text,
+ gint length, gint *position, gpointer data)
+{
+ for (int i = 0; i < length; i++) {
+ if (!isdigit(text[i]) && text[i] != '-' && text[i] != '.' && text[i] != ',') {
+ g_signal_stop_emission_by_name(G_OBJECT(editable), "insert-text");
+ return;
+ }
+ }
+}
+
+static GtkWidget *remmina_file_editor_create_int(RemminaFileEditor *gfe, GtkWidget *grid,
+ gint row, gint col, const gchar *label, const gint value,
+ gint left, gint right, gchar *setting_name)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+
+ widget = gtk_label_new(label);
+ gtk_widget_show(widget);
+#if GTK_CHECK_VERSION(3, 12, 0)
+ gtk_widget_set_margin_start(widget, left);
+ gtk_widget_set_margin_end(widget, right);
+#else
+ gtk_widget_set_margin_left(widget, left);
+ gtk_widget_set_margin_right(widget, right);
+#endif
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, col, row, 1, 1);
+
+ widget = gtk_entry_new();
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, col + 1, row, 1, 1);
+ gtk_entry_set_max_length(GTK_ENTRY(widget), 300);
+ gtk_widget_set_hexpand(widget, TRUE);
+ if (setting_name)
+ gtk_widget_set_name(widget, setting_name);
+
+ // Convert int to str.
+ int length = snprintf(NULL, 0, "%d", value) + 1; // +1 '\0' byte
+ char *str = malloc(length);
+ snprintf(str, length, "%d", value);
+
+ gtk_entry_set_text(GTK_ENTRY(widget), str);
+ free(str);
+
+ g_signal_connect(G_OBJECT(widget), "insert-text",
+ G_CALLBACK(remmina_file_editor_int_setting_filter), NULL);
+
+ return widget;
+}
+
+static GtkWidget *remmina_file_editor_create_double(RemminaFileEditor *gfe,
+ GtkWidget *grid, gint row, gint col,
+ const gchar *label, gdouble value, gint left,
+ gint right, gchar *setting_name)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+
+ widget = gtk_label_new(label);
+ gtk_widget_show(widget);
+#if GTK_CHECK_VERSION(3, 12, 0)
+ gtk_widget_set_margin_start(widget, left);
+ gtk_widget_set_margin_end(widget, right);
+#else
+ gtk_widget_set_margin_left(widget, left);
+ gtk_widget_set_margin_right(widget, right);
+#endif
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, col, row, 1, 1);
+
+ widget = gtk_entry_new();
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, col + 1, row, 1, 1);
+ gtk_entry_set_max_length(GTK_ENTRY(widget), 300);
+ gtk_widget_set_hexpand(widget, TRUE);
+ if (setting_name)
+ gtk_widget_set_name(widget, setting_name);
+
+ // Convert double to str.
+ int length = snprintf(NULL, 0, "%.8g", value) + 1; // +1 '\0' byte
+ char *str = malloc(length);
+ snprintf(str, length, "%f", value);
+
+ gtk_entry_set_text(GTK_ENTRY(widget), str);
+ free(str);
+
+ g_signal_connect(G_OBJECT(widget), "insert-text",
+ G_CALLBACK(remmina_file_editor_double_setting_filter), NULL);
+
+ return widget;
+}
+
+
+
+static void remmina_file_editor_create_settings(RemminaFileEditor *gfe, GtkWidget *grid,
+ const RemminaProtocolSetting *settings)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv = gfe->priv;
+ GtkWidget *widget;
+ gint grid_row = 0;
+ gint grid_column = 0;
+ gchar **strarr;
+ gchar *setting_name;
+ const gchar *escaped;
+
+ while (settings->type != REMMINA_PROTOCOL_SETTING_TYPE_END) {
+ setting_name = (gchar *)(remmina_plugin_manager_get_canonical_setting_name(settings));
+ switch (settings->type) {
+ case REMMINA_PROTOCOL_SETTING_TYPE_SERVER:
+ remmina_file_editor_create_server(gfe, settings, grid, grid_row);
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD:
+ widget = remmina_file_editor_create_password(gfe, grid, grid_row, 0,
+ g_dgettext(priv->plugin->domain, settings->label),
+ remmina_file_get_string(priv->remmina_file, setting_name),
+ setting_name);
+ g_hash_table_insert(priv->setting_widgets, setting_name, widget);
+ grid_row++;
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION:
+ remmina_file_editor_create_resolution(gfe, settings, grid, grid_row);
+ grid_row ++;
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_ASSISTANCE:
+ remmina_file_editor_create_assistance(gfe, settings, grid, grid_row);
+ grid_row += 3;
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_KEYMAP:
+ strarr = remmina_pref_keymap_groups();
+ priv->keymap_combo = remmina_file_editor_create_select(gfe, grid,
+ grid_row + 1, 0,
+ _("Keyboard mapping"), (const gpointer *)strarr,
+ remmina_file_get_string(priv->remmina_file, "keymap"),
+ setting_name);
+ g_strfreev(strarr);
+ grid_row++;
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_TEXT:
+ widget = remmina_file_editor_create_text(gfe, grid, grid_row, 0,
+ g_dgettext(priv->plugin->domain, settings->label),
+ remmina_file_get_string(priv->remmina_file, setting_name),
+ setting_name);
+ g_hash_table_insert(priv->setting_widgets, setting_name, widget);
+ if (settings->opt2)
+ gtk_widget_set_tooltip_text(widget, _((const gchar *)settings->opt2));
+ grid_row++;
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_TEXTAREA:
+ escaped = remmina_file_get_string(priv->remmina_file, setting_name);
+ escaped = g_uri_unescape_string(escaped, NULL);
+ widget = remmina_file_editor_create_textarea(gfe, grid, grid_row, 0,
+ g_dgettext(priv->plugin->domain, settings->label), escaped,
+ setting_name);
+ g_hash_table_insert(priv->setting_widgets, setting_name, widget);
+ grid_row++;
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_SELECT:
+ widget = remmina_file_editor_create_select(gfe, grid, grid_row, 0,
+ g_dgettext(priv->plugin->domain, settings->label),
+ (const gpointer *)settings->opt1,
+ remmina_file_get_string(priv->remmina_file, setting_name),
+ setting_name);
+ g_hash_table_insert(priv->setting_widgets, setting_name, widget);
+ if (settings->opt2)
+ gtk_widget_set_tooltip_text(widget, _((const gchar *)settings->opt2));
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_COMBO:
+ widget = remmina_file_editor_create_combo(gfe, grid, grid_row, 0,
+ g_dgettext(priv->plugin->domain, settings->label),
+ (const gchar *)settings->opt1,
+ remmina_file_get_string(priv->remmina_file, setting_name),
+ setting_name);
+ g_hash_table_insert(priv->setting_widgets, setting_name, widget);
+ if (settings->opt2)
+ gtk_widget_set_tooltip_text(widget, _((const gchar *)settings->opt2));
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_CHECK:
+ widget = remmina_file_editor_create_check(gfe, grid, grid_row, grid_column,
+ g_dgettext(priv->plugin->domain, settings->label),
+ remmina_file_get_int(priv->remmina_file, setting_name, FALSE),
+ setting_name);
+ g_hash_table_insert(priv->setting_widgets, setting_name, widget);
+ if (settings->opt2)
+ gtk_widget_set_tooltip_text(widget, _((const gchar *)settings->opt2));
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_FILE:
+ widget = remmina_file_editor_create_chooser(gfe, grid, grid_row, 0,
+ g_dgettext(priv->plugin->domain, settings->label),
+ remmina_file_get_string(priv->remmina_file, setting_name),
+ GTK_FILE_CHOOSER_ACTION_OPEN, setting_name);
+ g_hash_table_insert(priv->setting_widgets, setting_name, widget);
+ if (settings->opt2)
+ gtk_widget_set_tooltip_text(widget, _((const gchar *)settings->opt2));
+ break;
+
+ case REMMINA_PROTOCOL_SETTING_TYPE_FOLDER:
+ widget = remmina_file_editor_create_chooser(gfe, grid, grid_row, 0,
+ g_dgettext(priv->plugin->domain, settings->label),
+ remmina_file_get_string(priv->remmina_file, setting_name),
+ GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
+ setting_name);
+ g_hash_table_insert(priv->setting_widgets, setting_name, widget);
+ if (settings->opt2)
+ gtk_widget_set_tooltip_text(widget, _((const gchar *)settings->opt2));
+ break;
+ case REMMINA_PROTOCOL_SETTING_TYPE_INT:
+ widget = remmina_file_editor_create_int(gfe, grid, grid_row, 0,
+ g_dgettext(priv->plugin->domain, settings->label),
+ remmina_file_get_int(priv->remmina_file, setting_name, 0),
+ 0, 40, setting_name);
+ g_hash_table_insert(priv->setting_widgets, setting_name, widget);
+ if (settings->opt2)
+ gtk_widget_set_tooltip_text(widget, _((const gchar *)settings->opt2));
+ grid_row++;
+ break;
+ case REMMINA_PROTOCOL_SETTING_TYPE_DOUBLE:
+ widget = remmina_file_editor_create_double(gfe, grid, grid_row, 0,
+ g_dgettext(priv->plugin->domain, settings->label),
+ remmina_file_get_double(priv->remmina_file, setting_name, 0.0f),
+ 0, 40, setting_name);
+ g_hash_table_insert(priv->setting_widgets, setting_name, widget);
+ if (settings->opt2)
+ gtk_widget_set_tooltip_text(widget, _((const gchar *)settings->opt2));
+ grid_row++;
+ break;
+
+ default:
+ break;
+ }
+ /* If the setting wants compactness, move to the next column */
+ if (settings->compact)
+ grid_column++;
+ /* Add a new settings row and move to the first column
+ * if the setting doesn’t want the compactness
+ * or we already have two columns */
+ if (!settings->compact || grid_column > 1) {
+ grid_row++;
+ grid_column = 0;
+ }
+ settings++;
+ }
+}
+
+static void remmina_file_editor_create_behavior_tab(RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv = gfe->priv;
+ GtkWidget *grid;
+ GtkWidget *widget;
+ const gchar *cs;
+
+ /* The Behavior tab (implementation) */
+ grid = remmina_file_editor_create_notebook_tab(gfe, NULL, _("Behavior"), 20, 2);
+
+ /* Execute Command frame */
+ remmina_public_create_group(GTK_GRID(grid), _("Execute a Command"), 0, 1, 2);
+
+ /* PRE connection command */
+ cs = remmina_file_get_string(priv->remmina_file, "precommand");
+ widget = remmina_file_editor_create_text2(gfe, grid, 2, 0, _("Before connecting"), cs, 24, 26, "precommand");
+ priv->behavior_precommand_entry = widget;
+ gtk_entry_set_placeholder_text(GTK_ENTRY(widget), _("command %h %u %t %U %p %g --option"));
+ gtk_widget_set_tooltip_markup(widget, _(cmd_tips));
+
+ /* POST connection command */
+ cs = remmina_file_get_string(priv->remmina_file, "postcommand");
+ widget = remmina_file_editor_create_text2(gfe, grid, 3, 0, _("After connecting"), cs, 24, 16, "postcommand");
+ priv->behavior_postcommand_entry = widget;
+ gtk_entry_set_placeholder_text(GTK_ENTRY(widget), _("/path/to/command -opt1 arg %h %u %t -opt2 %U %p %g"));
+ gtk_widget_set_tooltip_markup(widget, _(cmd_tips));
+
+ /* Startup frame */
+ remmina_public_create_group(GTK_GRID(grid), _("Start-up"), 4, 1, 2);
+
+ /* Autostart profile option */
+ priv->behavior_autostart_check = remmina_file_editor_create_check(gfe, grid, 6, 1, _("Auto-start this profile"),
+ remmina_file_get_int(priv->remmina_file, "enable-autostart", FALSE), "enable-autostart");
+
+ /* Startup frame */
+ remmina_public_create_group(GTK_GRID(grid), _("Connection profile security"), 8, 1, 2);
+
+ /* Autostart profile option */
+ priv->behavior_lock_check = remmina_file_editor_create_check(gfe, grid, 10, 1, _("Require password to connect or edit the profile"),
+ remmina_file_get_int(priv->remmina_file, "profile-lock", FALSE), "profile-lock");
+
+ /* Startup frame */
+ remmina_public_create_group(GTK_GRID(grid), _("Unexpected disconnect"), 12, 1, 2);
+
+ /* Autostart profile option */
+ priv->behavior_disconnect = remmina_file_editor_create_check(gfe, grid, 16, 1, _("Keep window from closing if not disconnected by Remmina"),
+ remmina_file_get_int(priv->remmina_file, "disconnect-prompt", FALSE), "disconnect-prompt");
+}
+
+#ifdef HAVE_LIBSSH
+static gpointer ssh_tunnel_auth_list[] =
+{
+ "0", N_("Password"),
+ "1", N_("SSH identity file"),
+ "2", N_("SSH agent"),
+ "3", N_("Public key (automatic)"),
+ "4", N_("Kerberos (GSSAPI)"),
+ NULL
+};
+#endif
+
+static void remmina_file_editor_create_ssh_tunnel_tab(RemminaFileEditor *gfe, RemminaProtocolSSHSetting ssh_setting)
+{
+ TRACE_CALL(__func__);
+#ifdef HAVE_LIBSSH
+ RemminaFileEditorPriv *priv = gfe->priv;
+ GtkWidget *grid;
+ GtkWidget *widget;
+ const gchar *cs;
+ gchar *s;
+ gchar *p;
+ gint row = 0;
+
+ if (ssh_setting == REMMINA_PROTOCOL_SSH_SETTING_NONE)
+ return;
+
+ /* The SSH tab (implementation) */
+ grid = remmina_file_editor_create_notebook_tab(gfe, NULL,
+ _("SSH Tunnel"), 9, 3);
+ widget = gtk_toggle_button_new_with_label(_("Enable SSH tunnel"));
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "toggled",
+ G_CALLBACK(remmina_file_editor_ssh_tunnel_enabled_check_on_toggled), gfe);
+ priv->ssh_tunnel_enabled_check = widget;
+
+ widget = gtk_check_button_new_with_label(_("Tunnel via loopback address"));
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, row, 2, 1);
+ priv->ssh_tunnel_loopback_check = widget;
+
+ // 1
+ row++;
+ /* SSH Server group */
+
+ switch (ssh_setting) {
+ case REMMINA_PROTOCOL_SSH_SETTING_TUNNEL:
+ s = g_strdup_printf(_("Same server at port %i"), DEFAULT_SSH_PORT);
+ widget = gtk_radio_button_new_with_label(NULL, s);
+ g_free(s);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 3, 1);
+ priv->ssh_tunnel_server_default_radio = widget;
+ // 2
+ row++;
+
+ widget = gtk_radio_button_new_with_label_from_widget(
+ GTK_RADIO_BUTTON(priv->ssh_tunnel_server_default_radio), _("Custom"));
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 1);
+ g_signal_connect(G_OBJECT(widget), "toggled",
+ G_CALLBACK(remmina_file_editor_ssh_tunnel_server_custom_radio_on_toggled), gfe);
+ priv->ssh_tunnel_server_custom_radio = widget;
+
+ widget = gtk_entry_new();
+ gtk_entry_set_max_length(GTK_ENTRY(widget), 100);
+ gtk_widget_set_tooltip_markup(widget, _(server_tips2));
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, row, 2, 1);
+ priv->ssh_tunnel_server_entry = widget;
+ // 3
+ row++;
+ break;
+
+ case REMMINA_PROTOCOL_SSH_SETTING_REVERSE_TUNNEL:
+ priv->ssh_tunnel_server_default_radio = NULL;
+ priv->ssh_tunnel_server_custom_radio = NULL;
+
+ priv->ssh_tunnel_server_entry = remmina_file_editor_create_text(gfe, grid, 1, 0,
+ _("Server"), NULL, "ssh_reverse_tunnel_server");
+ gtk_widget_set_tooltip_markup(priv->ssh_tunnel_server_entry, _(server_tips));
+ // 2
+ row++;
+ break;
+ case REMMINA_PROTOCOL_SSH_SETTING_SSH:
+ case REMMINA_PROTOCOL_SSH_SETTING_SFTP:
+ priv->ssh_tunnel_server_default_radio = NULL;
+ priv->ssh_tunnel_server_custom_radio = NULL;
+ priv->ssh_tunnel_server_entry = NULL;
+
+ break;
+
+ default:
+ break;
+ }
+
+ /* This is not used? */
+ p = remmina_public_combo_get_active_text(GTK_COMBO_BOX(priv->protocol_combo));
+ if (ssh_setting == REMMINA_PROTOCOL_SSH_SETTING_SFTP) {
+ widget = remmina_file_editor_create_text(gfe, grid, row, 1,
+ _("Start-up path"), NULL, "start-up-path");
+ cs = remmina_file_get_string(priv->remmina_file, "execpath");
+ gtk_entry_set_text(GTK_ENTRY(widget), cs ? cs : "");
+ g_hash_table_insert(priv->setting_widgets, "execpath", widget);
+ // 2
+ row++;
+ }
+
+ /* SSH Authentication frame */
+ remmina_public_create_group(GTK_GRID(grid), _("SSH Authentication"), row, 6, 1);
+ // 5
+ row += 2;
+
+ priv->ssh_tunnel_auth_combo = remmina_file_editor_create_select(gfe, grid, row, 0,
+ _("Authentication type"),
+ (const gpointer *)ssh_tunnel_auth_list,
+ remmina_file_get_string(priv->remmina_file, "ssh_tunnel_auth"), "ssh_tunnel_auth");
+ row++;
+
+ if (ssh_setting == REMMINA_PROTOCOL_SSH_SETTING_TUNNEL ||
+ ssh_setting == REMMINA_PROTOCOL_SSH_SETTING_REVERSE_TUNNEL) {
+ priv->ssh_tunnel_username_entry =
+ remmina_file_editor_create_text(gfe, grid, row, 0,
+ _("Username"), NULL, "ssh_tunnel_username");
+ // 5
+ row++;
+ }
+
+ widget = remmina_file_editor_create_password(gfe, grid, row, 0,
+ _("Password"),
+ remmina_file_get_string(priv->remmina_file, "ssh_tunnel_password"),
+ "ssh_tunnel_password");
+ priv->ssh_tunnel_auth_password = widget;
+ row++;
+
+ priv->ssh_tunnel_privatekey_chooser = remmina_file_editor_create_chooser(gfe, grid, row, 0,
+ _("SSH private key file"),
+ remmina_file_get_string(priv->remmina_file, "ssh_tunnel_privatekey"),
+ GTK_FILE_CHOOSER_ACTION_OPEN, "ssh_tunnel_privatekey");
+ row++;
+
+ priv->ssh_tunnel_certfile_chooser = remmina_file_editor_create_chooser(gfe, grid, row, 0,
+ _("SSH certificate file"),
+ remmina_file_get_string(priv->remmina_file, "ssh_tunnel_certfile"),
+ GTK_FILE_CHOOSER_ACTION_OPEN, "ssh_tunnel_certfile");
+ row++;
+
+ widget = gtk_label_new(_("Password to unlock private key"));
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 1);
+ widget = gtk_entry_new();
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, row, 2, 1);
+ gtk_entry_set_max_length(GTK_ENTRY(widget), 300);
+ gtk_entry_set_visibility(GTK_ENTRY(widget), FALSE);
+ gtk_widget_set_hexpand(widget, TRUE);
+ priv->ssh_tunnel_passphrase = widget;
+ row++;
+
+ /* Set the values */
+ cs = remmina_file_get_string(priv->remmina_file, "ssh_tunnel_server");
+ if (ssh_setting == REMMINA_PROTOCOL_SSH_SETTING_TUNNEL) {
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->ssh_tunnel_enabled_check),
+ remmina_file_get_int(priv->remmina_file, "ssh_tunnel_enabled", FALSE));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->ssh_tunnel_loopback_check),
+ remmina_file_get_int(priv->remmina_file, "ssh_tunnel_loopback", FALSE));
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cs ? priv->ssh_tunnel_server_custom_radio : priv->ssh_tunnel_server_default_radio), TRUE);
+ gtk_entry_set_text(GTK_ENTRY(priv->ssh_tunnel_server_entry),
+ cs ? cs : "");
+ } else if (ssh_setting == REMMINA_PROTOCOL_SSH_SETTING_REVERSE_TUNNEL) {
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->ssh_tunnel_enabled_check),
+ remmina_file_get_int(priv->remmina_file, "ssh_tunnel_enabled", FALSE));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->ssh_tunnel_loopback_check),
+ remmina_file_get_int(priv->remmina_file, "ssh_tunnel_loopback", FALSE));
+ gtk_entry_set_text(GTK_ENTRY(priv->ssh_tunnel_server_entry),
+ cs ? cs : "");
+ }
+
+ remmina_file_editor_ssh_tunnel_enabled_check_on_toggled(NULL, gfe, ssh_setting);
+ gtk_widget_show_all(grid);
+ g_free(p);
+#endif
+}
+
+static void remmina_file_editor_create_all_settings(RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv = gfe->priv;
+ GtkWidget *grid;
+
+ static const RemminaProtocolSetting notes_settings[] =
+ {
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXTAREA, "notes_text", NULL, FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL }
+ };
+
+ remmina_file_editor_create_notebook_container(gfe);
+
+ /* The Basic tab */
+ if (priv->plugin->basic_settings) {
+ grid = remmina_file_editor_create_notebook_tab(gfe, NULL, _("Basic"), 20, 2);
+ remmina_file_editor_create_settings(gfe, grid, priv->plugin->basic_settings);
+ }
+
+ /* The Advanced tab */
+ if (priv->plugin->advanced_settings) {
+ grid = remmina_file_editor_create_notebook_tab(gfe, NULL, _("Advanced"), 20, 2);
+ remmina_file_editor_create_settings(gfe, grid, priv->plugin->advanced_settings);
+ }
+
+ /* The Behavior tab */
+ remmina_file_editor_create_behavior_tab(gfe);
+
+ /* The SSH tab */
+ remmina_file_editor_create_ssh_tunnel_tab(gfe, priv->plugin->ssh_setting);
+
+ /* Notes tab */
+ grid = remmina_file_editor_create_notebook_tab(gfe, NULL, _("Notes"), 1, 1);
+ remmina_file_editor_create_settings(gfe, grid, notes_settings);
+}
+
+static void remmina_file_editor_protocol_combo_on_changed(GtkComboBox *combo, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv = gfe->priv;
+ gchar *protocol;
+
+ if (priv->config_container) {
+ gtk_widget_destroy(priv->config_container);
+ priv->config_container = NULL;
+ gtk_widget_destroy(priv->config_viewport);
+ priv->config_viewport = NULL;
+ gtk_widget_destroy(priv->config_scrollable);
+ priv->config_scrollable = NULL;
+ }
+
+ priv->server_combo = NULL;
+ priv->resolution_iws_radio = NULL;
+ priv->resolution_auto_radio = NULL;
+ priv->resolution_custom_radio = NULL;
+ priv->resolution_custom_combo = NULL;
+ priv->keymap_combo = NULL;
+
+ priv->ssh_tunnel_enabled_check = NULL;
+ priv->ssh_tunnel_loopback_check = NULL;
+ priv->ssh_tunnel_server_default_radio = NULL;
+ priv->ssh_tunnel_server_custom_radio = NULL;
+ priv->ssh_tunnel_server_entry = NULL;
+ priv->ssh_tunnel_username_entry = NULL;
+ priv->ssh_tunnel_auth_combo = NULL;
+ priv->ssh_tunnel_auth_password = NULL;
+ priv->ssh_tunnel_privatekey_chooser = NULL;
+ priv->ssh_tunnel_certfile_chooser = NULL;
+
+ g_hash_table_remove_all(priv->setting_widgets);
+
+ protocol = remmina_public_combo_get_active_text(combo);
+ if (protocol) {
+ priv->plugin = (RemminaProtocolPlugin *)remmina_plugin_manager_get_plugin(REMMINA_PLUGIN_TYPE_PROTOCOL,
+ protocol);
+ g_free(protocol);
+ remmina_file_editor_create_all_settings(gfe);
+ }
+}
+
+static void remmina_file_editor_save_behavior_tab(RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv = gfe->priv;
+
+ remmina_file_set_string(priv->remmina_file, "precommand", gtk_entry_get_text(GTK_ENTRY(priv->behavior_precommand_entry)));
+ remmina_file_set_string(priv->remmina_file, "postcommand", gtk_entry_get_text(GTK_ENTRY(priv->behavior_postcommand_entry)));
+
+ gboolean autostart_enabled = (priv->behavior_autostart_check ? gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->behavior_autostart_check)) : FALSE);
+ remmina_file_set_int(priv->remmina_file, "enable-autostart", autostart_enabled);
+ gboolean lock_enabled = (priv->behavior_lock_check ? gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->behavior_lock_check)) : FALSE);
+ remmina_file_set_int(priv->remmina_file, "profile-lock", lock_enabled);
+ gboolean disconect_prompt = (priv->behavior_disconnect ? gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->behavior_disconnect)) : FALSE);
+ remmina_file_set_int(priv->remmina_file, "disconnect-prompt", disconect_prompt);
+}
+
+static void remmina_file_editor_save_ssh_tunnel_tab(RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv = gfe->priv;
+ gboolean ssh_tunnel_enabled;
+ int ssh_tunnel_auth;
+
+ ssh_tunnel_enabled = (priv->ssh_tunnel_enabled_check ? gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->ssh_tunnel_enabled_check)) : FALSE);
+ remmina_file_set_int(priv->remmina_file,
+ "ssh_tunnel_loopback",
+ (priv->ssh_tunnel_loopback_check ? gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->ssh_tunnel_loopback_check)) : FALSE));
+ remmina_file_set_int(priv->remmina_file, "ssh_tunnel_enabled", ssh_tunnel_enabled);
+ remmina_file_set_string(priv->remmina_file, "ssh_tunnel_auth",
+ remmina_public_combo_get_active_text(GTK_COMBO_BOX(priv->ssh_tunnel_auth_combo)));
+ remmina_file_set_string(priv->remmina_file, "ssh_tunnel_username",
+ (ssh_tunnel_enabled ? gtk_entry_get_text(GTK_ENTRY(priv->ssh_tunnel_username_entry)) : NULL));
+ remmina_file_set_string(
+ priv->remmina_file,
+ "ssh_tunnel_server",
+ (ssh_tunnel_enabled && priv->ssh_tunnel_server_entry && (priv->ssh_tunnel_server_custom_radio == NULL || gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->ssh_tunnel_server_custom_radio))) ? gtk_entry_get_text(GTK_ENTRY(priv->ssh_tunnel_server_entry)) : NULL));
+
+ ssh_tunnel_auth = gtk_combo_box_get_active(GTK_COMBO_BOX(priv->ssh_tunnel_auth_combo));
+
+ remmina_file_set_int(
+ priv->remmina_file,
+ "ssh_tunnel_auth",
+ ssh_tunnel_auth);
+
+ // If box is unchecked for private key and certfile file choosers,
+ // set the string to NULL in the remmina file
+ if (gtk_widget_get_sensitive(priv->ssh_tunnel_privatekey_chooser)) {
+ remmina_file_set_string(
+ priv->remmina_file,
+ "ssh_tunnel_privatekey",
+ (priv->ssh_tunnel_privatekey_chooser ? gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(priv->ssh_tunnel_privatekey_chooser)) : NULL));
+ }
+ else {
+ remmina_file_set_string(priv->remmina_file, "ssh_tunnel_privatekey", NULL);
+ }
+
+ if (gtk_widget_get_sensitive(priv->ssh_tunnel_certfile_chooser)) {
+ remmina_file_set_string(
+ priv->remmina_file,
+ "ssh_tunnel_certfile",
+ (priv->ssh_tunnel_certfile_chooser ? gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(priv->ssh_tunnel_certfile_chooser)) : NULL));
+ }
+ else {
+ remmina_file_set_string(priv->remmina_file, "ssh_tunnel_certfile", NULL);
+ }
+
+ remmina_file_set_string(
+ priv->remmina_file,
+ "ssh_tunnel_password",
+ (ssh_tunnel_enabled && (ssh_tunnel_auth == SSH_AUTH_PASSWORD)) ? gtk_entry_get_text(GTK_ENTRY(priv->ssh_tunnel_auth_password)) : NULL);
+
+ remmina_file_set_string(
+ priv->remmina_file,
+ "ssh_tunnel_passphrase",
+ (ssh_tunnel_enabled && (ssh_tunnel_auth == SSH_AUTH_PUBLICKEY || ssh_tunnel_auth == SSH_AUTH_AUTO_PUBLICKEY)) ? gtk_entry_get_text(GTK_ENTRY(priv->ssh_tunnel_passphrase)) : NULL);
+}
+
+static gboolean remmina_file_editor_validate_settings(RemminaFileEditor * gfe,
+ gchar * setting_name_to_validate,
+ gconstpointer value,
+ GError ** err)
+{
+ if (!setting_name_to_validate || !value || !gfe) {
+ if (!setting_name_to_validate) {
+ g_critical(_("(%s: %i): Can't validate setting '%s' since 'value' or 'gfe' "
+ "are NULL!"),
+ __func__, __LINE__, setting_name_to_validate);
+ } else {
+ g_critical(_("(%s: %i): Can't validate user input since "
+ "'setting_name_to_validate', 'value' or 'gfe' are NULL!"),
+ __func__, __LINE__);
+ }
+ g_set_error(err, 1, 1, _("Internal error."));
+ return FALSE;
+ }
+
+ if (strcmp(setting_name_to_validate, "notes_text") == 0) {
+ // Not a plugin setting. Bail out early.
+ return TRUE;
+ }
+
+ const RemminaProtocolSetting *setting_iter;
+ RemminaProtocolPlugin *protocol_plugin;
+ RemminaFileEditorPriv *priv = gfe->priv;
+ protocol_plugin = priv->plugin;
+
+ setting_iter = protocol_plugin->basic_settings;
+ if (setting_iter) {
+ // gboolean found = FALSE;
+ while (setting_iter->type != REMMINA_PROTOCOL_SETTING_TYPE_END) {
+ if (setting_iter->name == NULL) {
+ g_error("Internal error: a setting name in protocol plugin %s is "
+ "null. Please fix RemminaProtocolSetting struct content.",
+ protocol_plugin->name);
+ } else if ((gchar *)setting_name_to_validate) {
+ if (strcmp((gchar *)setting_name_to_validate, setting_iter->name) == 0) {
+ // found = TRUE;
+
+ gpointer validator_data = setting_iter->validator_data;
+ GCallback validator = setting_iter->validator;
+
+ // Default behaviour is that everything is valid,
+ // except a validator is given and its returned GError is not NULL.
+ GError *err_ret = NULL;
+
+ g_debug("Checking setting '%s' for validation.", setting_iter->name);
+ if (validator != NULL) {
+ // Looks weird but it calls the setting's validator
+ // function using setting_name_to_validate, value and
+ // validator_data as parameters and it returns a GError*.
+ err_ret = ((GError * (*)(gpointer, gconstpointer, gpointer)) validator)(setting_name_to_validate, value, validator_data);
+ }
+
+ if (err_ret) {
+ g_debug("it has a validator function and it had an error!");
+ // pass err (returned value) to function caller.
+ *err = err_ret;
+ return FALSE;
+ }
+
+ break;
+ }
+ }
+ setting_iter++;
+ }
+
+ // if (!found) {
+ // TOO VERBOSE:
+ // g_warning("%s is not a plugin setting!", setting_name_to_validate);
+ // }
+ }
+
+ return TRUE;
+}
+
+static GError *remmina_file_editor_update_settings(RemminaFileEditor * gfe,
+ GtkWidget ** failed_widget)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv = gfe->priv;
+ GHashTableIter iter;
+ gpointer key;
+ gpointer widget;
+ GtkTextBuffer *buffer;
+ gchar *escaped, *unescaped;
+ GtkTextIter start, end;
+
+ GError *err = NULL;
+ *failed_widget = NULL;
+
+ g_hash_table_iter_init(&iter, priv->setting_widgets);
+ while (g_hash_table_iter_next(&iter, &key, &widget)) {
+
+ // We don't want to save or validate grayed-out settings.
+ // If widget is a file chooser, it was made not sensitive because
+ // the box was unchecked. In that case, don't continue. The
+ // relevant file strings will be set to NULL in the remmina file.
+ if (!gtk_widget_get_sensitive(GTK_WIDGET(widget)) && !GTK_IS_FILE_CHOOSER(widget)) {
+ g_debug("Grayed-out setting-widget '%s' will not be saved.",
+ gtk_widget_get_name(widget));
+ continue;
+ }
+
+ if (GTK_IS_ENTRY(widget)) {
+ const gchar *value = gtk_entry_get_text(GTK_ENTRY(widget));
+
+ if (!remmina_file_editor_validate_settings(gfe, (gchar *)key, value, &err)) {
+ // Error while validating!
+ // err should be set now.
+ *failed_widget = widget;
+ break;
+ }
+
+ remmina_file_set_string(priv->remmina_file, (gchar *)key, value);
+ } else if (GTK_IS_TEXT_VIEW(widget)) {
+ buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
+ gtk_text_buffer_get_start_iter(buffer, &start);
+ gtk_text_buffer_get_end_iter(buffer, &end);
+ unescaped = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
+ escaped = g_uri_escape_string(unescaped, NULL, TRUE);
+
+ if (!remmina_file_editor_validate_settings(gfe, (gchar *)key, escaped, &err)) {
+ // Error while validating!
+ // err should be set now.
+ *failed_widget = widget;
+ break;
+ }
+
+ remmina_file_set_string(priv->remmina_file, (gchar *)key, escaped);
+ g_free(escaped);
+ } else if (GTK_IS_COMBO_BOX(widget)) {
+ gchar *value = remmina_public_combo_get_active_text(GTK_COMBO_BOX(widget));
+
+ if (!remmina_file_editor_validate_settings(gfe, (gchar *)key, value, &err)) {
+ // Error while validating!
+ // err should be set now.
+ *failed_widget = widget;
+ break;
+ }
+
+ remmina_file_set_string(priv->remmina_file, (gchar *)key, value);
+ } else if (GTK_IS_FILE_CHOOSER(widget)) {
+ gchar *value = gtk_widget_get_sensitive(GTK_WIDGET(widget)) ? gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)) : NULL;
+
+ if (!gtk_widget_get_sensitive(GTK_WIDGET(widget))) {
+ remmina_file_set_string(priv->remmina_file, (gchar *)key, value);
+ continue;
+ }
+
+ if (!remmina_file_editor_validate_settings(gfe, (gchar *)key, value, &err)) {
+ // Error while validating!
+ // err should be set now.
+ g_free(value);
+ *failed_widget = widget;
+ break;
+ }
+
+ remmina_file_set_string(priv->remmina_file, (gchar *)key, value);
+ g_free(value);
+ } else if (GTK_IS_TOGGLE_BUTTON(widget)) {
+ gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
+
+ if (!remmina_file_editor_validate_settings(gfe, (gchar *)key, &value, &err)) {
+ // Error while validating!
+ // err should be set now.
+ *failed_widget = widget;
+ break;
+ }
+
+ remmina_file_set_int(priv->remmina_file, (gchar *)key, value);
+ }
+ }
+
+ if (err) {
+ return err;
+ }
+
+ return NULL;
+}
+
+static GError *remmina_file_editor_update(RemminaFileEditor * gfe,
+ GtkWidget ** failed_widget)
+{
+ TRACE_CALL(__func__);
+ int res_w, res_h;
+ gchar *custom_resolution;
+ RemminaProtocolWidgetResolutionMode res_mode;
+
+ RemminaFileEditorPriv *priv = gfe->priv;
+
+ remmina_file_set_string(priv->remmina_file, "name", gtk_entry_get_text(GTK_ENTRY(priv->name_entry)));
+
+ remmina_file_set_string(priv->remmina_file, "labels", gtk_entry_get_text(GTK_ENTRY(priv->labels_entry)));
+
+ remmina_file_set_string(priv->remmina_file, "group",
+ (priv->group_combo ? remmina_public_combo_get_active_text(GTK_COMBO_BOX(priv->group_combo)) : NULL));
+
+ remmina_file_set_string(priv->remmina_file, "protocol",
+ remmina_public_combo_get_active_text(GTK_COMBO_BOX(priv->protocol_combo)));
+
+ remmina_file_set_string(priv->remmina_file, "server",
+ (priv->server_combo ? remmina_public_combo_get_active_text(GTK_COMBO_BOX(priv->server_combo)) : NULL));
+
+ if (priv->resolution_auto_radio) {
+ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->resolution_auto_radio))) {
+ /* Resolution is set to auto (which means: Use client fullscreen resolution, aka use client resolution) */
+ res_w = res_h = 0;
+ res_mode = RES_USE_CLIENT;
+ } else if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->resolution_iws_radio))) {
+ /* Resolution is set to initial window size */
+ res_w = res_h = 0;
+ res_mode = RES_USE_INITIAL_WINDOW_SIZE;
+ } else {
+ /* Resolution is set to a value from the list */
+ custom_resolution = remmina_public_combo_get_active_text(GTK_COMBO_BOX(priv->resolution_custom_combo));
+ if (remmina_public_split_resolution_string(custom_resolution, &res_w, &res_h))
+ res_mode = RES_USE_CUSTOM;
+ else
+ res_mode = RES_USE_INITIAL_WINDOW_SIZE;
+ g_free(custom_resolution);
+ }
+ remmina_file_set_int(priv->remmina_file, "resolution_mode", res_mode);
+ remmina_file_set_int(priv->remmina_file, "resolution_width", res_w);
+ remmina_file_set_int(priv->remmina_file, "resolution_height", res_h);
+ }
+
+ if (priv->assistance_toggle){
+ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->assistance_toggle))) {
+ remmina_file_set_string(priv->remmina_file, "assistance_file", gtk_entry_get_text(GTK_ENTRY(priv->assistance_file)));
+ remmina_file_set_string(priv->remmina_file, "assistance_pass", gtk_entry_get_text(GTK_ENTRY(priv->assistance_password)));
+ remmina_file_set_int(priv->remmina_file, "assistance_mode", 1);
+ }else{
+ remmina_file_set_int(priv->remmina_file, "assistance_mode", 0);
+ }
+
+ }
+
+ if (priv->keymap_combo)
+ remmina_file_set_string(priv->remmina_file, "keymap",
+ remmina_public_combo_get_active_text(GTK_COMBO_BOX(priv->keymap_combo)));
+
+ remmina_file_editor_save_behavior_tab(gfe);
+ remmina_file_editor_save_ssh_tunnel_tab(gfe);
+ return remmina_file_editor_update_settings(gfe, failed_widget);
+}
+
+static void remmina_file_editor_on_default(GtkWidget *button, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *gf;
+ GtkWidget *dialog;
+
+ GtkWidget *failed_widget = NULL;
+ GError *err = remmina_file_editor_update(gfe, &failed_widget);
+ if (err) {
+ g_warning(_("Couldn't validate user input. %s"), err->message);
+ remmina_file_editor_show_validation_error_popup(gfe, failed_widget, err);
+ return;
+ }
+
+ gf = remmina_file_dup(gfe->priv->remmina_file);
+
+ remmina_file_set_filename(gf, remmina_pref_file);
+
+ /* Clear properties that should never be default */
+ remmina_file_set_string(gf, "name", NULL);
+ remmina_file_set_string(gf, "server", NULL);
+ remmina_file_set_string(gf, "password", NULL);
+ remmina_file_set_string(gf, "precommand", NULL);
+ remmina_file_set_string(gf, "postcommand", NULL);
+
+ remmina_file_set_string(gf, "ssh_tunnel_server", NULL);
+ remmina_file_set_string(gf, "ssh_tunnel_password", NULL);
+ remmina_file_set_string(gf, "ssh_tunnel_passphrase", NULL);
+
+ remmina_file_save(gf);
+ remmina_file_free(gf);
+
+ dialog = gtk_message_dialog_new(GTK_WINDOW(gfe), GTK_DIALOG_MODAL, GTK_MESSAGE_INFO,
+ GTK_BUTTONS_OK, _("Default settings saved."));
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+}
+
+static void remmina_file_editor_on_save(GtkWidget *button, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+
+ GtkWidget *failed_widget = NULL;
+ GError *err = remmina_file_editor_update(gfe, &failed_widget);
+ if (err) {
+ g_warning(_("Couldn't validate user input. %s"), err->message);
+ remmina_file_editor_show_validation_error_popup(gfe, failed_widget, err);
+ return;
+ }
+
+ remmina_file_editor_file_save(gfe);
+
+ remmina_file_save(gfe->priv->remmina_file);
+ remmina_icon_populate_menu();
+
+ gtk_widget_destroy(GTK_WIDGET(gfe));
+}
+
+static void remmina_file_editor_on_connect(GtkWidget *button, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *gf;
+
+ GtkWidget *failed_widget = NULL;
+ GError *err = remmina_file_editor_update(gfe, &failed_widget);
+ if (err) {
+ g_warning(_("Couldn't validate user input. %s"), err->message);
+ remmina_file_editor_show_validation_error_popup(gfe, failed_widget, err);
+ return;
+ }
+
+ gf = remmina_file_dup(gfe->priv->remmina_file);
+ /* Put server into name for "Quick Connect" */
+ if (remmina_file_get_filename(gf) == NULL)
+ remmina_file_set_string(gf, "name", remmina_file_get_string(gf, "server"));
+ gtk_widget_destroy(GTK_WIDGET(gfe));
+ gf->prevent_saving = TRUE;
+ rcw_open_from_file(gf);
+}
+
+static void remmina_file_editor_on_save_connect(GtkWidget *button, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ /** @TODO: Call remmina_file_editor_on_save */
+ RemminaFile *gf;
+
+ GtkWidget *failed_widget = NULL;
+ GError *err = remmina_file_editor_update(gfe, &failed_widget);
+ if (err) {
+ g_warning(_("Couldn't validate user input. %s"), err->message);
+ remmina_file_editor_show_validation_error_popup(gfe, failed_widget, err);
+ return;
+ }
+
+ remmina_file_editor_file_save(gfe);
+
+ remmina_file_save(gfe->priv->remmina_file);
+ remmina_icon_populate_menu();
+
+ gf = remmina_file_dup(gfe->priv->remmina_file);
+ /* Put server into name for Quick Connect */
+ if (remmina_file_get_filename(gf) == NULL)
+ remmina_file_set_string(gf, "name", remmina_file_get_string(gf, "server"));
+ gtk_widget_destroy(GTK_WIDGET(gfe));
+ rcw_open_from_file(gf);
+}
+
+static void remmina_file_editor_on_cancel(GtkWidget *button, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ gtk_widget_destroy(GTK_WIDGET(gfe));
+}
+
+static void remmina_file_editor_init(RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv;
+ GtkWidget *widget;
+
+ priv = g_new0(RemminaFileEditorPriv, 1);
+ gfe->priv = priv;
+
+ /* Create the editor dialog */
+ gtk_window_set_title(GTK_WINDOW(gfe), _("Remote Connection Profile"));
+
+ widget = gtk_dialog_add_button(GTK_DIALOG(gfe), (_("_Cancel")), GTK_RESPONSE_CANCEL);
+ g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(remmina_file_editor_on_cancel), gfe);
+
+ /* Default button */
+ widget = gtk_dialog_add_button(GTK_DIALOG(gfe), (_("Save as Default")), GTK_RESPONSE_OK);
+ gtk_widget_set_tooltip_text(GTK_WIDGET(widget), _("Use the current settings as the default for all new connection profiles"));
+ g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(remmina_file_editor_on_default), gfe);
+
+ widget = gtk_dialog_add_button(GTK_DIALOG(gfe), (_("_Save")), GTK_RESPONSE_APPLY);
+ g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(remmina_file_editor_on_save), gfe);
+ gtk_widget_set_sensitive(widget, FALSE);
+ priv->save_button = widget;
+
+ widget = gtk_dialog_add_button(GTK_DIALOG(gfe), (_("Connect")), GTK_RESPONSE_ACCEPT);
+ g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(remmina_file_editor_on_connect), gfe);
+
+ widget = gtk_dialog_add_button(GTK_DIALOG(gfe), (_("_Save and Connect")), GTK_RESPONSE_OK);
+ gtk_widget_set_can_default(widget, TRUE);
+ g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(remmina_file_editor_on_save_connect), gfe);
+
+ gtk_dialog_set_default_response(GTK_DIALOG(gfe), GTK_RESPONSE_OK);
+ gtk_window_set_default_size(GTK_WINDOW(gfe), 800, 600);
+
+ g_signal_connect(G_OBJECT(gfe), "destroy", G_CALLBACK(remmina_file_editor_destroy), NULL);
+ g_signal_connect(G_OBJECT(gfe), "realize", G_CALLBACK(remmina_file_editor_on_realize), NULL);
+
+ priv->setting_widgets = g_hash_table_new(g_str_hash, g_str_equal);
+
+ remmina_widget_pool_register(GTK_WIDGET(gfe));
+}
+
+static gboolean remmina_file_editor_iterate_protocol(gchar *protocol, RemminaPlugin *plugin, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditor *gfe = REMMINA_FILE_EDITOR(data);
+ GtkListStore *store;
+ GtkTreeIter iter;
+ gboolean first;
+
+ store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(gfe->priv->protocol_combo)));
+
+ first = !gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, protocol, 1, g_dgettext(plugin->domain, plugin->description), 2,
+ ((RemminaProtocolPlugin *)plugin)->icon_name, -1);
+
+ if (first || g_strcmp0(protocol, remmina_file_get_string(gfe->priv->remmina_file, "protocol")) == 0)
+ gtk_combo_box_set_active_iter(GTK_COMBO_BOX(gfe->priv->protocol_combo), &iter);
+
+ return FALSE;
+}
+
+void remmina_file_editor_check_profile(RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv;
+
+ priv = gfe->priv;
+ gtk_widget_set_sensitive(priv->group_combo, TRUE);
+ gtk_widget_set_sensitive(priv->save_button, TRUE);
+}
+
+static void remmina_file_editor_entry_on_changed(GtkEditable *editable, RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv;
+
+ priv = gfe->priv;
+ if (remmina_file_get_filename(priv->remmina_file) == NULL) {
+ remmina_file_generate_filename(priv->remmina_file);
+ /* TODO: Probably to be removed */
+ remmina_file_editor_check_profile(gfe);
+ } else {
+ remmina_file_delete(remmina_file_get_filename(priv->remmina_file));
+ remmina_file_generate_filename(priv->remmina_file);
+ remmina_file_editor_check_profile(gfe);
+ }
+}
+
+void remmina_file_editor_file_save(RemminaFileEditor *gfe)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditorPriv *priv;
+
+ priv = gfe->priv;
+ if (remmina_file_get_filename(priv->remmina_file) == NULL) {
+ remmina_file_generate_filename(priv->remmina_file);
+ } else {
+ remmina_file_delete(remmina_file_get_filename(priv->remmina_file));
+ remmina_file_generate_filename(priv->remmina_file);
+ }
+}
+
+GtkWidget *remmina_file_editor_new_from_file(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ RemminaFileEditor *gfe;
+ RemminaFileEditorPriv *priv;
+ GtkWidget *grid;
+ GtkWidget *widget;
+ gchar *groups;
+ gchar *s;
+ const gchar *cs;
+
+ gfe = REMMINA_FILE_EDITOR(g_object_new(REMMINA_TYPE_FILE_EDITOR, NULL));
+ priv = gfe->priv;
+ priv->remmina_file = remminafile;
+
+ if (remmina_file_get_filename(remminafile) == NULL)
+ gtk_dialog_set_response_sensitive(GTK_DIALOG(gfe), GTK_RESPONSE_APPLY, FALSE);
+
+ /* Create the "Profile" group on the top (for name and protocol) */
+ grid = gtk_grid_new();
+ gtk_widget_show(grid);
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 4);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
+ gtk_grid_set_column_homogeneous(GTK_GRID(grid), TRUE);
+ gtk_container_set_border_width(GTK_CONTAINER(grid), 8);
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(gfe))), grid, FALSE, FALSE, 2);
+
+ // remmina_public_create_group(GTK_GRID(grid), _("Profile"), 0, 4, 3);
+
+ gboolean profile_file_exists = (remmina_file_get_filename(remminafile) != NULL);
+
+ /* Profile: Name */
+ widget = gtk_label_new(_("Name"));
+ gtk_widget_show(widget);
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, 3, 2, 1);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 10);
+
+ widget = gtk_entry_new();
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 3, 3, 1);
+ gtk_entry_set_max_length(GTK_ENTRY(widget), 100);
+ priv->name_entry = widget;
+
+ if (!profile_file_exists) {
+ gtk_entry_set_text(GTK_ENTRY(widget), _("Quick Connect"));
+#if GTK_CHECK_VERSION(3, 16, 0)
+ gtk_entry_grab_focus_without_selecting(GTK_ENTRY(widget));
+#endif
+ g_signal_connect(G_OBJECT(widget), "changed", G_CALLBACK(remmina_file_editor_entry_on_changed), gfe);
+ } else {
+ cs = remmina_file_get_string(remminafile, "name");
+ gtk_entry_set_text(GTK_ENTRY(widget), cs ? cs : "");
+ }
+
+ /* Profile: Group */
+ widget = gtk_label_new(_("Group"));
+ gtk_widget_show(widget);
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, 6, 2, 1);
+
+ groups = remmina_file_manager_get_groups();
+ priv->group_combo = remmina_public_create_combo_entry(groups, remmina_file_get_string(remminafile, "group"), FALSE);
+ g_free(groups);
+ gtk_widget_show(priv->group_combo);
+ gtk_grid_attach(GTK_GRID(grid), priv->group_combo, 1, 6, 3, 1);
+ gtk_widget_set_sensitive(priv->group_combo, FALSE);
+
+ s = g_strdup_printf(_("Use '%s' as subgroup delimiter"), "/");
+ gtk_widget_set_tooltip_text(priv->group_combo, s);
+ g_free(s);
+
+ /* Profile: Labels */
+ widget = gtk_label_new(_("Labels"));
+ gtk_widget_show(widget);
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, 9, 2, 1);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 10);
+
+ widget = gtk_entry_new();
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 9, 3, 1);
+ gtk_entry_set_max_length(GTK_ENTRY(widget), 255);
+ priv->labels_entry = widget;
+
+ if (!profile_file_exists) {
+ gtk_widget_set_tooltip_text(widget, _("Label1,Label2"));
+#if GTK_CHECK_VERSION(3, 16, 0)
+ gtk_entry_grab_focus_without_selecting(GTK_ENTRY(widget));
+#endif
+ g_signal_connect(G_OBJECT(widget), "changed", G_CALLBACK(remmina_file_editor_entry_on_changed), gfe);
+ } else {
+ cs = remmina_file_get_string(remminafile, "labels");
+ gtk_entry_set_text(GTK_ENTRY(widget), cs ? cs : "");
+ }
+
+ /* Profile: Protocol */
+ widget = gtk_label_new(_("Protocol"));
+ gtk_widget_show(widget);
+ gtk_widget_set_valign(widget, GTK_ALIGN_START);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, 12, 2, 1);
+
+ widget = remmina_public_create_combo(TRUE);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 1, 12, 3, 1);
+ priv->protocol_combo = widget;
+ remmina_plugin_manager_for_each_plugin(REMMINA_PLUGIN_TYPE_PROTOCOL, remmina_file_editor_iterate_protocol, gfe);
+ g_signal_connect(G_OBJECT(widget), "changed", G_CALLBACK(remmina_file_editor_protocol_combo_on_changed), gfe);
+
+ /* Create the "Preference" frame */
+ widget = gtk_event_box_new();
+ gtk_widget_show(widget);
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(gfe))), widget, TRUE, TRUE, 2);
+ priv->config_box = widget;
+
+ priv->config_container = NULL;
+ priv->config_scrollable = NULL;
+
+ remmina_file_editor_protocol_combo_on_changed(GTK_COMBO_BOX(priv->protocol_combo), gfe);
+
+ remmina_file_editor_check_profile(gfe);
+
+ return GTK_WIDGET(gfe);
+}
+
+GtkWidget *remmina_file_editor_new(void)
+{
+ TRACE_CALL(__func__);
+ return remmina_file_editor_new_full(NULL, NULL);
+}
+
+GtkWidget *remmina_file_editor_new_full(const gchar *server, const gchar *protocol)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+
+ remminafile = remmina_file_new();
+ if (server)
+ remmina_file_set_string(remminafile, "server", server);
+ if (protocol)
+ remmina_file_set_string(remminafile, "protocol", protocol);
+
+ return remmina_file_editor_new_from_file(remminafile);
+}
+
+GtkWidget *remmina_file_editor_new_copy(const gchar *filename)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+ GtkWidget *dialog;
+
+ remminafile = remmina_file_copy(filename);
+
+ if (remminafile) {
+ return remmina_file_editor_new_from_file(remminafile);
+ } else {
+ dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
+ _("Could not find the file “%s”."), filename);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ return NULL;
+ }
+}
+
+GtkWidget *remmina_file_editor_new_from_filename(const gchar *filename)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+
+ remminafile = remmina_file_manager_load_file(filename);
+ if (remminafile) {
+ if (remmina_file_get_int(remminafile, "profile-lock", FALSE) && remmina_unlock_new(remmina_main_get_window()) == 0)
+ return NULL;
+ return remmina_file_editor_new_from_file(remminafile);
+ } else {
+ GtkWidget *dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
+ _("Could not find the file “%s”."), filename);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ return NULL;
+ }
+}
diff --git a/src/remmina_file_editor.h b/src/remmina_file_editor.h
new file mode 100644
index 0000000..baf034a
--- /dev/null
+++ b/src/remmina_file_editor.h
@@ -0,0 +1,80 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 <stdarg.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "remmina_file.h"
+
+G_BEGIN_DECLS
+
+#define REMMINA_TYPE_FILE_EDITOR (remmina_file_editor_get_type())
+#define REMMINA_FILE_EDITOR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_FILE_EDITOR, RemminaFileEditor))
+#define REMMINA_FILE_EDITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_FILE_EDITOR, RemminaFileEditorClass))
+#define REMMINA_IS_FILE_EDITOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_FILE_EDITOR))
+#define REMMINA_IS_FILE_EDITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_FILE_EDITOR))
+#define REMMINA_FILE_EDITOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_FILE_EDITOR, RemminaFileEditorClass))
+
+typedef struct _RemminaFileEditorPriv RemminaFileEditorPriv;
+
+typedef struct _RemminaFileEditor {
+ GtkDialog dialog;
+
+ RemminaFileEditorPriv * priv;
+} RemminaFileEditor;
+
+typedef struct _RemminaFileEditorClass {
+ GtkDialogClass parent_class;
+} RemminaFileEditorClass;
+
+GType remmina_file_editor_get_type(void)
+G_GNUC_CONST;
+
+/* Base constructor */
+GtkWidget *remmina_file_editor_new_from_file(RemminaFile *remminafile);
+/* Create new file */
+GtkWidget *remmina_file_editor_new(void);
+GtkWidget *remmina_file_editor_new_full(const gchar *server, const gchar *protocol);
+GtkWidget *remmina_file_editor_new_copy(const gchar *filename);
+/* Open existing file */
+GtkWidget *remmina_file_editor_new_from_filename(const gchar *filename);
+void remmina_file_editor_check_profile(RemminaFileEditor *gfe);
+void remmina_file_editor_file_save(RemminaFileEditor *gfe);
+
+G_END_DECLS
diff --git a/src/remmina_file_manager.c b/src/remmina_file_manager.c
new file mode 100644
index 0000000..10a3022
--- /dev/null
+++ b/src/remmina_file_manager.c
@@ -0,0 +1,372 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-2010 Vic Lee
+ * Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo
+ * Copyright (C) 2016-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.
+ *
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <gtk/gtk.h>
+#include <string.h>
+
+#include "remmina_log.h"
+#include "remmina_public.h"
+#include "remmina_pref.h"
+#include "remmina_string_array.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_file_manager.h"
+#include "remmina/remmina_trace_calls.h"
+
+static gchar *remminadir;
+static gchar *cachedir;
+
+/**
+ * Return datadir_path from pref or first found data dir as per XDG specs.
+ *
+ * The returned string must be freed by the caller with g_free
+ */
+gchar *remmina_file_get_datadir(void)
+{
+ TRACE_CALL(__func__);
+ const gchar *dir = ".remmina";
+ int i;
+
+ /* From preferences, datadir_path */
+ remminadir = remmina_pref_get_value("datadir_path");
+ if (remminadir != NULL && strlen(remminadir) > 0)
+ if (g_file_test(remminadir, G_FILE_TEST_IS_DIR))
+ return remminadir;
+ g_free(remminadir), remminadir = NULL;
+ /* Legacy ~/.remmina */
+ remminadir = g_build_path("/", g_get_home_dir(), dir, NULL);
+ if (g_file_test(remminadir, G_FILE_TEST_IS_DIR))
+ return remminadir;
+ g_free(remminadir), remminadir = NULL;
+ /* ~/.local/share/remmina */
+ remminadir = g_build_path("/", g_get_user_data_dir(), "remmina", NULL);
+ if (g_file_test(remminadir, G_FILE_TEST_IS_DIR))
+ return remminadir;
+ g_free(remminadir), remminadir = NULL;
+ /* /usr/local/share/remmina */
+ const gchar *const *dirs = g_get_system_data_dirs();
+
+ g_free(remminadir), remminadir = NULL;
+ for (i = 0; dirs[i] != NULL; ++i) {
+ remminadir = g_build_path("/", dirs[i], "remmina", NULL);
+ if (g_file_test(remminadir, G_FILE_TEST_IS_DIR))
+ return remminadir;
+ g_free(remminadir), remminadir = NULL;
+ }
+ /* The last case we use the home ~/.local/share/remmina */
+ remminadir = g_build_path("/", g_get_user_data_dir(), "remmina", NULL);
+ return remminadir;
+}
+
+/** @todo remmina_pref_file_do_copy and remmina_file_manager_do_copy to remmina_files_copy */
+static gboolean remmina_file_manager_do_copy(const char *src_path, const char *dst_path)
+{
+ GFile *src = g_file_new_for_path(src_path), *dst = g_file_new_for_path(dst_path);
+ /* We don’t overwrite the target if it exists */
+ const gboolean ok = g_file_copy(src, dst, G_FILE_COPY_NONE, NULL, NULL, NULL, NULL);
+
+ g_object_unref(dst);
+ g_object_unref(src);
+
+ return ok;
+}
+
+/**
+ * It creates the Remmina data and cache folders
+ *
+ * If it finds the legacy ~/.remmina folder it copies the connection profiles into the new folder.
+ *
+ * If it finds default profiles in the XDG_DATA_DIRS it copies the profiles into the user data-folder.
+ */
+void remmina_file_manager_init(void)
+{
+ TRACE_CALL(__func__);
+ GDir *dir;
+ const gchar *legacy = ".remmina";
+ const gchar *filename;
+ int i;
+
+ /* Get and create the XDG_DATA_HOME directory */
+ remminadir = remmina_pref_get_value("datadir_path");
+ if (g_mkdir_with_parents(remminadir, 0750) == 0) {
+ REMMINA_DEBUG("Initialized the \"%s\" data folder", remminadir);
+ g_free(remminadir), remminadir = NULL;
+ } else {
+ g_free(remminadir), remminadir = NULL;
+ /* Get and create the XDG_DATA_HOME directory */
+ remminadir = g_build_path("/", g_get_user_data_dir(), "remmina", NULL);
+ if (g_mkdir_with_parents(remminadir, 0750) == 0)
+ REMMINA_DEBUG("Initialized the \"%s\" data folder", remminadir);
+ else
+ REMMINA_CRITICAL("Cannot create the \"%s\" data folder", remminadir);
+ g_free(remminadir), remminadir = NULL;
+ }
+ /* Create the XDG_CACHE_HOME directory */
+ cachedir = g_build_path("/", g_get_user_cache_dir(), "remmina", NULL);
+ g_mkdir_with_parents(cachedir, 0750);
+ g_free(cachedir), cachedir = NULL;
+ /* Empty legacy ~/.remmina */
+ remminadir = g_build_path("/", g_get_home_dir(), legacy, NULL);
+ if (g_file_test(remminadir, G_FILE_TEST_IS_DIR)) {
+ dir = g_dir_open(remminadir, 0, NULL);
+ while ((filename = g_dir_read_name(dir)) != NULL) {
+ remmina_file_manager_do_copy(
+ g_build_path("/", remminadir, filename, NULL),
+ g_build_path("/", g_get_user_data_dir(),
+ "remmina", filename, NULL));
+ }
+ g_dir_close(dir);
+ }
+
+ /* XDG_DATA_DIRS, i.e. /usr/local/share/remmina */
+ const gchar *const *dirs = g_get_system_data_dirs();
+
+ g_free(remminadir), remminadir = NULL;
+ for (i = 0; dirs[i] != NULL; ++i) {
+ remminadir = g_build_path("/", dirs[i], "remmina", NULL);
+ if (g_file_test(remminadir, G_FILE_TEST_IS_DIR)) {
+ dir = g_dir_open(remminadir, 0, NULL);
+ while ((filename = g_dir_read_name(dir)) != NULL) {
+ remmina_file_manager_do_copy(
+ g_build_path("/", remminadir, filename, NULL),
+ g_build_path("/", g_get_user_data_dir(),
+ "remmina", filename, NULL));
+ }
+ }
+ g_free(remminadir), remminadir = NULL;
+ }
+ /* At last we make sure we use XDG_USER_DATA */
+ if (remminadir != NULL)
+ g_free(remminadir), remminadir = NULL;
+}
+
+gint remmina_file_manager_iterate(GFunc func, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gchar filename[MAX_PATH_LEN];
+ GDir *dir;
+ const gchar *name;
+ RemminaFile *remminafile;
+ gint items_count = 0;
+ gchar *remmina_data_dir;
+
+ remmina_data_dir = remmina_file_get_datadir();
+ dir = g_dir_open(remmina_data_dir, 0, NULL);
+
+ if (dir) {
+ while ((name = g_dir_read_name(dir)) != NULL) {
+ if (!g_str_has_suffix(name, ".remmina"))
+ continue;
+ g_snprintf(filename, MAX_PATH_LEN, "%s/%s",
+ remmina_data_dir, name);
+ remminafile = remmina_file_load(filename);
+ if (remminafile) {
+ (*func)(remminafile, user_data);
+ remmina_file_free(remminafile);
+ items_count++;
+ }
+ }
+ g_dir_close(dir);
+ }
+ g_free(remmina_data_dir);
+ return items_count;
+}
+
+gchar *remmina_file_manager_get_groups(void)
+{
+ TRACE_CALL(__func__);
+ gchar filename[MAX_PATH_LEN];
+ GDir *dir;
+ const gchar *name;
+ RemminaFile *remminafile;
+ RemminaStringArray *array;
+ const gchar *group;
+ gchar *groups;
+ gchar *remmina_data_dir;
+
+ remmina_data_dir = remmina_file_get_datadir();
+ array = remmina_string_array_new();
+
+ dir = g_dir_open(remmina_data_dir, 0, NULL);
+
+ if (dir == NULL)
+ return 0;
+ while ((name = g_dir_read_name(dir)) != NULL) {
+ if (!g_str_has_suffix(name, ".remmina"))
+ continue;
+ g_snprintf(filename, MAX_PATH_LEN, "%s/%s", remmina_data_dir, name);
+ remminafile = remmina_file_load(filename);
+ if (remminafile) {
+ group = remmina_file_get_string(remminafile, "group");
+ if (group && remmina_string_array_find(array, group) < 0)
+ remmina_string_array_add(array, group);
+ remmina_file_free(remminafile);
+ }
+ }
+ g_dir_close(dir);
+ remmina_string_array_sort(array);
+ groups = remmina_string_array_to_string(array);
+ remmina_string_array_free(array);
+ g_free(remmina_data_dir);
+ return groups;
+}
+
+static void remmina_file_manager_add_group(GNode *node, const gchar *group)
+{
+ TRACE_CALL(__func__);
+ gint cmp;
+ gchar *p1;
+ gchar *p2;
+ GNode *child;
+ gboolean found;
+ RemminaGroupData *data;
+
+ if (node == NULL)
+ return;
+
+ if (group == NULL || group[0] == '\0')
+ return;
+
+ p1 = g_strdup(group);
+ p2 = strchr(p1, '/');
+
+ if (p2)
+ *p2++ = '\0';
+
+ found = FALSE;
+
+ for (child = g_node_first_child(node); child; child = g_node_next_sibling(child)) {
+ cmp = g_strcmp0(((RemminaGroupData *)child->data)->name, p1);
+
+ if (cmp == 0) {
+ found = TRUE;
+ break;
+ }
+
+ if (cmp > 0)
+ break;
+ }
+
+ if (!found) {
+ data = g_new0(RemminaGroupData, 1);
+ data->name = p1;
+ if (node->data)
+ data->group = g_strdup_printf("%s/%s", ((RemminaGroupData *)node->data)->group, p1);
+ else
+ data->group = g_strdup(p1);
+ if (child)
+ child = g_node_insert_data_before(node, child, data);
+ else
+ child = g_node_append_data(node, data);
+ }
+ remmina_file_manager_add_group(child, p2);
+
+ if (found)
+ g_free(p1);
+}
+
+GNode *remmina_file_manager_get_group_tree(void)
+{
+ TRACE_CALL(__func__);
+ gchar filename[MAX_PATH_LEN];
+ GDir *dir;
+ g_autofree gchar *datadir = NULL;
+ const gchar *name;
+ RemminaFile *remminafile;
+ const gchar *group;
+ GNode *root;
+
+ root = g_node_new(NULL);
+
+ datadir = g_strdup(remmina_file_get_datadir());
+ dir = g_dir_open(datadir, 0, NULL);
+
+ if (dir == NULL)
+ return root;
+ while ((name = g_dir_read_name(dir)) != NULL) {
+ if (!g_str_has_suffix(name, ".remmina"))
+ continue;
+ g_snprintf(filename, MAX_PATH_LEN, "%s/%s", datadir, name);
+ remminafile = remmina_file_load(filename);
+ if (remminafile) {
+ group = remmina_file_get_string(remminafile, "group");
+ remmina_file_manager_add_group(root, group);
+ remmina_file_free(remminafile);
+ }
+ }
+ g_dir_close(dir);
+ return root;
+}
+
+void remmina_file_manager_free_group_tree(GNode *node)
+{
+ TRACE_CALL(__func__);
+ RemminaGroupData *data;
+ GNode *child;
+
+ if (!node)
+ return;
+ data = (RemminaGroupData *)node->data;
+ if (data) {
+ g_free(data->name);
+ g_free(data->group);
+ g_free(data);
+ node->data = NULL;
+ }
+ for (child = g_node_first_child(node); child; child = g_node_next_sibling(child))
+ remmina_file_manager_free_group_tree(child);
+ g_node_unlink(node);
+}
+
+RemminaFile *remmina_file_manager_load_file(const gchar *filename)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile = NULL;
+ RemminaFilePlugin *plugin;
+ gchar *p;
+
+ if ((p = strrchr(filename, '.')) != NULL && g_strcmp0(p + 1, "remmina") == 0) {
+ remminafile = remmina_file_load(filename);
+ } else {
+ plugin = remmina_plugin_manager_get_import_file_handler(filename);
+ if (plugin)
+ remminafile = plugin->import_func(plugin, filename);
+ }
+ return remminafile;
+}
diff --git a/src/remmina_file_manager.h b/src/remmina_file_manager.h
new file mode 100644
index 0000000..08535e3
--- /dev/null
+++ b/src/remmina_file_manager.h
@@ -0,0 +1,61 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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
+
+#include "remmina_file.h"
+
+G_BEGIN_DECLS
+
+typedef struct _RemminaGroupData {
+ gchar * name;
+ gchar * group;
+ gchar * datetime;
+ gchar * labels;
+} RemminaGroupData;
+
+/* Initialize */
+gchar *remmina_file_get_datadir(void);
+void remmina_file_manager_init(void);
+/* Iterate all .remmina connections in the home directory */
+gint remmina_file_manager_iterate(GFunc func, gpointer user_data);
+/* Get a list of groups */
+gchar *remmina_file_manager_get_groups(void);
+GNode *remmina_file_manager_get_group_tree(void);
+void remmina_file_manager_free_group_tree(GNode *node);
+/* Load or import a file */
+RemminaFile *remmina_file_manager_load_file(const gchar *filename);
+
+G_END_DECLS
diff --git a/src/remmina_ftp_client.c b/src/remmina_ftp_client.c
new file mode 100644
index 0000000..294f2db
--- /dev/null
+++ b/src/remmina_ftp_client.c
@@ -0,0 +1,1276 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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.
+ *
+ */
+
+#define _FILE_OFFSET_BITS 64
+
+#include <gdk/gdk.h>
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include "config.h"
+#include "remmina_public.h"
+#include "remmina_pref.h"
+#include "remmina_marshals.h"
+#include "remmina_ftp_client.h"
+#include "remmina_masterthread_exec.h"
+#include "remmina/remmina_trace_calls.h"
+
+/* -------------------- RemminaCellRendererPixbuf ----------------------- */
+/* A tiny cell renderer that extends the default pixbuf cell render to accept activation */
+
+#define REMMINA_TYPE_CELL_RENDERER_PIXBUF \
+ (remmina_cell_renderer_pixbuf_get_type())
+#define REMMINA_CELL_RENDERER_PIXBUF(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_CELL_RENDERER_PIXBUF, RemminaCellRendererPixbuf))
+#define REMMINA_CELL_RENDERER_PIXBUF_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_CELL_RENDERER_PIXBUF, RemminaCellRendererPixbufClass))
+#define REMMINA_IS_CELL_RENDERER_PIXBUF(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_CELL_RENDERER_PIXBUF))
+
+typedef struct _RemminaCellRendererPixbuf {
+ GtkCellRendererPixbuf renderer;
+} RemminaCellRendererPixbuf;
+
+typedef struct _RemminaCellRendererPixbufClass {
+ GtkCellRendererPixbufClass parent_class;
+
+ void (*activate)(RemminaCellRendererPixbuf * renderer);
+} RemminaCellRendererPixbufClass;
+
+GType remmina_cell_renderer_pixbuf_get_type(void)
+G_GNUC_CONST;
+
+G_DEFINE_TYPE(RemminaCellRendererPixbuf, remmina_cell_renderer_pixbuf, GTK_TYPE_CELL_RENDERER_PIXBUF)
+
+static guint remmina_cell_renderer_pixbuf_signals[1] =
+{ 0 };
+
+static gboolean remmina_cell_renderer_pixbuf_activate(GtkCellRenderer *renderer, GdkEvent *event, GtkWidget *widget,
+ const gchar *path, const GdkRectangle *background_area, const GdkRectangle *cell_area,
+ GtkCellRendererState flags)
+{
+ TRACE_CALL(__func__);
+ g_signal_emit(G_OBJECT(renderer), remmina_cell_renderer_pixbuf_signals[0], 0, path);
+ return TRUE;
+}
+
+static void remmina_cell_renderer_pixbuf_class_init(RemminaCellRendererPixbufClass *klass)
+{
+ TRACE_CALL(__func__);
+ GtkCellRendererClass *renderer_class = GTK_CELL_RENDERER_CLASS(klass);
+
+ renderer_class->activate = remmina_cell_renderer_pixbuf_activate;
+
+ remmina_cell_renderer_pixbuf_signals[0] = g_signal_new("activate", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaCellRendererPixbufClass, activate), NULL,
+ NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING);
+}
+
+static void remmina_cell_renderer_pixbuf_init(RemminaCellRendererPixbuf *renderer)
+{
+ TRACE_CALL(__func__);
+ g_object_set(G_OBJECT(renderer), "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE, NULL);
+}
+
+static GtkCellRenderer*
+remmina_cell_renderer_pixbuf_new(void)
+{
+ TRACE_CALL(__func__);
+ GtkCellRenderer *renderer;
+
+ renderer = GTK_CELL_RENDERER(g_object_new(REMMINA_TYPE_CELL_RENDERER_PIXBUF, NULL));
+ return renderer;
+}
+
+/* --------------------- RemminaFTPClient ----------------------------*/
+G_DEFINE_TYPE( RemminaFTPClient, remmina_ftp_client, GTK_TYPE_GRID)
+
+#define BUSY_CURSOR \
+ if (GDK_IS_WINDOW(gtk_widget_get_window(GTK_WIDGET(client)))) \
+ { \
+ gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(client)), gdk_cursor_new_for_display(gdk_display_get_default(), GDK_WATCH)); \
+ gdk_display_flush(gdk_display_get_default()); \
+ }
+
+#define NORMAL_CURSOR \
+ if (GDK_IS_WINDOW(gtk_widget_get_window(GTK_WIDGET(client)))) \
+ { \
+ gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(client)), NULL); \
+ }
+
+struct _RemminaFTPClientPriv {
+ GtkWidget *directory_combo;
+ GtkWidget *vpaned;
+
+ GtkTreeModel *file_list_model;
+ GtkTreeModel *file_list_filter;
+ GtkTreeModel *file_list_sort;
+ GtkWidget *file_list_view;
+ gboolean file_list_show_hidden;
+
+ GtkTreeModel *task_list_model;
+ GtkWidget *task_list_view;
+
+ gchar *current_directory;
+ gchar *working_directory;
+
+ GtkWidget *file_action_widgets[10];
+ gboolean sensitive;
+ gboolean overwrite_all;
+ gboolean resume_all;
+};
+
+static gint remmina_ftp_client_taskid = 1;
+
+enum {
+ OPEN_DIR_SIGNAL, NEW_TASK_SIGNAL, CANCEL_TASK_SIGNAL, DELETE_FILE_SIGNAL, LAST_SIGNAL
+};
+
+static guint remmina_ftp_client_signals[LAST_SIGNAL] =
+{ 0 };
+
+static void remmina_ftp_client_class_init(RemminaFTPClientClass *klass)
+{
+ TRACE_CALL(__func__);
+ remmina_ftp_client_signals[OPEN_DIR_SIGNAL] = g_signal_new("open-dir", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaFTPClientClass, open_dir), NULL, NULL,
+ g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING);
+ remmina_ftp_client_signals[NEW_TASK_SIGNAL] = g_signal_new("new-task", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaFTPClientClass, new_task), NULL, NULL,
+ g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+ remmina_ftp_client_signals[CANCEL_TASK_SIGNAL] = g_signal_new("cancel-task", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaFTPClientClass, cancel_task), NULL, NULL,
+ remmina_marshal_BOOLEAN__INT, G_TYPE_BOOLEAN, 1, G_TYPE_INT);
+ remmina_ftp_client_signals[DELETE_FILE_SIGNAL] = g_signal_new("delete-file", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaFTPClientClass, delete_file), NULL, NULL,
+ remmina_marshal_BOOLEAN__INT_STRING, G_TYPE_BOOLEAN, 2, G_TYPE_INT, G_TYPE_STRING);
+}
+
+static void remmina_ftp_client_destroy(RemminaFTPClient *client, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ g_free(priv->current_directory);
+ g_free(priv->working_directory);
+ g_free(priv);
+}
+
+static void remmina_ftp_client_cell_data_filetype_pixbuf(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gint type;
+
+ /* Same as REMMINA_FTP_TASK_COLUMN_TYPE */
+ gtk_tree_model_get(model, iter, REMMINA_FTP_FILE_COLUMN_TYPE, &type, -1);
+
+ switch (type) {
+ case REMMINA_FTP_FILE_TYPE_DIR:
+ g_object_set(renderer, "icon-name", "folder", NULL);
+ break;
+ case REMMINA_FTP_FILE_TYPE_LINK:
+ g_object_set(renderer, "icon-name", "emblem-symbolic-link", NULL);
+ break;
+ default:
+ g_object_set(renderer, "icon-name", "text-x-generic", NULL);
+ break;
+ }
+}
+
+static void remmina_ftp_client_cell_data_progress_pixbuf(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gint tasktype, status;
+
+ gtk_tree_model_get(model, iter, REMMINA_FTP_TASK_COLUMN_TASKTYPE, &tasktype, REMMINA_FTP_TASK_COLUMN_STATUS, &status,
+ -1);
+
+ switch (status) {
+ case REMMINA_FTP_TASK_STATUS_WAIT:
+ g_object_set(renderer, "icon-name", "media-playback-pause", NULL);
+ break;
+ case REMMINA_FTP_TASK_STATUS_RUN:
+ g_object_set(renderer, "icon-name",
+ (tasktype == REMMINA_FTP_TASK_TYPE_UPLOAD ? "go-up" : "go-down"), NULL);
+ break;
+ case REMMINA_FTP_TASK_STATUS_FINISH:
+ g_object_set(renderer, "icon-name", "emblem-default", NULL);
+ break;
+ case REMMINA_FTP_TASK_STATUS_ERROR:
+ g_object_set(renderer, "icon-name", "emblem-unreadable", NULL);
+ break;
+ }
+}
+
+static gchar*
+remmina_ftp_client_size_to_str(gfloat size)
+{
+ TRACE_CALL(__func__);
+
+ return g_format_size_full((guint64)size, G_FORMAT_SIZE_IEC_UNITS);
+}
+
+static void remmina_ftp_client_cell_data_size(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gfloat size;
+ gchar *str;
+
+ gtk_tree_model_get(model, iter, REMMINA_FTP_FILE_COLUMN_SIZE, &size, -1);
+
+ str = remmina_ftp_client_size_to_str(size);
+ g_object_set(renderer, "text", str, NULL);
+ g_object_set(renderer, "xalign", 1.0, NULL);
+ g_free(str);
+}
+
+static void remmina_ftp_client_cell_data_permission(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gint permission;
+ gchar buf[11];
+
+ gtk_tree_model_get(model, iter, REMMINA_FTP_FILE_COLUMN_PERMISSION, &permission, -1);
+
+ buf[0] = ((permission & 040000) ? 'd' : '-');
+ buf[1] = ((permission & 0400) ? 'r' : '-');
+ buf[2] = ((permission & 0200) ? 'w' : '-');
+ buf[3] = ((permission & 0100) ? 'x' : '-');
+ buf[4] = ((permission & 040) ? 'r' : '-');
+ buf[5] = ((permission & 020) ? 'w' : '-');
+ buf[6] = ((permission & 010) ? 'x' : '-');
+ buf[7] = ((permission & 04) ? 'r' : '-');
+ buf[8] = ((permission & 02) ? 'w' : '-');
+ buf[9] = ((permission & 01) ? 'x' : '-');
+ buf[10] = '\0';
+
+ g_object_set(renderer, "text", buf, NULL);
+}
+
+static void remmina_ftp_client_cell_data_modified(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gint32 modified = 0;
+ GDateTime *datetime;
+ gchar* str;
+
+ gtk_tree_model_get(model, iter, REMMINA_FTP_FILE_COLUMN_MODIFIED, &modified, -1);
+
+ datetime = g_date_time_new_from_unix_local(modified);
+ str = g_date_time_format(datetime, "\%Y-\%m-\%d \%H:\%M:\%S");
+
+ g_object_set(renderer, "text", str, NULL);
+
+ g_date_time_unref(datetime);
+ g_free(str);
+}
+
+static void remmina_ftp_client_cell_data_size_progress(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gint status;
+ gfloat size, donesize;
+ gchar *strsize, *strdonesize, *str;
+
+ gtk_tree_model_get(model, iter, REMMINA_FTP_TASK_COLUMN_STATUS, &status, REMMINA_FTP_TASK_COLUMN_SIZE, &size,
+ REMMINA_FTP_TASK_COLUMN_DONESIZE, &donesize, -1);
+
+ if (status == REMMINA_FTP_TASK_STATUS_FINISH) {
+ str = remmina_ftp_client_size_to_str(size);
+ }else {
+ strsize = remmina_ftp_client_size_to_str(size);
+ strdonesize = remmina_ftp_client_size_to_str(donesize);
+ str = g_strdup_printf("%s / %s", strdonesize, strsize);
+ g_free(strsize);
+ g_free(strdonesize);
+ }
+
+ g_object_set(renderer, "text", str, NULL);
+ g_object_set(renderer, "xalign", 1.0, NULL);
+ g_free(str);
+}
+
+static void remmina_ftp_client_cell_data_progress(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gint status;
+ gfloat size, donesize;
+ gint progress;
+
+ gtk_tree_model_get(model, iter, REMMINA_FTP_TASK_COLUMN_STATUS, &status, REMMINA_FTP_TASK_COLUMN_SIZE, &size,
+ REMMINA_FTP_TASK_COLUMN_DONESIZE, &donesize, -1);
+ if (status == REMMINA_FTP_TASK_STATUS_FINISH) {
+ progress = 100;
+ }else {
+ if (size <= 1) {
+ progress = 0;
+ }else {
+ progress = (gint)(donesize / size * 100);
+ if (progress > 99)
+ progress = 99;
+ }
+ }
+ g_object_set(renderer, "value", progress, NULL);
+}
+
+static void remmina_ftp_client_open_dir(RemminaFTPClient *client, const gchar *dir)
+{
+ TRACE_CALL(__func__);
+ BUSY_CURSOR
+ g_signal_emit(G_OBJECT(client), remmina_ftp_client_signals[OPEN_DIR_SIGNAL], 0, dir);
+ NORMAL_CURSOR
+}
+
+static void remmina_ftp_client_dir_on_activate(GtkWidget *widget, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ remmina_ftp_client_open_dir(client, gtk_entry_get_text(GTK_ENTRY(widget)));
+}
+
+static void remmina_ftp_client_dir_on_changed(GtkWidget *widget, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *entry = gtk_bin_get_child(GTK_BIN(widget));
+
+ if (!gtk_widget_is_focus(entry)) {
+ gtk_widget_grab_focus(entry);
+ /* If the text was changed but the entry is not the focus, it should be changed by the drop-down list.
+ Not sure this will always work in the future, but it works right now :) */
+ remmina_ftp_client_open_dir(client, gtk_entry_get_text(GTK_ENTRY(entry)));
+ }
+}
+
+static void remmina_ftp_client_set_file_action_sensitive(RemminaFTPClient *client, gboolean sensitive)
+{
+ TRACE_CALL(__func__);
+ gint i;
+ for (i = 0; client->priv->file_action_widgets[i]; i++) {
+ gtk_widget_set_sensitive(client->priv->file_action_widgets[i], sensitive);
+ }
+ client->priv->sensitive = sensitive;
+}
+
+static void remmina_ftp_client_file_selection_on_changed(GtkTreeSelection *selection, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ GList *list;
+
+ list = gtk_tree_selection_get_selected_rows(selection, NULL);
+ remmina_ftp_client_set_file_action_sensitive(client, (list ? TRUE : FALSE));
+ g_list_free(list);
+}
+
+static gchar*
+remmina_ftp_client_get_download_dir(RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkFileChooserNative *dialog;
+ gchar *localdir = NULL;
+
+ dialog = gtk_file_chooser_native_new(_("Choose download location"),
+ GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(client))), GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
+ _("_OK"), _("_Cancel"));
+ if (priv->working_directory) {
+ gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), priv->working_directory);
+ }
+ if (gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ g_free(priv->working_directory);
+ priv->working_directory = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(dialog));
+ localdir = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+ }
+ gtk_native_dialog_destroy(GTK_NATIVE_DIALOG(dialog));
+ return localdir;
+}
+
+static void remmina_ftp_client_download(RemminaFTPClient *client, GtkTreeIter *piter, const gchar *localdir)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkListStore *store = GTK_LIST_STORE(priv->task_list_model);
+ GtkTreeIter iter;
+ gint type;
+ gchar *name;
+ gfloat size;
+
+ gtk_tree_model_get(priv->file_list_sort, piter, REMMINA_FTP_FILE_COLUMN_TYPE, &type, REMMINA_FTP_FILE_COLUMN_NAME,
+ &name, REMMINA_FTP_FILE_COLUMN_SIZE, &size, -1);
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, REMMINA_FTP_TASK_COLUMN_TYPE, type, REMMINA_FTP_TASK_COLUMN_NAME, name,
+ REMMINA_FTP_TASK_COLUMN_SIZE, size, REMMINA_FTP_TASK_COLUMN_TASKID, remmina_ftp_client_taskid++,
+ REMMINA_FTP_TASK_COLUMN_TASKTYPE, REMMINA_FTP_TASK_TYPE_DOWNLOAD, REMMINA_FTP_TASK_COLUMN_REMOTEDIR,
+ priv->current_directory, REMMINA_FTP_TASK_COLUMN_LOCALDIR, localdir, REMMINA_FTP_TASK_COLUMN_STATUS,
+ REMMINA_FTP_TASK_STATUS_WAIT, REMMINA_FTP_TASK_COLUMN_DONESIZE, 0.0, REMMINA_FTP_TASK_COLUMN_TOOLTIP,
+ NULL, -1);
+
+ g_free(name);
+
+ g_signal_emit(G_OBJECT(client), remmina_ftp_client_signals[NEW_TASK_SIGNAL], 0);
+}
+
+static gboolean remmina_ftp_client_task_list_on_query_tooltip(GtkWidget *widget, gint x, gint y, gboolean keyboard_tip,
+ GtkTooltip *tooltip, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkTreeIter iter;
+ GtkTreePath *path = NULL;
+ gchar *tmp;
+
+ if (!gtk_tree_view_get_tooltip_context(GTK_TREE_VIEW(priv->task_list_view), &x, &y, keyboard_tip, NULL, &path, &iter)) {
+ return FALSE;
+ }
+
+ gtk_tree_model_get(priv->task_list_model, &iter, REMMINA_FTP_TASK_COLUMN_TOOLTIP, &tmp, -1);
+ if (!tmp)
+ return FALSE;
+
+ gtk_tooltip_set_text(tooltip, tmp);
+
+ gtk_tree_view_set_tooltip_row(GTK_TREE_VIEW(priv->task_list_view), tooltip, path);
+
+ gtk_tree_path_free(path);
+ g_free(tmp);
+
+ return TRUE;
+}
+
+static void remmina_ftp_client_action_parent(GObject *object, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ remmina_ftp_client_open_dir(client, "..");
+}
+
+static void remmina_ftp_client_action_home(GObject *object, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ remmina_ftp_client_open_dir(client, NULL);
+}
+
+static void remmina_ftp_client_action_refresh(GObject *object, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ remmina_ftp_client_open_dir(client, ".");
+}
+
+static void remmina_ftp_client_action_download(GObject *object, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkTreeSelection *selection;
+ gchar *localdir;
+ GList *list, *list_iter;
+ GtkTreeIter iter;
+
+ selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->file_list_view));
+ if (!selection)
+ return;
+ list = gtk_tree_selection_get_selected_rows(selection, NULL);
+ if (!list)
+ return;
+
+ localdir = remmina_ftp_client_get_download_dir(client);
+ if (!localdir) {
+ g_list_free(list);
+ return;
+ }
+
+ list_iter = g_list_first(list);
+ while (list_iter) {
+ gtk_tree_model_get_iter(priv->file_list_sort, &iter, (GtkTreePath*)list_iter->data);
+ remmina_ftp_client_download(client, &iter, localdir);
+ list_iter = g_list_next(list_iter);
+ }
+ g_list_free(list);
+ g_free(localdir);
+}
+
+static void remmina_ftp_client_action_delete(GObject *object, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkWidget *dialog;
+ GtkTreeSelection *selection;
+ GList *list, *list_iter;
+ GtkTreeIter iter;
+ gint type;
+ gchar *name;
+ gchar *path;
+ gint response;
+ gboolean ret = TRUE;
+
+ selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->file_list_view));
+ if (!selection)
+ return;
+ list = gtk_tree_selection_get_selected_rows(selection, NULL);
+ if (!list)
+ return;
+
+ dialog = gtk_message_dialog_new(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(client))), GTK_DIALOG_MODAL,
+ GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, _("Are you sure to delete the selected files on server?"));
+ response = gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ if (response != GTK_RESPONSE_YES)
+ return;
+
+ BUSY_CURSOR
+
+ list_iter = g_list_first(list);
+ while (list_iter) {
+ gtk_tree_model_get_iter(priv->file_list_sort, &iter, (GtkTreePath*)list_iter->data);
+
+ gtk_tree_model_get(priv->file_list_sort, &iter, REMMINA_FTP_FILE_COLUMN_TYPE, &type,
+ REMMINA_FTP_FILE_COLUMN_NAME, &name, -1);
+
+ path = remmina_public_combine_path(priv->current_directory, name);
+ g_signal_emit(G_OBJECT(client), remmina_ftp_client_signals[DELETE_FILE_SIGNAL], 0, type, path, &ret);
+ g_free(name);
+ g_free(path);
+ if (!ret)
+ break;
+
+ list_iter = g_list_next(list_iter);
+ }
+ g_list_free(list);
+
+ NORMAL_CURSOR
+
+ if (ret) {
+ remmina_ftp_client_action_refresh(object, client);
+ }
+}
+
+static void remmina_ftp_client_upload_folder_on_toggled(GtkToggleButton *togglebutton, GtkWidget *widget)
+{
+ TRACE_CALL(__func__);
+ gtk_file_chooser_set_action(
+ GTK_FILE_CHOOSER(widget),
+ gtk_toggle_button_get_active(togglebutton) ?
+ GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER : GTK_FILE_CHOOSER_ACTION_OPEN);
+}
+
+static void remmina_ftp_client_action_upload(GObject *object, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkListStore *store = GTK_LIST_STORE(priv->task_list_model);
+ GtkTreeIter iter;
+ GtkWidget *dialog;
+ GtkWidget *upload_folder_check;
+ gint type;
+ GSList *files = NULL;
+ GSList *element;
+ gchar *path;
+ gchar *dir, *name;
+ struct stat st;
+
+ dialog = gtk_file_chooser_dialog_new(_("Choose a file to upload"),
+ GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(client))), GTK_FILE_CHOOSER_ACTION_OPEN, _("_Cancel"),
+ GTK_RESPONSE_CANCEL, _("Upload"), GTK_RESPONSE_ACCEPT, NULL);
+ gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE);
+ if (priv->working_directory) {
+ gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), priv->working_directory);
+ }
+ upload_folder_check = gtk_check_button_new_with_label(_("Upload folder"));
+ gtk_widget_show(upload_folder_check);
+ g_signal_connect(G_OBJECT(upload_folder_check), "toggled", G_CALLBACK(remmina_ftp_client_upload_folder_on_toggled),
+ dialog);
+ gtk_file_chooser_set_extra_widget(GTK_FILE_CHOOSER(dialog), upload_folder_check);
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ g_free(priv->working_directory);
+ priv->working_directory = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(dialog));
+ files = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
+ }
+ type = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(upload_folder_check)) ?
+ REMMINA_FTP_FILE_TYPE_DIR : REMMINA_FTP_FILE_TYPE_FILE;
+ gtk_widget_destroy(dialog);
+ if (!files)
+ return;
+
+ for (element = files; element; element = element->next) {
+ path = (gchar*)element->data;
+
+ if (g_stat(path, &st) < 0)
+ continue;
+
+ name = g_strrstr(path, "/");
+ if (name) {
+ *name++ = '\0';
+ dir = path;
+ }else {
+ name = path;
+ dir = NULL;
+ }
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, REMMINA_FTP_TASK_COLUMN_TYPE, type, REMMINA_FTP_TASK_COLUMN_NAME, name,
+ REMMINA_FTP_TASK_COLUMN_SIZE, (gfloat)st.st_size, REMMINA_FTP_TASK_COLUMN_TASKID,
+ remmina_ftp_client_taskid++, REMMINA_FTP_TASK_COLUMN_TASKTYPE, REMMINA_FTP_TASK_TYPE_UPLOAD,
+ REMMINA_FTP_TASK_COLUMN_REMOTEDIR, priv->current_directory, REMMINA_FTP_TASK_COLUMN_LOCALDIR,
+ dir, REMMINA_FTP_TASK_COLUMN_STATUS, REMMINA_FTP_TASK_STATUS_WAIT,
+ REMMINA_FTP_TASK_COLUMN_DONESIZE, 0.0, REMMINA_FTP_TASK_COLUMN_TOOLTIP, NULL, -1);
+
+ g_free(path);
+ }
+
+ g_slist_free(files);
+
+ g_signal_emit(G_OBJECT(client), remmina_ftp_client_signals[NEW_TASK_SIGNAL], 0);
+}
+
+static void remmina_ftp_client_popup_menu(RemminaFTPClient *client, GdkEventButton *event)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *menu;
+ GtkWidget *menuitem;
+ GtkWidget *image;
+
+ menu = gtk_menu_new();
+
+ menuitem = gtk_menu_item_new_with_label(_("Download"));
+ gtk_widget_show(menuitem);
+ image = gtk_image_new_from_icon_name("org.remmina.Remmina-document-save-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_widget_show(image);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(remmina_ftp_client_action_download), client);
+
+ menuitem = gtk_menu_item_new_with_label(_("Upload"));
+ gtk_widget_show(menuitem);
+ image = gtk_image_new_from_icon_name("org.remmina.Remmina-document-send-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_widget_show(image);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(remmina_ftp_client_action_upload), client);
+
+ menuitem = gtk_menu_item_new_with_mnemonic(_("_Delete"));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+ g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(remmina_ftp_client_action_delete), client);
+
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event);
+#else
+ gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
+#endif
+}
+
+static gboolean remmina_ftp_client_file_list_on_button_press(GtkWidget *widget, GdkEventButton *event, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GList *list;
+ GtkTreeIter iter;
+ gint type;
+ gchar *name;
+ gchar *localdir;
+
+ if (event->button == 3) {
+ remmina_ftp_client_popup_menu(client, event);
+ }else if (event->button == 1 && event->type == GDK_2BUTTON_PRESS) {
+ list = gtk_tree_selection_get_selected_rows(
+ gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->file_list_view)), NULL);
+ if (list) {
+ gtk_tree_model_get_iter(priv->file_list_sort, &iter, (GtkTreePath*)list->data);
+ gtk_tree_model_get(priv->file_list_sort, &iter, REMMINA_FTP_FILE_COLUMN_TYPE, &type,
+ REMMINA_FTP_FILE_COLUMN_NAME, &name, -1);
+ switch (type) {
+ case REMMINA_FTP_FILE_TYPE_DIR:
+ remmina_ftp_client_open_dir(client, name);
+ break;
+ case REMMINA_FTP_FILE_TYPE_LINK:
+ remmina_ftp_client_open_dir(client, name);
+ break;
+ case REMMINA_FTP_FILE_TYPE_FILE:
+ default:
+ localdir = remmina_ftp_client_get_download_dir(client);
+ if (localdir) {
+ remmina_ftp_client_download(client, &iter, localdir);
+ g_free(localdir);
+ }
+ break;
+ }
+ g_list_free(list);
+ g_free(name);
+ }
+ }
+
+ return FALSE;
+}
+
+static void remmina_ftp_client_task_list_cell_on_activate(GtkCellRenderer *renderer, gchar *path, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkTreeIter iter;
+ GtkTreePath *treepath;
+ gint taskid;
+ gboolean ret = FALSE;
+
+ treepath = gtk_tree_path_new_from_string(path);
+ gtk_tree_model_get_iter(priv->task_list_model, &iter, treepath);
+ gtk_tree_path_free(treepath);
+
+ gtk_tree_model_get(priv->task_list_model, &iter, REMMINA_FTP_TASK_COLUMN_TASKID, &taskid, -1);
+
+ g_signal_emit(G_OBJECT(client), remmina_ftp_client_signals[CANCEL_TASK_SIGNAL], 0, taskid, &ret);
+
+ if (ret) {
+ gtk_list_store_remove(GTK_LIST_STORE(priv->task_list_model), &iter);
+ }
+}
+
+static GtkWidget* remmina_ftp_client_create_toolbar(RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *box;
+ GtkWidget *button;
+ gint i = 0;
+
+ box = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
+ gtk_button_box_set_layout(GTK_BUTTON_BOX(box), GTK_BUTTONBOX_START);
+ gtk_grid_attach(GTK_GRID(client), box, 0, 0, 1, 1);
+
+ button = gtk_button_new_with_label(_("Home"));
+ gtk_widget_set_tooltip_text(button, _("Go to home folder"));
+ gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(remmina_ftp_client_action_home), client);
+
+ button = gtk_button_new_with_label(_("Up"));
+ gtk_widget_set_tooltip_text(button, _("Go to parent folder"));
+ gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(remmina_ftp_client_action_parent), client);
+
+ button = gtk_button_new_with_label(_("Refresh"));
+ gtk_widget_set_tooltip_text(button, _("Refresh current folder"));
+ gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(remmina_ftp_client_action_refresh), client);
+
+ button = gtk_button_new_with_label(_("Download"));
+ gtk_widget_set_tooltip_text(button, _("Download from server"));
+ gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(remmina_ftp_client_action_download), client);
+
+ client->priv->file_action_widgets[i++] = button;
+
+ button = gtk_button_new_with_label(_("Upload"));
+ gtk_widget_set_tooltip_text(button, _("Upload to server"));
+ gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(remmina_ftp_client_action_upload), client);
+
+ button = gtk_button_new_with_label(_("Delete"));
+ gtk_widget_set_tooltip_text(button, _("Delete files on server"));
+ gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(remmina_ftp_client_action_delete), client);
+
+ client->priv->file_action_widgets[i++] = button;
+ gtk_widget_show_all(box);
+
+ return box;
+}
+
+void remmina_ftp_client_set_show_hidden(RemminaFTPClient *client, gboolean show_hidden)
+{
+ TRACE_CALL(__func__);
+ client->priv->file_list_show_hidden = show_hidden;
+ gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(client->priv->file_list_filter));
+}
+
+static gboolean remmina_ftp_client_filter_visible_func(GtkTreeModel *model, GtkTreeIter *iter, RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ gchar *name;
+ gboolean result = TRUE;
+
+ if (client->priv->file_list_show_hidden)
+ return TRUE;
+
+ gtk_tree_model_get(model, iter, REMMINA_FTP_FILE_COLUMN_NAME, &name, -1);
+ if (name && name[0] == '.') {
+ result = FALSE;
+ }
+ g_free(name);
+ return result;
+}
+
+/* Set the overwrite_all status */
+void remmina_ftp_client_set_overwrite_status(RemminaFTPClient *client, gboolean status)
+{
+ TRACE_CALL(__func__);
+ client->priv->overwrite_all = status;
+}
+
+/* Get the overwrite_all status */
+gboolean remmina_ftp_client_get_overwrite_status(RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ return client->priv->overwrite_all;
+}
+
+/* Set the resume status */
+void remmina_ftp_client_set_resume_status(RemminaFTPClient *client, gboolean status)
+{
+ TRACE_CALL(__func__);
+ client->priv->resume_all = status;
+}
+
+/* Get the resume status */
+gboolean remmina_ftp_client_get_resume_status(RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ return client->priv->resume_all;
+}
+
+
+static void remmina_ftp_client_init(RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv;
+ GtkWidget *vpaned;
+ GtkWidget *toolbar;
+ GtkWidget *scrolledwindow;
+ GtkWidget *widget;
+ GtkCellRenderer *renderer;
+ GtkTreeViewColumn *column;
+ GtkWidget *vbox;
+
+ priv = g_new0(RemminaFTPClientPriv, 1);
+ client->priv = priv;
+
+ /* Initialize overwrite status to FALSE */
+ client->priv->overwrite_all = FALSE;
+ /* Initialize resume status to FALSE */
+ client->priv->resume_all = FALSE;
+
+ /* Main container */
+ gtk_widget_set_vexpand(GTK_WIDGET(client), TRUE);
+ gtk_widget_set_hexpand(GTK_WIDGET(client), TRUE);
+
+ /* Toolbar */
+ toolbar = remmina_ftp_client_create_toolbar(client);
+ gtk_widget_set_hexpand(toolbar, TRUE);
+
+ /* The Paned to separate File List and Task List */
+ vpaned = gtk_paned_new(GTK_ORIENTATION_VERTICAL);
+ gtk_widget_set_vexpand(GTK_WIDGET(vpaned), TRUE);
+ gtk_widget_set_hexpand(GTK_WIDGET(vpaned), TRUE);
+ gtk_widget_show(vpaned);
+ gtk_grid_attach_next_to(GTK_GRID(client), vpaned, toolbar, GTK_POS_BOTTOM, 1, 1);
+
+ priv->vpaned = vpaned;
+
+ /* Remote */
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_show(vbox);
+ gtk_paned_pack1(GTK_PANED(vpaned), vbox, TRUE, FALSE);
+
+ /* Remote Directory */
+ widget = gtk_combo_box_text_new_with_entry();
+ gtk_widget_show(widget);
+ gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(widget), "/");
+ gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, TRUE, 0);
+
+ priv->directory_combo = widget;
+
+ /* Remote File List */
+ scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_show(scrolledwindow);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
+ gtk_box_pack_start(GTK_BOX(vbox), scrolledwindow, TRUE, TRUE, 0);
+
+ widget = gtk_tree_view_new();
+ gtk_widget_show(widget);
+ gtk_container_add(GTK_CONTAINER(scrolledwindow), widget);
+
+ gtk_tree_selection_set_mode(gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)), GTK_SELECTION_MULTIPLE);
+
+ priv->file_list_view = widget;
+
+ /* Remote File List - Columns */
+ column = gtk_tree_view_column_new();
+ gtk_tree_view_column_set_title(column, _("Filename"));
+ gtk_tree_view_column_set_expand(column, TRUE);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, REMMINA_FTP_FILE_COLUMN_NAME_SORT);
+ renderer = gtk_cell_renderer_pixbuf_new();
+ gtk_tree_view_column_pack_start(column, renderer, FALSE);
+ gtk_tree_view_column_set_cell_data_func(column, renderer, remmina_ftp_client_cell_data_filetype_pixbuf, NULL, NULL);
+ renderer = gtk_cell_renderer_text_new();
+ gtk_tree_view_column_pack_start(column, renderer, FALSE);
+ gtk_tree_view_column_add_attribute(column, renderer, "text", REMMINA_FTP_FILE_COLUMN_NAME);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->file_list_view), column);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Size"), renderer, NULL);
+ gtk_tree_view_column_set_alignment(column, 1.0);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_cell_data_func(column, renderer, remmina_ftp_client_cell_data_size, NULL, NULL);
+ gtk_tree_view_column_set_sort_column_id(column, REMMINA_FTP_FILE_COLUMN_SIZE);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->file_list_view), column);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("User"), renderer, "text", REMMINA_FTP_FILE_COLUMN_USER, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, REMMINA_FTP_FILE_COLUMN_USER);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->file_list_view), column);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Group"), renderer, "text", REMMINA_FTP_FILE_COLUMN_GROUP, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, REMMINA_FTP_FILE_COLUMN_GROUP);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->file_list_view), column);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Permission"), renderer, "text", REMMINA_FTP_FILE_COLUMN_PERMISSION,
+ NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_cell_data_func(column, renderer, remmina_ftp_client_cell_data_permission, NULL, NULL);
+ gtk_tree_view_column_set_sort_column_id(column, REMMINA_FTP_FILE_COLUMN_PERMISSION);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->file_list_view), column);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Modified"), renderer, "text", REMMINA_FTP_FILE_COLUMN_MODIFIED,
+ NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_cell_data_func(column, renderer, remmina_ftp_client_cell_data_modified, NULL, NULL);
+ gtk_tree_view_column_set_sort_column_id(column, REMMINA_FTP_FILE_COLUMN_MODIFIED);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->file_list_view), column);
+
+ /* Remote File List - Model */
+ priv->file_list_model = GTK_TREE_MODEL(
+ gtk_list_store_new(REMMINA_FTP_FILE_N_COLUMNS, G_TYPE_INT, G_TYPE_STRING, G_TYPE_FLOAT, G_TYPE_STRING,
+ G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING));
+
+ priv->file_list_filter = gtk_tree_model_filter_new(priv->file_list_model, NULL);
+ gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(priv->file_list_filter),
+ (GtkTreeModelFilterVisibleFunc)remmina_ftp_client_filter_visible_func, client, NULL);
+
+ priv->file_list_sort = gtk_tree_model_sort_new_with_model(priv->file_list_filter);
+ gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(priv->file_list_sort), REMMINA_FTP_FILE_COLUMN_NAME_SORT,
+ GTK_SORT_ASCENDING);
+ gtk_tree_view_set_model(GTK_TREE_VIEW(priv->file_list_view), priv->file_list_sort);
+
+ /* Task List */
+ scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_show(scrolledwindow);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
+ gtk_paned_pack2(GTK_PANED(vpaned), scrolledwindow, FALSE, TRUE);
+
+ widget = gtk_tree_view_new();
+ gtk_widget_show(widget);
+ gtk_container_add(GTK_CONTAINER(scrolledwindow), widget);
+ g_object_set(widget, "has-tooltip", TRUE, NULL);
+
+ priv->task_list_view = widget;
+
+ /* Task List - Columns */
+ column = gtk_tree_view_column_new();
+ gtk_tree_view_column_set_title(column, _("Filename"));
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_expand(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, REMMINA_FTP_TASK_COLUMN_NAME);
+ renderer = gtk_cell_renderer_pixbuf_new();
+ gtk_tree_view_column_pack_start(column, renderer, FALSE);
+ gtk_tree_view_column_set_cell_data_func(column, renderer, remmina_ftp_client_cell_data_progress_pixbuf, NULL, NULL);
+ renderer = gtk_cell_renderer_pixbuf_new();
+ gtk_tree_view_column_pack_start(column, renderer, FALSE);
+ gtk_tree_view_column_set_cell_data_func(column, renderer, remmina_ftp_client_cell_data_filetype_pixbuf, NULL, NULL);
+ renderer = gtk_cell_renderer_text_new();
+ gtk_tree_view_column_pack_start(column, renderer, FALSE);
+ gtk_tree_view_column_add_attribute(column, renderer, "text", REMMINA_FTP_FILE_COLUMN_NAME);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->task_list_view), column);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Remote"), renderer, "text", REMMINA_FTP_TASK_COLUMN_REMOTEDIR,
+ NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, REMMINA_FTP_TASK_COLUMN_REMOTEDIR);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->task_list_view), column);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Local"), renderer, "text", REMMINA_FTP_TASK_COLUMN_LOCALDIR, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, REMMINA_FTP_TASK_COLUMN_LOCALDIR);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->task_list_view), column);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Size"), renderer, NULL);
+ gtk_tree_view_column_set_alignment(column, 1.0);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_cell_data_func(column, renderer, remmina_ftp_client_cell_data_size_progress, NULL, NULL);
+ gtk_tree_view_column_set_sort_column_id(column, REMMINA_FTP_TASK_COLUMN_SIZE);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->task_list_view), column);
+
+ renderer = gtk_cell_renderer_progress_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Progress"), renderer, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_cell_data_func(column, renderer, remmina_ftp_client_cell_data_progress, NULL, NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->task_list_view), column);
+
+ renderer = remmina_cell_renderer_pixbuf_new();
+ column = gtk_tree_view_column_new_with_attributes(NULL, renderer, NULL);
+ g_object_set(G_OBJECT(renderer), "icon-name", "process-stop", NULL);
+ gtk_tree_view_column_set_resizable(column, FALSE);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(priv->task_list_view), column);
+
+ g_signal_connect(G_OBJECT(renderer), "activate", G_CALLBACK(remmina_ftp_client_task_list_cell_on_activate), client);
+
+ /* Task List - Model */
+ priv->task_list_model = GTK_TREE_MODEL(
+ gtk_list_store_new(REMMINA_FTP_TASK_N_COLUMNS, G_TYPE_INT, G_TYPE_STRING, G_TYPE_FLOAT, G_TYPE_INT,
+ G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT, G_TYPE_FLOAT, G_TYPE_STRING));
+ gtk_tree_view_set_model(GTK_TREE_VIEW(priv->task_list_view), priv->task_list_model);
+
+ /* Setup the internal signals */
+ g_signal_connect(G_OBJECT(client), "destroy", G_CALLBACK(remmina_ftp_client_destroy), NULL);
+ g_signal_connect(G_OBJECT(gtk_bin_get_child(GTK_BIN(priv->directory_combo))), "activate",
+ G_CALLBACK(remmina_ftp_client_dir_on_activate), client);
+ g_signal_connect(G_OBJECT(priv->directory_combo), "changed", G_CALLBACK(remmina_ftp_client_dir_on_changed), client);
+ g_signal_connect(G_OBJECT(priv->file_list_view), "button-press-event",
+ G_CALLBACK(remmina_ftp_client_file_list_on_button_press), client);
+ g_signal_connect(G_OBJECT(gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->file_list_view))), "changed",
+ G_CALLBACK(remmina_ftp_client_file_selection_on_changed), client);
+ g_signal_connect(G_OBJECT(priv->task_list_view), "query-tooltip",
+ G_CALLBACK(remmina_ftp_client_task_list_on_query_tooltip), client);
+}
+
+GtkWidget*
+remmina_ftp_client_new(void)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClient *client;
+
+ client = REMMINA_FTP_CLIENT(g_object_new(REMMINA_TYPE_FTP_CLIENT, NULL));
+
+ return GTK_WIDGET(client);
+}
+
+void remmina_ftp_client_save_state(RemminaFTPClient *client, RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ gint pos;
+
+ pos = gtk_paned_get_position(GTK_PANED(client->priv->vpaned));
+ remmina_file_set_int(remminafile, "ftp_vpanedpos", pos);
+}
+
+void remmina_ftp_client_load_state(RemminaFTPClient *client, RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ gint pos;
+ GtkAllocation a;
+
+ pos = remmina_file_get_int(remminafile, "ftp_vpanedpos", 0);
+ if (pos) {
+ gtk_widget_get_allocation(client->priv->vpaned, &a);
+ if (a.height > 0 && pos > a.height - 60) {
+ pos = a.height - 60;
+ }
+ gtk_paned_set_position(GTK_PANED(client->priv->vpaned), pos);
+ }
+}
+
+void remmina_ftp_client_clear_file_list(RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+
+ gtk_list_store_clear(GTK_LIST_STORE(priv->file_list_model));
+ remmina_ftp_client_set_file_action_sensitive(client, FALSE);
+}
+
+void remmina_ftp_client_add_file(RemminaFTPClient *client, ...)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkListStore *store = GTK_LIST_STORE(priv->file_list_model);
+ GtkTreeIter iter;
+ va_list args;
+ gint type;
+ gchar *name;
+ gchar *ptr;
+
+ va_start(args, client);
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set_valist(store, &iter, args);
+ va_end(args);
+
+ gtk_tree_model_get(GTK_TREE_MODEL(store), &iter,
+ REMMINA_FTP_FILE_COLUMN_TYPE, &type,
+ REMMINA_FTP_FILE_COLUMN_NAME, &name,
+ -1);
+
+ ptr = g_strdup_printf("%i%s", type, name);
+ gtk_list_store_set(store, &iter, REMMINA_FTP_FILE_COLUMN_NAME_SORT, ptr, -1);
+ g_free(ptr);
+ g_free(name);
+}
+
+void remmina_ftp_client_set_dir(RemminaFTPClient *client, const gchar *dir)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ gboolean ret;
+ gchar *t;
+
+ if (priv->current_directory && g_strcmp0(priv->current_directory, dir) == 0)
+ return;
+
+ model = gtk_combo_box_get_model(GTK_COMBO_BOX(priv->directory_combo));
+ for (ret = gtk_tree_model_get_iter_first(model, &iter); ret; ret = gtk_tree_model_iter_next(model, &iter)) {
+ gtk_tree_model_get(model, &iter, 0, &t, -1);
+ if (g_strcmp0(t, dir) == 0) {
+ gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
+ g_free(t);
+ break;
+ }
+ g_free(t);
+ }
+
+ gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(priv->directory_combo), dir);
+ gtk_combo_box_set_active(GTK_COMBO_BOX(priv->directory_combo), 0);
+
+ g_free(priv->current_directory);
+ priv->current_directory = g_strdup(dir);
+}
+
+gchar*
+remmina_ftp_client_get_dir(RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+
+ return g_strdup(priv->current_directory);
+}
+
+RemminaFTPTask*
+remmina_ftp_client_get_waiting_task(RemminaFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkTreePath *path;
+ GtkTreeIter iter;
+ RemminaFTPTask task;
+
+ if ( !remmina_masterthread_exec_is_main_thread() ) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ RemminaFTPTask* retval;
+ d = (RemminaMTExecData*)g_malloc( sizeof(RemminaMTExecData) );
+ d->func = FUNC_FTP_CLIENT_GET_WAITING_TASK;
+ d->p.ftp_client_get_waiting_task.client = client;
+ remmina_masterthread_exec_and_wait(d);
+ retval = d->p.ftp_client_get_waiting_task.retval;
+ g_free(d);
+ return retval;
+ }
+
+ if (!gtk_tree_model_get_iter_first(priv->task_list_model, &iter))
+ return NULL;
+
+ while (1) {
+ gtk_tree_model_get(priv->task_list_model, &iter, REMMINA_FTP_TASK_COLUMN_TYPE, &task.type,
+ REMMINA_FTP_TASK_COLUMN_NAME, &task.name, REMMINA_FTP_TASK_COLUMN_SIZE, &task.size,
+ REMMINA_FTP_TASK_COLUMN_TASKID, &task.taskid, REMMINA_FTP_TASK_COLUMN_TASKTYPE, &task.tasktype,
+ REMMINA_FTP_TASK_COLUMN_REMOTEDIR, &task.remotedir, REMMINA_FTP_TASK_COLUMN_LOCALDIR,
+ &task.localdir, REMMINA_FTP_TASK_COLUMN_STATUS, &task.status, REMMINA_FTP_TASK_COLUMN_DONESIZE,
+ &task.donesize, REMMINA_FTP_TASK_COLUMN_TOOLTIP, &task.tooltip, -1);
+ if (task.status == REMMINA_FTP_TASK_STATUS_WAIT) {
+ path = gtk_tree_model_get_path(priv->task_list_model, &iter);
+ task.rowref = gtk_tree_row_reference_new(priv->task_list_model, path);
+ gtk_tree_path_free(path);
+#if GLIB_CHECK_VERSION(2,68,0)
+ return (RemminaFTPTask*)g_memdup2(&task, sizeof(RemminaFTPTask));
+#else
+ return (RemminaFTPTask*)g_memdup(&task, sizeof(RemminaFTPTask));
+#endif
+ }
+ if (!gtk_tree_model_iter_next(priv->task_list_model, &iter))
+ break;
+ }
+
+ return NULL;
+}
+
+void remmina_ftp_client_update_task(RemminaFTPClient *client, RemminaFTPTask* task)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPClientPriv *priv = (RemminaFTPClientPriv*)client->priv;
+ GtkListStore *store = GTK_LIST_STORE(priv->task_list_model);
+ GtkTreePath *path;
+ GtkTreeIter iter;
+
+ if ( !remmina_masterthread_exec_is_main_thread() ) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ d = (RemminaMTExecData*)g_malloc( sizeof(RemminaMTExecData) );
+ d->func = FUNC_FTP_CLIENT_UPDATE_TASK;
+ d->p.ftp_client_update_task.client = client;
+ d->p.ftp_client_update_task.task = task;
+ remmina_masterthread_exec_and_wait(d);
+ g_free(d);
+ return;
+ }
+
+
+
+ path = gtk_tree_row_reference_get_path(task->rowref);
+ if (path == NULL)
+ return;
+ gtk_tree_model_get_iter(priv->task_list_model, &iter, path);
+ gtk_tree_path_free(path);
+ gtk_list_store_set(store, &iter, REMMINA_FTP_TASK_COLUMN_SIZE, task->size, REMMINA_FTP_TASK_COLUMN_STATUS, task->status,
+ REMMINA_FTP_TASK_COLUMN_DONESIZE, task->donesize, REMMINA_FTP_TASK_COLUMN_TOOLTIP, task->tooltip, -1);
+}
+
+void remmina_ftp_task_free(RemminaFTPTask *task)
+{
+ TRACE_CALL(__func__);
+ if (task) {
+ g_free(task->name);
+ g_free(task->remotedir);
+ g_free(task->localdir);
+ g_free(task->tooltip);
+ g_free(task);
+ }
+}
+
diff --git a/src/remmina_ftp_client.h b/src/remmina_ftp_client.h
new file mode 100644
index 0000000..0b73c91
--- /dev/null
+++ b/src/remmina_ftp_client.h
@@ -0,0 +1,158 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 <gtk/gtk.h>
+#include "remmina_file.h"
+
+G_BEGIN_DECLS
+
+#define REMMINA_TYPE_FTP_CLIENT (remmina_ftp_client_get_type())
+#define REMMINA_FTP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_FTP_CLIENT, RemminaFTPClient))
+#define REMMINA_FTP_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_FTP_CLIENT, RemminaFTPClientClass))
+#define REMMINA_IS_FTP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_FTP_CLIENT))
+#define REMMINA_IS_FTP_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_FTP_CLIENT))
+#define REMMINA_FTP_CLIENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_FTP_CLIENT, RemminaFTPClientClass))
+
+typedef struct _RemminaFTPClientPriv RemminaFTPClientPriv;
+
+typedef struct _RemminaFTPClient {
+ GtkBox vbox;
+
+ RemminaFTPClientPriv * priv;
+} RemminaFTPClient;
+
+typedef struct _RemminaFTPClientClass {
+ GtkBoxClass parent_class;
+
+ void (*open_dir)(RemminaFTPClient *client);
+ void (*new_task)(RemminaFTPClient *client);
+ void (*cancel_task)(RemminaFTPClient *client);
+ void (*delete_file)(RemminaFTPClient *client);
+} RemminaFTPClientClass;
+
+GType remmina_ftp_client_get_type(void)
+G_GNUC_CONST;
+
+enum {
+ REMMINA_FTP_FILE_TYPE_DIR,
+ REMMINA_FTP_FILE_TYPE_FILE,
+ REMMINA_FTP_FILE_TYPE_LINK,
+ REMMINA_FTP_FILE_N_TYPES,
+};
+
+enum {
+ REMMINA_FTP_FILE_COLUMN_TYPE,
+ REMMINA_FTP_FILE_COLUMN_NAME,
+ REMMINA_FTP_FILE_COLUMN_SIZE,
+ REMMINA_FTP_FILE_COLUMN_USER,
+ REMMINA_FTP_FILE_COLUMN_GROUP,
+ REMMINA_FTP_FILE_COLUMN_PERMISSION,
+ REMMINA_FTP_FILE_COLUMN_MODIFIED,
+ REMMINA_FTP_FILE_COLUMN_NAME_SORT, /* Auto populate */
+ REMMINA_FTP_FILE_N_COLUMNS
+};
+
+enum {
+ REMMINA_FTP_TASK_TYPE_DOWNLOAD, REMMINA_FTP_TASK_TYPE_UPLOAD, REMMINA_FTP_TASK_N_TYPES
+};
+
+enum {
+ REMMINA_FTP_TASK_STATUS_WAIT,
+ REMMINA_FTP_TASK_STATUS_RUN,
+ REMMINA_FTP_TASK_STATUS_FINISH,
+ REMMINA_FTP_TASK_STATUS_ERROR,
+ REMMINA_FTP_TASK_N_STATUSES
+};
+
+enum {
+ REMMINA_FTP_TASK_COLUMN_TYPE,
+ REMMINA_FTP_TASK_COLUMN_NAME,
+ REMMINA_FTP_TASK_COLUMN_SIZE,
+ REMMINA_FTP_TASK_COLUMN_TASKID,
+ REMMINA_FTP_TASK_COLUMN_TASKTYPE,
+ REMMINA_FTP_TASK_COLUMN_REMOTEDIR,
+ REMMINA_FTP_TASK_COLUMN_LOCALDIR,
+ REMMINA_FTP_TASK_COLUMN_STATUS,
+ REMMINA_FTP_TASK_COLUMN_DONESIZE,
+ REMMINA_FTP_TASK_COLUMN_TOOLTIP,
+ REMMINA_FTP_TASK_N_COLUMNS
+};
+
+typedef struct _RemminaFTPTask {
+ /* Read-only */
+ gint type;
+ gchar * name;
+ gint taskid;
+ gint tasktype;
+ gchar * remotedir;
+ gchar * localdir;
+ GtkTreeRowReference * rowref;
+ /* Updatable */
+ gfloat size;
+ gint status;
+ gfloat donesize;
+ gchar * tooltip;
+} RemminaFTPTask;
+
+GtkWidget *remmina_ftp_client_new(void);
+
+void remmina_ftp_client_save_state(RemminaFTPClient *client, RemminaFile *remminafile);
+void remmina_ftp_client_load_state(RemminaFTPClient *client, RemminaFile *remminafile);
+
+void remmina_ftp_client_set_show_hidden(RemminaFTPClient *client, gboolean show_hidden);
+void remmina_ftp_client_clear_file_list(RemminaFTPClient *client);
+/* column, value, …, -1 */
+void remmina_ftp_client_add_file(RemminaFTPClient *client, ...);
+/* Set the current directory. Should be called by opendir signal handler */
+void remmina_ftp_client_set_dir(RemminaFTPClient *client, const gchar *dir);
+/* Get the current directory as newly allocated string */
+gchar *remmina_ftp_client_get_dir(RemminaFTPClient *client);
+/* Get the next waiting task */
+RemminaFTPTask *remmina_ftp_client_get_waiting_task(RemminaFTPClient *client);
+/* Update the task */
+void remmina_ftp_client_update_task(RemminaFTPClient *client, RemminaFTPTask *task);
+/* Free the RemminaFTPTask object */
+void remmina_ftp_task_free(RemminaFTPTask *task);
+/* Get/Set Set overwrite_all status */
+void remmina_ftp_client_set_overwrite_status(RemminaFTPClient *client, gboolean status);
+gboolean remmina_ftp_client_get_overwrite_status(RemminaFTPClient *client);
+/* Get/Set Set resume_all status */
+void remmina_ftp_client_set_resume_status(RemminaFTPClient *client, gboolean status);
+gboolean remmina_ftp_client_get_resume_status(RemminaFTPClient *client);
+
+G_END_DECLS
diff --git a/src/remmina_icon.c b/src/remmina_icon.c
new file mode 100644
index 0000000..15d98ac
--- /dev/null
+++ b/src/remmina_icon.c
@@ -0,0 +1,491 @@
+/*
+ * 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 "config.h"
+
+#include <glib/gi18n.h>
+
+#include "remmina_icon.h"
+
+#ifdef HAVE_LIBAPPINDICATOR
+# ifdef HAVE_AYATANA_LIBAPPINDICATOR
+# include <libayatana-appindicator/app-indicator.h>
+# else
+# include <libappindicator/app-indicator.h>
+# endif
+#include "remmina_widget_pool.h"
+#include "remmina_pref.h"
+#include "remmina_exec.h"
+#ifdef HAVE_LIBAVAHI_CLIENT
+#include "remmina_avahi.h"
+#endif
+#include "remmina_applet_menu_item.h"
+#include "remmina_applet_menu.h"
+#include "rcw.h"
+#include "remmina_log.h"
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_sysinfo.h"
+
+typedef struct _RemminaIcon {
+ AppIndicator * icon;
+ gboolean indicator_connected;
+#ifdef HAVE_LIBAVAHI_CLIENT
+ RemminaAvahi * avahi;
+#endif
+ guint32 popup_time;
+ gchar * autostart_file;
+} RemminaIcon;
+
+static RemminaIcon remmina_icon =
+{ 0 };
+
+void remmina_icon_destroy(void)
+{
+ TRACE_CALL(__func__);
+ if (remmina_icon.icon) {
+ app_indicator_set_status(remmina_icon.icon, APP_INDICATOR_STATUS_PASSIVE);
+ remmina_icon.icon = NULL;
+ }
+#ifdef HAVE_LIBAVAHI_CLIENT
+ if (remmina_icon.avahi) {
+ remmina_avahi_free(remmina_icon.avahi);
+ remmina_icon.avahi = NULL;
+ }
+#endif
+ if (remmina_icon.autostart_file) {
+ g_free(remmina_icon.autostart_file);
+ remmina_icon.autostart_file = NULL;
+ }
+}
+
+static void remmina_icon_main(void)
+{
+ TRACE_CALL(__func__);
+ remmina_exec_command(REMMINA_COMMAND_MAIN, NULL);
+}
+
+static void remmina_icon_preferences(void)
+{
+ TRACE_CALL(__func__);
+ remmina_exec_command(REMMINA_COMMAND_PREF, "2");
+}
+
+static void remmina_icon_about(void)
+{
+ TRACE_CALL(__func__);
+ remmina_exec_command(REMMINA_COMMAND_ABOUT, NULL);
+}
+
+#ifdef HAVE_LIBAVAHI_CLIENT
+static void remmina_icon_enable_avahi(GtkCheckMenuItem *checkmenuitem, gpointer data)
+{
+ TRACE_CALL(__func__);
+ if (!remmina_icon.avahi)
+ return;
+
+ if (gtk_check_menu_item_get_active(checkmenuitem)) {
+ remmina_pref.applet_enable_avahi = TRUE;
+ if (!remmina_icon.avahi->started)
+ remmina_avahi_start(remmina_icon.avahi);
+ } else {
+ remmina_pref.applet_enable_avahi = FALSE;
+ remmina_avahi_stop(remmina_icon.avahi);
+ }
+ remmina_pref_save();
+}
+#endif
+
+static void remmina_icon_populate_additional_menu_item(GtkWidget *menu)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *menuitem;
+
+
+#ifdef HAVE_LIBAVAHI_CLIENT
+
+ menuitem = gtk_separator_menu_item_new();
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem);
+
+ menuitem = gtk_check_menu_item_new_with_label(_("Enable Service Discovery"));
+ if (remmina_pref.applet_enable_avahi)
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE);
+ gtk_widget_show(menuitem);
+ g_signal_connect(G_OBJECT(menuitem), "toggled", G_CALLBACK(remmina_icon_enable_avahi), NULL);
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem);
+
+
+#endif
+
+ menuitem = gtk_separator_menu_item_new();
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_mnemonic(_("_Quit"));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem);
+ g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(remmina_exec_exitremmina_one_confirm), NULL);
+
+ menuitem = gtk_menu_item_new_with_mnemonic(_("_About"));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem);
+ g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(remmina_icon_about), NULL);
+
+ menuitem = gtk_menu_item_new_with_mnemonic(_("_Preferences"));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem);
+ g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(remmina_icon_preferences), NULL);
+
+ menuitem = gtk_menu_item_new_with_label(_("Open Main Window"));
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem);
+ g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(remmina_icon_main), NULL);
+
+
+}
+
+static void remmina_icon_on_launch_item(RemminaAppletMenu *menu, RemminaAppletMenuItem *menuitem, gpointer data)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+
+ switch (menuitem->item_type) {
+ case REMMINA_APPLET_MENU_ITEM_NEW:
+ remmina_exec_command(REMMINA_COMMAND_NEW, NULL);
+ break;
+ case REMMINA_APPLET_MENU_ITEM_FILE:
+ remmina_exec_command(REMMINA_COMMAND_CONNECT, menuitem->filename);
+ break;
+ case REMMINA_APPLET_MENU_ITEM_DISCOVERED:
+ s = g_strdup_printf("%s,%s", menuitem->protocol, menuitem->name);
+ remmina_exec_command(REMMINA_COMMAND_NEW, s);
+ g_free(s);
+ break;
+ }
+}
+
+static void remmina_icon_on_edit_item(RemminaAppletMenu *menu, RemminaAppletMenuItem *menuitem, gpointer data)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+
+ switch (menuitem->item_type) {
+ case REMMINA_APPLET_MENU_ITEM_NEW:
+ remmina_exec_command(REMMINA_COMMAND_NEW, NULL);
+ break;
+ case REMMINA_APPLET_MENU_ITEM_FILE:
+ remmina_exec_command(REMMINA_COMMAND_EDIT, menuitem->filename);
+ break;
+ case REMMINA_APPLET_MENU_ITEM_DISCOVERED:
+ s = g_strdup_printf("%s,%s", menuitem->protocol, menuitem->name);
+ remmina_exec_command(REMMINA_COMMAND_NEW, s);
+ g_free(s);
+ break;
+ }
+}
+
+static void remmina_icon_populate_extra_menu_item(GtkWidget *menu)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *menuitem;
+ gboolean new_ontop;
+
+ new_ontop = remmina_pref.applet_new_ontop;
+
+#ifdef HAVE_LIBAVAHI_CLIENT
+ GHashTableIter iter;
+ gchar *tmp;
+ /* Iterate all discovered services from Avahi */
+ if (remmina_icon.avahi) {
+ g_hash_table_iter_init(&iter, remmina_icon.avahi->discovered_services);
+ while (g_hash_table_iter_next(&iter, NULL, (gpointer *)&tmp)) {
+ menuitem = remmina_applet_menu_item_new(REMMINA_APPLET_MENU_ITEM_DISCOVERED, tmp);
+ gtk_widget_show(menuitem);
+ remmina_applet_menu_add_item(REMMINA_APPLET_MENU(menu), REMMINA_APPLET_MENU_ITEM(menuitem));
+ }
+ }
+#endif
+
+ /* New Connection */
+ menuitem = remmina_applet_menu_item_new(REMMINA_APPLET_MENU_ITEM_NEW);
+ gtk_widget_show(menuitem);
+ remmina_applet_menu_register_item(REMMINA_APPLET_MENU(menu), REMMINA_APPLET_MENU_ITEM(menuitem));
+ if (new_ontop)
+ gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), menuitem);
+ else
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+
+ g_signal_connect(G_OBJECT(menu), "launch-item", G_CALLBACK(remmina_icon_on_launch_item), NULL);
+ g_signal_connect(G_OBJECT(menu), "edit-item", G_CALLBACK(remmina_icon_on_edit_item), NULL);
+}
+
+void
+remmina_icon_populate_menu(void)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *menu;
+ GtkWidget *menuitem;
+
+ if (remmina_icon.icon && !remmina_pref.disable_tray_icon) {
+ menu = remmina_applet_menu_new();
+ app_indicator_set_menu(remmina_icon.icon, GTK_MENU(menu));
+
+ remmina_applet_menu_set_hide_count(REMMINA_APPLET_MENU(menu), remmina_pref.applet_hide_count);
+ remmina_applet_menu_populate(REMMINA_APPLET_MENU(menu));
+
+
+ menuitem = gtk_separator_menu_item_new();
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
+
+ remmina_icon_populate_additional_menu_item(menu);
+ remmina_icon_populate_extra_menu_item(menu);
+ }
+}
+
+static void remmina_icon_save_autostart_file(GKeyFile *gkeyfile)
+{
+ TRACE_CALL(__func__);
+ gchar *content;
+ gsize length;
+
+ content = g_key_file_to_data(gkeyfile, &length, NULL);
+ if (remmina_icon.autostart_file != NULL) {
+ g_file_set_contents(remmina_icon.autostart_file, content, length, NULL);
+ }
+ else {
+ REMMINA_WARNING("Cannot save remmina icon autostart file. Uncheck Preferences -> Applet -> No Tray Icon to recreate it.");
+ }
+ g_free(content);
+}
+
+static void remmina_icon_create_autostart_file(void)
+{
+ TRACE_CALL(__func__);
+ if (g_file_test(remmina_icon.autostart_file, G_FILE_TEST_EXISTS))
+ return;
+
+ GKeyFile *gkeyfile;
+
+ gkeyfile = g_key_file_new();
+ g_key_file_set_string(gkeyfile, "Desktop Entry", "Version", "1.0");
+ // TRANSLATORS: Applet name as per the Freedesktop Desktop entry specification https://specifications.freedesktop.org/desktop-entry-spec/latest/
+ g_key_file_set_string(gkeyfile, "Desktop Entry", "Name", _("Remmina Applet"));
+ // TRANSLATORS: Applet comment/description as per the Freedesktop Desktop entry specification https://specifications.freedesktop.org/desktop-entry-spec/latest/
+ g_key_file_set_string(gkeyfile, "Desktop Entry", "Comment", _("Connect to remote desktops through the applet menu"));
+ g_key_file_set_string(gkeyfile, "Desktop Entry", "Icon", REMMINA_APP_ID);
+ if (getenv("FLATPAK_ID")){
+ g_key_file_set_string(gkeyfile, "Desktop Entry", "Exec", "flatpak run org.remmina.Remmina -i");
+ }
+ else{
+ g_key_file_set_string(gkeyfile, "Desktop Entry", "Exec", "remmina -i");
+ }
+ g_key_file_set_boolean(gkeyfile, "Desktop Entry", "Terminal", FALSE);
+ g_key_file_set_string(gkeyfile, "Desktop Entry", "Type", "Application");
+ g_key_file_set_boolean(gkeyfile, "Desktop Entry", "Hidden", FALSE);
+ remmina_icon_save_autostart_file(gkeyfile);
+ g_key_file_free(gkeyfile);
+}
+
+/**
+ * Determine whenever the Remmina icon is available.
+ * Return TRUE if a remmina_icon (status indicator/systray menu) is
+ * available and shown to the user, so the user can continue
+ * its work without the remmina main window.
+ * @return TRUE if the Remmina icon is available.
+ */
+gboolean remmina_icon_is_available(void)
+{
+ TRACE_CALL(__func__);
+
+ if (!remmina_icon.icon)
+ return FALSE;
+ if (remmina_pref.disable_tray_icon)
+ return FALSE;
+
+ if (remmina_icon.indicator_connected == FALSE) {
+ REMMINA_DEBUG("Indicator is not connected to panel, thus it cannot be displayed.");
+ return FALSE;
+ } else {
+ REMMINA_DEBUG("Indicator is connected to panel, thus it can be displayed.");
+ return TRUE;
+ }
+ /** Special treatment under GNOME Shell
+ * Remmina > v1.4.18 won't be shipped in distributions with GNOME Shell <= 3.18
+ * therefore checking the the GNOME Shell version is useless.
+ * We just return TRUE
+ */
+ return TRUE;
+}
+
+static void
+remmina_icon_connection_changed_cb(AppIndicator *indicator, gboolean connected, gpointer data)
+{
+ TRACE_CALL(__func__);
+ REMMINA_DEBUG("Indicator connection changed to: %d", connected);
+ remmina_icon.indicator_connected = connected;
+}
+
+void remmina_icon_init(void)
+{
+ TRACE_CALL(__func__);
+
+ gchar remmina_panel[29];
+ gboolean sni_supported;
+
+ g_stpcpy(remmina_panel, "org.remmina.Remmina-status");
+
+ /* Print on stdout the availability of appindicators on DBUS */
+ sni_supported = remmina_sysinfo_is_appindicator_available();
+
+ g_autofree gchar *wmname = g_ascii_strdown(remmina_sysinfo_get_wm_name(), -1);
+ //TRANSLATORS: These are Linux desktop components to show icons in the system tray, after the “ there's the Desktop Name (like GNOME).
+ g_autofree gchar *msg = g_strconcat(
+ _("StatusNotifier/Appindicator support in “"),
+ wmname,
+ "”:",
+ NULL);
+
+ if (sni_supported) {
+ //TRANSLATORS: %s is a placeholder for "StatusNotifier/Appindicator suppor in “DESKTOP NAME”: "
+ REMMINA_INFO(_("%s your desktop does support it"), msg);
+ //TRANSLATORS: %s is a placeholder for "StatusNotifier/Appindicator suppor in “DESKTOP NAME”: "
+ REMMINA_INFO(_("%s and Remmina has built-in (compiled) support for libappindicator."), msg);
+ } else {
+ //TRANSLATORS: %s is a placeholder for "StatusNotifier/Appindicator suppor in “DESKTOP NAME”: "
+ REMMINA_INFO(_("%s not supported natively by your Desktop Environment. libappindicator will try to fallback to GtkStatusIcon/xembed"), msg);
+ }
+ if (g_strrstr(wmname, "mate") != NULL)
+ //TRANSLATORS: %s is a placeholder for "StatusNotifier/Appindicator suppor in “DESKTOP NAME”: "
+ REMMINA_INFO(_("%s You may need to install, and use XApp Status Applet"), msg);
+ if (g_strrstr(wmname, "kde") != NULL)
+ //TRANSLATORS: %s is a placeholder for "StatusNotifier/Appindicator suppor in “DESKTOP NAME”: "
+ REMMINA_INFO(_("%s You may need to install, and use KStatusNotifierItem"), msg);
+ if (g_strrstr(wmname, "plasma") != NULL)
+ //TRANSLATORS: %s is a placeholder for "StatusNotifier/Appindicator suppor in “DESKTOP NAME”: "
+ REMMINA_INFO(_("%s You may need to install, and use XEmbed SNI Proxy"), msg);
+ if (g_strrstr(wmname, "gnome") != NULL)
+ //TRANSLATORS: %s is a placeholder for "StatusNotifier/Appindicator suppor in “DESKTOP NAME”: "
+ REMMINA_INFO(_("%s You may need to install, and use Gnome Shell Extension Appindicator"), msg);
+
+ if (!remmina_icon.icon && !remmina_pref.disable_tray_icon) {
+ remmina_icon.icon = app_indicator_new("remmina-icon", remmina_panel, APP_INDICATOR_CATEGORY_APPLICATION_STATUS);
+ app_indicator_set_status(remmina_icon.icon, APP_INDICATOR_STATUS_ACTIVE);
+ app_indicator_set_title(remmina_icon.icon, "Remmina");
+ remmina_icon_populate_menu();
+ } else if (remmina_icon.icon) {
+ app_indicator_set_status(remmina_icon.icon, remmina_pref.disable_tray_icon ?
+ APP_INDICATOR_STATUS_PASSIVE : APP_INDICATOR_STATUS_ACTIVE);
+ /* With libappindicator we can also change the icon on the fly */
+ app_indicator_set_icon(remmina_icon.icon, remmina_panel);
+ }
+ remmina_icon.indicator_connected = TRUE;
+#ifdef HAVE_LIBAVAHI_CLIENT
+ if (!remmina_icon.avahi)
+ remmina_icon.avahi = remmina_avahi_new();
+ if (remmina_icon.avahi) {
+ if (remmina_pref.applet_enable_avahi) {
+ if (!remmina_icon.avahi->started)
+ remmina_avahi_start(remmina_icon.avahi);
+ } else {
+ remmina_avahi_stop(remmina_icon.avahi);
+ }
+ }
+#endif
+ if (!remmina_icon.autostart_file && !remmina_pref.disable_tray_icon) {
+ remmina_icon.autostart_file = g_strdup_printf("%s/.config/autostart/remmina-applet.desktop", g_get_home_dir());
+ remmina_icon_create_autostart_file();
+ }
+ // "connected" property means a visible indicator, otherwise could be hidden. or fall back to GtkStatusIcon
+ if (remmina_icon.icon)
+ g_signal_connect(G_OBJECT(remmina_icon.icon), "connection-changed", G_CALLBACK(remmina_icon_connection_changed_cb), NULL);
+ //g_object_get(G_OBJECT(remmina_icon.icon), "connected", &remmina_icon.indicator_connected, NULL);
+}
+
+gboolean remmina_icon_is_autostart(void)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+ gboolean b;
+
+ gkeyfile = g_key_file_new();
+
+ if (remmina_icon.autostart_file != NULL) {
+ g_key_file_load_from_file(gkeyfile, remmina_icon.autostart_file, G_KEY_FILE_NONE, NULL);
+ }
+ else {
+ REMMINA_WARNING("Cannot load remmina icon autostart file. Uncheck Preferences -> Applet -> No Tray Icon to recreate it.");
+ }
+
+ b = !g_key_file_get_boolean(gkeyfile, "Desktop Entry", "Hidden", NULL);
+ g_key_file_free(gkeyfile);
+ return b;
+}
+
+void remmina_icon_set_autostart(gboolean autostart)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+ gboolean b;
+
+ if (remmina_icon.autostart_file != NULL) {
+ gkeyfile = g_key_file_new();
+ g_key_file_load_from_file(gkeyfile, remmina_icon.autostart_file, G_KEY_FILE_NONE, NULL);
+ b = !g_key_file_get_boolean(gkeyfile, "Desktop Entry", "Hidden", NULL);
+ if (b != autostart) {
+ g_key_file_set_boolean(gkeyfile, "Desktop Entry", "Hidden", !autostart);
+ /* Refresh it in case translation is updated */
+ // TRANSLATORS: Applet Name as per the Freedesktop Desktop entry specification https://specifications.freedesktop.org/desktop-entry-spec/latest/
+ g_key_file_set_string(gkeyfile, "Desktop Entry", "Name", _("Remmina Applet"));
+ // TRANSLATORS: Applet comment/description as per the Freedesktop Desktop entry specification https://specifications.freedesktop.org/desktop-entry-spec/latest/
+ g_key_file_set_string(gkeyfile, "Desktop Entry", "Comment", _("Connect to remote desktops through the applet menu"));
+ remmina_icon_save_autostart_file(gkeyfile);
+ g_key_file_free(gkeyfile);
+ }
+ }
+ else {
+ REMMINA_WARNING("Cannot load remmina icon autostart file. Uncheck Preferences -> Applet -> No Tray Icon to recreate it.");
+ }
+}
+
+#else
+void remmina_icon_init(void) {};
+void remmina_icon_destroy(void) {};
+gboolean remmina_icon_is_available(void) {return FALSE;};
+void remmina_icon_populate_menu(void) {};
+void remmina_icon_set_autostart(gboolean autostart) {} ;
+gboolean remmina_icon_is_autostart(void) {return FALSE;};
+#endif
diff --git a/src/remmina_icon.h b/src/remmina_icon.h
new file mode 100644
index 0000000..9de1836
--- /dev/null
+++ b/src/remmina_icon.h
@@ -0,0 +1,49 @@
+/*
+ * 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
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+void remmina_icon_init(void);
+gboolean remmina_icon_is_autostart(void);
+void remmina_icon_set_autostart(gboolean autostart);
+void remmina_icon_populate_menu(void);
+void remmina_icon_destroy(void);
+gboolean remmina_icon_is_available(void);
+
+G_END_DECLS
diff --git a/src/remmina_info.c b/src/remmina_info.c
new file mode 100644
index 0000000..704ef3a
--- /dev/null
+++ b/src/remmina_info.c
@@ -0,0 +1,1328 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-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.
+ *
+ */
+
+/**
+ * @file remmina_info.c
+ * @brief Remmina usage statistics module.
+ * @author Antenore Gatta and Giovanni Panozzo
+ * @date 12 Feb 2018
+ *
+ * Since October 29, 2021 data is not collected nor sent to remmina.org anymore. All the
+ * code intended to send data has been removed. The following documentation
+ * has to be kept for those with versions of Remmina older than 1.4.22.
+ *
+ * When Remmina starts asks the user to share some usage statistics
+ * with the Remmina developers. As per the opt-in model
+ * (https://en.wikipedia.org/wiki/Opt-in_email), without the consent of the user,
+ * no data will be collected.
+ * Additionally a user can ask, at any moment, that any data linked to their
+ * profile to be deleted, and can change the Remmina settings to stop
+ * collecting and sharing usage statistics.
+ *
+ * All the data are encrypted at client side using RSA, through the OpenSSL
+ * libraries, and decrypted offline to maximize security.
+ *
+ * The following example show which kind of data are collected.
+ *
+ * @code
+ * {
+ *
+ * "UID": "P0M20TXN03DWF4-9a1e6da2ad"
+ * "REMMINAVERSION": {
+ * "version": "1.2.0-rcgit-26"
+ * "git_revision": "9c5c4805"
+ * "snap_build": 0
+ * }
+ * "SYSTEM": {
+ * "kernel_name": "Linux"
+ * "kernel_release": "4.14.11-200.fc26.x86_64"
+ * "kernel_arch": "x86_64"
+ * "lsb_distributor": "Fedora"
+ * "lsb_distro_description": "Fedora release 26 (Twenty Six)"
+ * "lsb_distro_release": "26"
+ * "lsb_distro_codename": "TwentySix"
+ * "etc_release": "Fedora release 26 (Twenty Six)"
+ * }
+ * "GTKVERSION": {
+ * "major": 3
+ * "minor": 22
+ * "micro": 21
+ * }
+ * "GTKBACKEND": "X11"
+ * "WINDOWMANAGER": {
+ * "window_manager": "GNOME i3-gnome"
+ * }
+ * "APPINDICATOR": {
+ * "appindicator_supported": 0
+ * "appindicator_compiled": 1
+ * "icon_is_active": 1
+ * "appindicator_type": "AppIndicator on GtkStatusIcon/xembed"
+ * }
+ * "PROFILES": {
+ * "profile_count": 457
+ * "SSH": 431
+ * "NX": 1
+ * "RDP": 7
+ * "TERMINAL": 2
+ * "X2GO": 5
+ * "SFTP": 4
+ * "PYTHON_SIMPLE": 4
+ * "SPICE": 3
+ * "DATE_SSH": "20180209"
+ * "DATE_NX": ""
+ * "DATE_RDP": "20180208"
+ * "DATE_TERMINAL": ""
+ * "DATE_X2GO": ""
+ * "DATE_SFTP": ""
+ * "DATE_PYTHON_SIMPLE": ""
+ * "DATE_SPICE": ""
+ * }
+ * "ENVIRONMENT": {
+ * "language": "en_US.utf8"
+ * }
+ * "ACTIVESECRETPLUGIN": {
+ * "plugin_name": "kwallet"
+ * }
+ * "HASPRIMARYPASSWORD": {
+ * "primary_password_status": "OFF"
+ * }
+ *
+ * }
+ * @endcode
+ *
+ * All of these data are solely transmitted to understand:
+ * - On which type of system Remmina is used
+ * - Operating System
+ * - Architecture (32/64bit)
+ * - Linux distributor or OS vendor
+ * - Desktop Environment type.
+ * - Main library versions installed on the system in use by Remmina.
+ * - Protocols used
+ * - Last time each protocol has been used (globally).
+ *
+ * @see https://www.remmina.org for more info.
+ */
+
+#include "config.h"
+#include <string.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <gtk/gtk.h>
+
+
+#include "remmina.h"
+#include "remmina_file.h"
+#include "remmina_file_manager.h"
+#include "remmina_icon.h"
+#include "remmina_log.h"
+#include "remmina_pref.h"
+#include "remmina_curl_connector.h"
+#include "remmina_sysinfo.h"
+#include "remmina_utils.h"
+#include "remmina_scheduler.h"
+#include "remmina_public.h"
+#include "remmina_main.h"
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_plugin_manager.h"
+
+#ifdef GDK_WINDOWING_WAYLAND
+ #include <gdk/gdkwayland.h>
+#endif
+#ifdef GDK_WINDOWING_X11
+ #include <gdk/gdkx.h>
+#endif
+#include "remmina_info.h"
+
+#define MAX_ENV_LEN 10000
+
+gboolean info_disable_stats = 1;
+gboolean info_disable_news = 1;
+gboolean info_disable_tip = 1;
+
+
+struct ProfilesData {
+ GHashTable *proto_count;
+ GHashTable *proto_date;
+ const gchar *protocol; /** Key in the proto_count hash table.*/
+ const gchar *pdatestr; /** Date in string format in the proto_date hash table. */
+ gint pcount;
+ gchar datestr;
+};
+
+#if !JSON_CHECK_VERSION(1, 2, 0)
+ #define json_node_unref(x) json_node_free(x)
+#endif
+
+/* Timers */
+#define INFO_PERIODIC_CHECK_1ST_MS 1000
+#define INFO_PERIODIC_CHECK_INTERVAL_MS 86400000
+
+#define PERIODIC_UPLOAD_URL "https://info.remmina.org/info/upload_stats"
+#define INFO_REQUEST_URL "https://info.remmina.org/info/handshake"
+
+
+static RemminaInfoDialog *remmina_info_dialog;
+#define GET_OBJ(object_name) gtk_builder_get_object(remmina_info_dialog->builder, object_name)
+
+typedef struct {
+ gboolean send_stats;
+ JsonNode *statsroot;
+} sc_tdata;
+
+JsonNode *remmina_info_stats_get_uid()
+{
+ TRACE_CALL(__func__);
+ JsonNode *r;
+ GChecksum *chs;
+ const gchar *uname, *hname;
+ const gchar *uid_suffix;
+ gchar *uid_prefix;
+ gchar *uid;
+
+ /** @warning this function is usually executed on a dedicated thread,
+ * not on the main thread
+ */
+
+ if (remmina_pref.info_uid_prefix == NULL || remmina_pref.info_uid_prefix[0] == 0) {
+ /* Generate a new UUID_PREFIX for this installation */
+ uid_prefix = remmina_gen_random_uuid();
+ if (remmina_pref.info_uid_prefix) {
+ g_free(remmina_pref.info_uid_prefix);
+ }
+ remmina_pref.info_uid_prefix = uid_prefix;
+ remmina_pref_save();
+ }
+
+ uname = g_get_user_name();
+ hname = g_get_host_name();
+ chs = g_checksum_new(G_CHECKSUM_SHA256);
+ g_checksum_update(chs, (const guchar *)uname, strlen(uname));
+ g_checksum_update(chs, (const guchar *)hname, strlen(hname));
+ uid_suffix = g_checksum_get_string(chs);
+
+ uid = g_strdup_printf("%s-%.10s", remmina_pref.info_uid_prefix, uid_suffix);
+ g_checksum_free(chs);
+
+ r = json_node_alloc();
+ json_node_init_string(r, uid);
+
+ g_free(uid);
+
+ return r;
+}
+
+JsonNode *remmina_info_stats_get_os_info()
+{
+ TRACE_CALL(__func__);
+ JsonBuilder *b;
+ JsonNode *r;
+
+ gchar *kernel_name;
+ gchar *kernel_release;
+ gchar *kernel_arch;
+ gchar *id;
+ gchar *description;
+ GHashTable *etc_release;
+ gchar *release;
+ gchar *codename;
+ GHashTableIter iter;
+ gchar *key, *value;
+ gchar *mage;
+
+ /** @warning this function is usually executed on a dedicated thread,
+ * not on the main thread */
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+ json_builder_begin_object(b);
+
+ json_builder_set_member_name(b, "kernel_name");
+ kernel_name = remmina_utils_get_kernel_name();
+ if (!kernel_name || kernel_name[0] == '\0') {
+ json_builder_add_null_value(b);
+ }else {
+ json_builder_add_string_value(b, kernel_name);
+ }
+ g_free(kernel_name);
+
+ json_builder_set_member_name(b, "kernel_release");
+ kernel_release = remmina_utils_get_kernel_release();
+ if (!kernel_release || kernel_release[0] == '\0') {
+ json_builder_add_null_value(b);
+ }else {
+ json_builder_add_string_value(b, kernel_release);
+ }
+ g_free(kernel_release);
+
+ json_builder_set_member_name(b, "kernel_arch");
+ kernel_arch = remmina_utils_get_kernel_arch();
+ if (!kernel_arch || kernel_arch[0] == '\0') {
+ json_builder_add_null_value(b);
+ }else {
+ json_builder_add_string_value(b, kernel_arch);
+ }
+ g_free(kernel_arch);
+
+ json_builder_set_member_name(b, "lsb_distributor");
+ id = remmina_utils_get_lsb_id();
+ if (!id || id[0] == '\0') {
+ json_builder_add_null_value(b);
+ }else {
+ json_builder_add_string_value(b, id);
+ }
+ g_free(id);
+
+ json_builder_set_member_name(b, "lsb_distro_description");
+ description = remmina_utils_get_lsb_description();
+ if (!description || description[0] == '\0') {
+ json_builder_add_null_value(b);
+ }else {
+ json_builder_add_string_value(b, description);
+ }
+ g_free(description);
+
+ json_builder_set_member_name(b, "lsb_distro_release");
+ release = remmina_utils_get_lsb_release();
+ if (!release || release[0] == '\0') {
+ json_builder_add_null_value(b);
+ }else {
+ json_builder_add_string_value(b, release);
+ }
+ g_free(release);
+
+ json_builder_set_member_name(b, "lsb_distro_codename");
+ codename = remmina_utils_get_lsb_codename();
+ if (!codename || codename[0] == '\0') {
+ json_builder_add_null_value(b);
+ }else {
+ json_builder_add_string_value(b, codename);
+ }
+ g_free(codename);
+
+ etc_release = remmina_utils_get_etc_release();
+ json_builder_set_member_name(b, "etc_release");
+ if (etc_release) {
+ json_builder_begin_object(b);
+ g_hash_table_iter_init (&iter, etc_release);
+ while (g_hash_table_iter_next (&iter, (gpointer)&key, (gpointer)&value)) {
+ json_builder_set_member_name(b, key);
+ json_builder_add_string_value(b, value);
+ }
+ json_builder_end_object(b);
+ g_hash_table_remove_all(etc_release);
+ g_hash_table_unref(etc_release);
+ }else {
+ json_builder_add_null_value(b);
+ }
+
+ mage = remmina_utils_get_mage();
+ json_builder_set_member_name(b, "mage");
+ json_builder_add_string_value(b, mage);
+ g_free(mage);
+
+
+ /** @todo Add other means to identify a release name/description
+ * to cover as much OS as possible, like /etc/issue
+ */
+
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+ return r;
+}
+
+/**
+ * Gets the following user environment:
+ * - Gets the user’s locale (or NULL by default) corresponding to LC_ALL.
+ *
+ * @return a JSON Node structure containing the user’s environment.
+ */
+JsonNode *remmina_info_stats_get_user_env()
+{
+ TRACE_CALL(__func__);
+ JsonBuilder *b;
+ JsonNode *r;
+
+ gchar *language;
+ gchar **environment;
+ gchar **uenv;
+ gchar *str;
+ gsize env_len = 0;
+
+ language = remmina_utils_get_lang();
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+
+ environment = g_get_environ();
+ uenv = environment;
+
+ json_builder_begin_object(b);
+ json_builder_set_member_name(b, "language");
+ json_builder_add_string_value(b, language);
+
+ gchar *safe_ptr;
+ while (*uenv && env_len <= MAX_ENV_LEN) {
+ str = strtok_r(*uenv, "=", &safe_ptr);
+ if (str != NULL) {
+ env_len += strlen(str);
+ json_builder_set_member_name(b, str);
+ }
+ str = strtok_r(NULL, "\n", &safe_ptr);
+ if (str != NULL) {
+ env_len += strlen(str);
+ json_builder_add_string_value(b, str);
+ }
+ else{
+ json_builder_add_string_value(b, "");
+ }
+ uenv++;
+ }
+
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+ g_strfreev(environment);
+ return r;
+}
+
+JsonNode *remmina_info_stats_get_host() {
+
+ TRACE_CALL(__func__);
+ JsonBuilder *b;
+ JsonNode *r;
+
+ gchar *logical, *physical;
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+
+ json_builder_begin_object(b);
+ json_builder_set_member_name(b, "host");
+ logical = remmina_utils_get_logical();
+ if (!logical || logical[0] == '\0') {
+ json_builder_add_null_value(b);
+ }
+ else {
+ json_builder_add_string_value(b, logical);
+ }
+ g_free(logical);
+
+ json_builder_set_member_name(b, "hw");
+ physical = remmina_utils_get_link();
+ if (!physical || physical[0] == '\0') {
+ json_builder_add_null_value(b);
+ }
+ else {
+ json_builder_add_string_value(b, physical);
+ }
+ g_free(physical);
+
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+ return r;
+}
+
+JsonNode *remmina_info_stats_get_version()
+{
+ TRACE_CALL(__func__);
+ JsonBuilder *b;
+ JsonNode *r;
+ gchar *flatpak_info;
+
+ /** @warning this function is usually executed on a dedicated thread,
+ * not on the main thread */
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+ json_builder_begin_object(b);
+ json_builder_set_member_name(b, "version");
+ json_builder_add_string_value(b, VERSION);
+ json_builder_set_member_name(b, "git_revision");
+ json_builder_add_string_value(b, REMMINA_GIT_REVISION);
+ json_builder_set_member_name(b, "snap_build");
+#ifdef SNAP_BUILD
+ json_builder_add_int_value(b, 1);
+#else
+ json_builder_add_int_value(b, 0);
+#endif
+
+ /**
+ * Detect if Remmina is running under Flatpak
+ */
+ json_builder_set_member_name(b, "flatpak_build");
+ /* Flatpak sandbox should contain the file ${XDG_RUNTIME_DIR}/flatpak-info */
+ flatpak_info = g_build_filename(g_get_user_runtime_dir(), "flatpak-info", NULL);
+ if (g_file_test(flatpak_info, G_FILE_TEST_EXISTS)) {
+ json_builder_add_int_value(b, 1);
+ } else {
+ json_builder_add_int_value(b, 0);
+ }
+ g_free(flatpak_info);
+
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+ return r;
+}
+
+JsonNode *remmina_info_stats_get_gtk_version()
+{
+ TRACE_CALL(__func__);
+ JsonBuilder *b;
+ JsonNode *r;
+
+ /** @warning this function is usually executed on a dedicated thread,
+ * not on the main thread
+ */
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+
+ json_builder_begin_object(b);
+ json_builder_set_member_name(b, "major");
+ json_builder_add_int_value(b, gtk_get_major_version());
+ json_builder_set_member_name(b, "minor");
+ json_builder_add_int_value(b, gtk_get_minor_version());
+ json_builder_set_member_name(b, "micro");
+ json_builder_add_int_value(b, gtk_get_micro_version());
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+ return r;
+}
+
+JsonNode *remmina_info_stats_get_gtk_backend()
+{
+ TRACE_CALL(__func__);
+ JsonNode *r;
+ GdkDisplay *disp;
+ gchar *bkend;
+
+ /** @warning this function is usually executed on a dedicated thread,
+ * not on the main thread
+ */
+
+ disp = gdk_display_get_default();
+
+#ifdef GDK_WINDOWING_WAYLAND
+ if (GDK_IS_WAYLAND_DISPLAY(disp)) {
+ bkend = "Wayland";
+ }else
+#endif
+#ifdef GDK_WINDOWING_X11
+ if (GDK_IS_X11_DISPLAY(disp)) {
+ bkend = "X11";
+ } else
+#endif
+ bkend = "n/a";
+
+ r = json_node_alloc();
+ json_node_init_string(r, bkend);
+
+ return r;
+
+}
+
+JsonNode *remmina_info_stats_get_wm_name()
+{
+ TRACE_CALL(__func__);
+ JsonBuilder *b;
+ JsonNode *r;
+ gchar *wmver;
+ gchar *wmname;
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+
+ json_builder_begin_object(b);
+
+ json_builder_set_member_name(b, "window_manager");
+
+ /** We try to get the GNOME Shell version */
+ wmver = remmina_sysinfo_get_gnome_shell_version();
+ if (!wmver || wmver[0] == '\0') {
+ }else {
+ json_builder_add_string_value(b, "GNOME Shell");
+ json_builder_set_member_name(b, "gnome_shell_ver");
+ json_builder_add_string_value(b, wmver);
+ goto end;
+ }
+ g_free(wmver);
+
+ wmname = remmina_sysinfo_get_wm_name();
+ if (!wmname || wmname[0] == '\0') {
+ json_builder_add_string_value(b, "n/a");
+ }else {
+ json_builder_add_string_value(b, wmname);
+ }
+ g_free(wmname);
+
+ end:
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+ return r;
+}
+
+JsonNode *remmina_info_stats_get_indicator()
+{
+ TRACE_CALL(__func__);
+ JsonBuilder *b;
+ JsonNode *r;
+ gboolean sni; /** Support for StatusNotifier or AppIndicator */
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+
+ json_builder_begin_object(b);
+
+ json_builder_set_member_name(b, "appindicator_supported");
+ sni = remmina_sysinfo_is_appindicator_available();
+ if (sni) {
+ /** StatusNotifier/Appindicator supported by desktop */
+ json_builder_add_int_value(b, 1);
+ json_builder_set_member_name(b, "appindicator_compiled");
+#ifdef HAVE_LIBAPPINDICATOR
+ /** libappindicator is compiled in remmina. */
+ json_builder_add_int_value(b, 1);
+#else
+ /** Remmina not compiled with -DWITH_APPINDICATOR=on */
+ json_builder_add_int_value(b, 0);
+#endif
+ }
+ else{
+ /** StatusNotifier/Appindicator NOT supported by desktop */
+ json_builder_add_int_value(b, 0);
+ }
+
+ json_builder_set_member_name(b, "icon_is_active");
+ if (remmina_icon_is_available()) {
+ /** Remmina icon is active */
+ json_builder_add_int_value(b, 1);
+ json_builder_set_member_name(b, "appindicator_type");
+#ifdef HAVE_LIBAPPINDICATOR
+ /** libappindicator fallback to GtkStatusIcon/xembed"); */
+ json_builder_add_string_value(b, "AppIndicator on GtkStatusIcon/xembed");
+#else
+ /** Remmina fallback to GtkStatusIcon/xembed */
+ json_builder_add_string_value(b, "Remmina icon on GtkStatusIcon/xembed");
+#endif
+ }else {
+ /** Remmina icon is NOT active */
+ json_builder_add_int_value(b, 0);
+ }
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+ return r;
+}
+
+/**
+ * Given a remmina file, fills a structure containing profiles keys/value tuples.
+ *
+ * This is used as a callback function with remmina_file_manager_iterate.
+ * @todo Move this in a separate file.
+ */
+static void remmina_info_profiles_get_data(RemminaFile *remminafile, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ gint count = 0;
+ gpointer pcount, kpo;
+ gpointer pdate;
+ gchar *hday, *hmonth, *hyear;
+ gchar *pday, *pmonth, *pyear;
+
+ GDateTime *prof_gdate; /** Source date -> from profile */
+ GDateTime *pdata_gdate; /** Destination date -> The date in the pdata structure */
+
+ struct ProfilesData* pdata;
+ pdata = (struct ProfilesData*)user_data;
+
+ pdata->protocol = remmina_file_get_string(remminafile, "protocol");
+ //pdata->pdatestr = remmina_file_get_string(remminafile, "last_success");
+ const gchar *last_success = remmina_file_get_string(remminafile, "last_success");
+
+ prof_gdate = pdata_gdate = NULL;
+ if (last_success && last_success[0] != '\0' && strlen(last_success) >= 6) {
+ pyear = g_strdup_printf("%.4s", last_success);
+ pmonth = g_strdup_printf("%.2s", last_success + 4);
+ pday = g_strdup_printf("%.2s", last_success + 6);
+ prof_gdate = g_date_time_new_local(
+ atoi(pyear),
+ atoi(pmonth),
+ atoi(pday), 0, 0, 0);
+ g_free(pyear);
+ g_free(pmonth);
+ g_free(pday);
+ }
+
+
+ if (pdata->protocol && pdata->protocol[0] != '\0') {
+ if (g_hash_table_lookup_extended(pdata->proto_count, pdata->protocol, &kpo, &pcount)) {
+ count = GPOINTER_TO_INT(pcount) + 1;
+ }else {
+ count = 1;
+ g_hash_table_insert(pdata->proto_count, g_strdup(pdata->protocol), GINT_TO_POINTER(count));
+ }
+ g_hash_table_replace(pdata->proto_count, g_strdup(pdata->protocol), GINT_TO_POINTER(count));
+ pdate = NULL;
+ if (g_hash_table_lookup_extended(pdata->proto_date, pdata->protocol, NULL, &pdate)) {
+
+ pdata_gdate = NULL;
+ if (pdate && strlen(pdate) >= 6) {
+ pdata->pdatestr = g_strdup(pdate);
+ hyear = g_strdup_printf("%.4s", (char*)pdate);
+ hmonth = g_strdup_printf("%.2s", (char*)pdate + 4);
+ hday = g_strdup_printf("%.2s", (char*)pdate + 6);
+ pdata_gdate = g_date_time_new_local(
+ atoi(hyear),
+ atoi(hmonth),
+ atoi(hday), 0, 0, 0);
+ g_free(hyear);
+ g_free(hmonth);
+ g_free(hday);
+ }
+
+ /** When both date in the hash and in the profile are valid we compare the date */
+ if (prof_gdate != NULL && pdata_gdate != NULL ) {
+ gint res = g_date_time_compare( pdata_gdate, prof_gdate );
+ /** If the date in the hash less than the date in the profile, we take the latter */
+ if (res < 0 ) {
+ g_hash_table_replace(pdata->proto_date, g_strdup(pdata->protocol), g_strdup(last_success));
+ } else {
+ g_hash_table_replace(pdata->proto_date, g_strdup(pdata->protocol), g_strdup(pdata->pdatestr));
+ }
+
+ }
+ /** If the date in the profile is NOT valid and the date in the hash is valid we keep the latter */
+ if (prof_gdate == NULL && pdata_gdate != NULL) {
+ g_hash_table_replace(pdata->proto_date, g_strdup(pdata->protocol), g_strdup(pdata->pdatestr));
+ }
+
+ /** If the date in the hash is NOT valid and the date in the profile is valid we keep the latter */
+ if (prof_gdate != NULL && pdata_gdate == NULL) {
+ g_hash_table_replace(pdata->proto_date, g_strdup(pdata->protocol), g_strdup(last_success));
+ }
+ /** If both date are NULL, we insert NULL for that protocol */
+ if ((prof_gdate == NULL && pdata_gdate == NULL) && pdata->pdatestr) {
+ g_hash_table_replace(pdata->proto_date, g_strdup(pdata->protocol), NULL);
+ }
+ } else {
+ /** If there is not the protocol in the hash, we add it */
+ /** If the date in the profile is not NULL we use it */
+ if (pdata->pdatestr) {
+ g_hash_table_replace(pdata->proto_date, g_strdup(pdata->protocol), g_strdup(pdata->pdatestr));
+ }else {
+ /** Otherwise we set it to NULL */
+ g_hash_table_replace(pdata->proto_date, g_strdup(pdata->protocol), NULL);
+ }
+ }
+ }
+ if (pdata_gdate)
+ g_date_time_unref(pdata_gdate);
+ if (prof_gdate)
+ g_date_time_unref(prof_gdate);
+}
+
+/**
+ * Add a JSON member profile_count with a child for each protocol used by the user.
+ * Count how many profiles are in use and for each protocol in use counts of how many
+ * profiles that uses such protocol.
+ *
+ * The data can be expressed as follows:
+ *
+ * | PROTO | PROF COUNT |
+ * |:-------|-----------:|
+ * | RDP | 2560 |
+ * | SPICE | 334 |
+ * | SSH | 1540 |
+ * | VNC | 2 |
+ *
+ * | PROTO | LAST USED |
+ * |:-------|----------:|
+ * | RDP | 20180129 |
+ * | SPICE | 20171122 |
+ * | SSH | 20180111 |
+ *
+ * @return a JSON Node structure containing the protocol usage statistics.
+ *
+ */
+JsonNode *remmina_info_stats_get_profiles()
+{
+ TRACE_CALL(__func__);
+
+ JsonBuilder *b;
+ JsonNode *r;
+ gchar *s;
+
+ gint profiles_count;
+ GHashTableIter pcountiter, pdateiter;
+ gpointer pcountkey, pcountvalue;
+ gpointer pdatekey, pdatevalue;
+
+ struct ProfilesData *pdata;
+ pdata = g_malloc0(sizeof(struct ProfilesData));
+ if (pdata == NULL) {
+ return NULL;
+ }
+
+ b = json_builder_new();
+ if (b == NULL) {
+ g_free(pdata);
+ return NULL;
+ }
+
+ json_builder_begin_object(b);
+
+ json_builder_set_member_name(b, "profile_count");
+
+ /** @warning this function is usually executed on a dedicated thread,
+ * not on the main thread */
+
+ pdata->proto_date = g_hash_table_new_full(g_str_hash, g_str_equal,
+ (GDestroyNotify)g_free, (GDestroyNotify)g_free);
+ pdata->proto_count = g_hash_table_new_full(g_str_hash, g_str_equal,
+ (GDestroyNotify)g_free, NULL);
+
+ profiles_count = remmina_file_manager_iterate(
+ (GFunc)remmina_info_profiles_get_data,
+ (gpointer)pdata);
+
+ json_builder_add_int_value(b, profiles_count);
+
+ g_hash_table_iter_init(&pcountiter, pdata->proto_count);
+ while (g_hash_table_iter_next(&pcountiter, &pcountkey, &pcountvalue)) {
+ json_builder_set_member_name(b, (gchar*)pcountkey);
+ json_builder_add_int_value(b, GPOINTER_TO_INT(pcountvalue));
+ }
+
+ g_hash_table_iter_init(&pdateiter, pdata->proto_date);
+ while (g_hash_table_iter_next(&pdateiter, &pdatekey, &pdatevalue)) {
+ s = g_strdup_printf("DATE_%s", (gchar*)pdatekey);
+ json_builder_set_member_name(b, s);
+ g_free(s);
+ json_builder_add_string_value(b, (gchar*)pdatevalue);
+ }
+
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+
+ g_hash_table_remove_all(pdata->proto_date);
+ g_hash_table_unref(pdata->proto_date);
+ g_hash_table_remove_all(pdata->proto_count);
+ g_hash_table_unref(pdata->proto_count);
+
+ g_free(pdata);
+
+ return r;
+}
+
+/**
+ * Add a JSON member ACTIVESECRETPLUGIN which shows the current secret plugin in use by Remmina.
+ *
+ * @return a JSON Node structure containing the secret plugin in use
+ *
+ */
+JsonNode *remmina_info_stats_get_secret_plugin()
+{
+ TRACE_CALL(__func__);
+
+ JsonBuilder *b;
+ JsonNode *r;
+ RemminaSecretPlugin *secret_plugin;
+ secret_plugin = remmina_plugin_manager_get_secret_plugin();
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+
+ json_builder_begin_object(b);
+
+ if (secret_plugin && secret_plugin->is_service_available) {
+ json_builder_set_member_name(b, "plugin_name");
+ json_builder_add_string_value(b, secret_plugin->name);
+ }
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+
+ return r;
+}
+
+/**
+ * Add a JSON member HASPRIMARYPASSWORD which shows the status of the master password.
+ *
+ * @return a JSON Node structure containing the status of the primary password
+ *
+ */
+JsonNode *remmina_info_stats_get_primary_password_status()
+{
+ TRACE_CALL(__func__);
+
+ JsonBuilder *b;
+ JsonNode *r;
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+
+ json_builder_begin_object(b);
+
+ json_builder_set_member_name(b, "primary_password_status");
+ if (remmina_pref_get_boolean("use_primary_password")) {
+ json_builder_add_string_value(b, "ON");
+ } else {
+ json_builder_add_string_value(b, "OFF");
+ }
+
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+
+ return r;
+}
+
+/**
+ * Add a json member KIOSK which shows the status of the kiosk.
+ *
+ * @return a JSON Node structure containing the status of the primary password
+ *
+ */
+JsonNode *remmina_info_stats_get_kiosk_mode()
+{
+ TRACE_CALL(__func__);
+
+ JsonBuilder *b;
+ JsonNode *r;
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+
+ json_builder_begin_object(b);
+
+ json_builder_set_member_name(b, "kiosk_status");
+ if (!kioskmode && kioskmode == FALSE) {
+ json_builder_add_string_value(b, "OFF");
+ }else {
+ json_builder_add_string_value(b, "ON");
+ }
+
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+
+ return r;
+}
+
+JsonNode *remmina_info_stats_get_python()
+{
+ TRACE_CALL(__func__);
+ JsonBuilder *b;
+ JsonNode *r;
+ gchar *version;
+
+ version = remmina_utils_get_python();
+ b = json_builder_new();
+ if(b != NULL)
+ {
+ json_builder_begin_object(b);
+ json_builder_set_member_name(b, "version");
+ if (!version || version[0] == '\0') {
+ json_builder_add_null_value(b);
+ } else {
+ json_builder_add_string_value(b, version);
+ }
+
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+ return r;
+ }
+ else
+ {
+ return NULL;
+ }
+}
+
+/**
+ * Get all statistics in JSON format to send periodically to the server.
+ * The caller should free the returned buffer with g_free()
+ * @warning This function is usually executed on a dedicated thread,
+ * not on the main thread.
+ * @return a pointer to the JSON string.
+ */
+JsonNode *remmina_info_stats_get_all()
+{
+ TRACE_CALL(__func__);
+ JsonBuilder *b;
+ JsonNode *n;
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return NULL;
+ }
+
+ json_builder_begin_object(b);
+
+ n = remmina_info_stats_get_uid();
+ json_builder_set_member_name(b, "UID");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_version();
+ json_builder_set_member_name(b, "REMMINAVERSION");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_os_info();
+ json_builder_set_member_name(b, "SYSTEM");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_user_env();
+ json_builder_set_member_name(b, "ENVIRONMENT");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_gtk_version();
+ json_builder_set_member_name(b, "GTKVERSION");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_gtk_backend();
+ json_builder_set_member_name(b, "GTKBACKEND");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_wm_name();
+ json_builder_set_member_name(b, "WINDOWMANAGER");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_indicator();
+ json_builder_set_member_name(b, "APPINDICATOR");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_profiles();
+ json_builder_set_member_name(b, "PROFILES");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_secret_plugin();
+ json_builder_set_member_name(b, "ACTIVESECRETPLUGIN");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_primary_password_status();
+ json_builder_set_member_name(b, "HASPRIMARYPASSWORD");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_kiosk_mode();
+ json_builder_set_member_name(b, "KIOSK");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_python();
+ json_builder_set_member_name(b, "PYTHON");
+ json_builder_add_value(b, n);
+
+ n = remmina_info_stats_get_host();
+ json_builder_set_member_name(b, "HOST");
+ json_builder_add_value(b, n);
+
+ json_builder_end_object(b);
+ n = json_builder_get_root(b);
+ g_object_unref(b);
+
+ return n;
+}
+
+void remmina_info_schedule()
+{
+ sc_tdata *data;
+
+ data = g_malloc(sizeof(sc_tdata));
+ if (data == NULL) {
+ return;
+ }
+ data->send_stats = !info_disable_stats;
+
+ remmina_scheduler_setup(remmina_info_periodic_check,
+ data,
+ INFO_PERIODIC_CHECK_1ST_MS,
+ INFO_PERIODIC_CHECK_INTERVAL_MS);
+}
+
+static void remmina_info_close_clicked(GtkButton *btn, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ if (remmina_info_dialog->dialog) {
+ gtk_widget_destroy(GTK_WIDGET(remmina_info_dialog->dialog));
+ }
+ remmina_info_dialog->dialog = NULL;
+ g_free(remmina_info_dialog);
+ remmina_info_dialog = NULL;
+}
+
+static gboolean remmina_info_dialog_deleted(GtkButton *btn, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gtk_widget_destroy(GTK_WIDGET(remmina_info_dialog->dialog));
+ remmina_info_dialog->dialog = NULL;
+ g_free(remmina_info_dialog);
+ remmina_info_dialog = NULL;
+
+ return FALSE;
+}
+
+
+
+gboolean remmina_info_show_response(gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkWindow * parent = remmina_main_get_window();
+ remmina_info_dialog = g_new0(RemminaInfoDialog, 1);
+ remmina_info_dialog->retval = 1;
+
+ RemminaInfoMessage *message = (RemminaInfoMessage*)user_data;
+
+ remmina_info_dialog->builder = remmina_public_gtk_builder_new_from_resource("/org/remmina/Remmina/src/../data/ui/remmina_info.glade");
+ remmina_info_dialog->dialog = GTK_DIALOG(gtk_builder_get_object(remmina_info_dialog->builder, "RemminaInfoDialog"));
+
+ remmina_info_dialog->remmina_info_text_view = GTK_TEXT_VIEW(GET_OBJ("remmina_info_text_view"));
+ remmina_info_dialog->remmina_info_label = GTK_LABEL(GET_OBJ("remmina_info_label"));
+
+ remmina_info_dialog->remmina_info_button_close = GTK_BUTTON(GET_OBJ("remmina_info_button_close"));
+ gtk_widget_set_can_default(GTK_WIDGET(remmina_info_dialog->remmina_info_button_close), TRUE);
+ gtk_widget_grab_default(GTK_WIDGET(remmina_info_dialog->remmina_info_button_close));
+
+ gtk_label_set_markup(remmina_info_dialog->remmina_info_label, message->info_string);
+
+ g_signal_connect(remmina_info_dialog->remmina_info_button_close, "clicked",
+ G_CALLBACK(remmina_info_close_clicked), (gpointer)remmina_info_dialog);
+ g_signal_connect(remmina_info_dialog->dialog, "close",
+ G_CALLBACK(remmina_info_close_clicked), NULL);
+ g_signal_connect(remmina_info_dialog->dialog, "delete-event",
+ G_CALLBACK(remmina_info_dialog_deleted), NULL);
+
+ /* Connect signals */
+ gtk_builder_connect_signals(remmina_info_dialog->builder, NULL);
+
+ /* Show the modal news dialog */
+ gtk_widget_show_all(GTK_WIDGET(remmina_info_dialog->dialog));
+ gtk_window_present(GTK_WINDOW(remmina_info_dialog->dialog));
+ if (parent) {
+ gtk_window_set_transient_for(GTK_WINDOW(remmina_info_dialog->dialog), parent);
+ }
+ gtk_window_set_modal(GTK_WINDOW(remmina_info_dialog->dialog), TRUE);
+ gtk_window_set_title(GTK_WINDOW(remmina_info_dialog->dialog), message->title_string);
+ g_free(message);
+ return FALSE;
+}
+
+
+/**
+ * Post request to info server
+ *
+ * @return gboolean
+ */
+static gboolean remmina_info_stats_collector_done(gpointer data)
+{
+ JsonNode *n;
+ JsonGenerator *g;
+ gchar *unenc_s, *enc_s;
+ JsonBuilder *b;
+ JsonObject *o;
+ gchar *uid;
+ JsonNode *root;
+
+ root = (JsonNode *)data;
+ if (root == NULL) {
+ return G_SOURCE_REMOVE;
+ }
+
+ n = root;
+
+ if ((o = json_node_get_object(n)) == NULL) {
+ g_free(data);
+ return G_SOURCE_REMOVE;
+ }
+
+ uid = g_strdup(json_object_get_string_member(o, "UID"));
+
+ g = json_generator_new();
+ json_generator_set_root(g, n);
+ json_node_unref(n);
+ unenc_s = json_generator_to_data(g, NULL);
+ g_object_unref(g);
+
+ EVP_PKEY *remmina_pubkey = remmina_get_pubkey(RSA_KEYTYPE, remmina_RSA_PubKey_v1);
+ if (remmina_pubkey == NULL) {
+ g_free(uid);
+ g_free(unenc_s);
+ return G_SOURCE_REMOVE;
+ }
+ enc_s = remmina_rsa_encrypt_string(remmina_pubkey, unenc_s);
+ if (enc_s == NULL) {
+ g_free(uid);
+ g_free(unenc_s);
+ return G_SOURCE_REMOVE;
+ }
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return G_SOURCE_REMOVE;
+ }
+
+
+ EVP_PKEY_free(remmina_pubkey);
+ json_builder_begin_object(b);
+ json_builder_set_member_name(b, "keyversion");
+ json_builder_add_int_value(b, 1);
+ json_builder_set_member_name(b, "encdata");
+ json_builder_add_string_value(b, enc_s);
+ json_builder_set_member_name(b, "UID");
+ json_builder_add_string_value(b, uid);
+ json_builder_set_member_name(b, "news_enabled");
+ json_builder_add_int_value(b, TRUE ? 0 : 1);
+ json_builder_set_member_name(b, "stats_enabled");
+ json_builder_add_int_value(b, TRUE ? 0 : 1);
+ json_builder_set_member_name(b, "tip_enabled");
+ json_builder_add_int_value(b, TRUE ? 0 : 1);
+
+
+ json_builder_end_object(b);
+ n = json_builder_get_root(b);
+
+ g_free(unenc_s);
+ g_object_unref(b);
+ g_free(uid);
+ g_free(enc_s);
+
+ g = json_generator_new();
+ json_generator_set_root(g, n);
+ enc_s = json_generator_to_data(g, NULL);
+ g_object_unref(g);
+
+ remmina_curl_compose_message(enc_s, "POST", PERIODIC_UPLOAD_URL, NULL);
+
+ json_node_unref(n);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * Collect stats and notify when collection is done
+ *
+ * @return gpointer
+ */
+gpointer remmina_info_stats_collector()
+{
+ JsonNode *n;
+
+ n = remmina_info_stats_get_all();
+
+ /* Stats collecting is done. Notify main thread calling
+ * remmina_info_stats_collector_done() */
+ g_idle_add(remmina_info_stats_collector_done, n);
+
+ return NULL;
+}
+
+void remmina_info_request(gpointer data)
+{
+ // send initial handshake here, in a callback decide how to handle response
+
+ JsonNode *n;
+ JsonGenerator *g;
+ gchar *enc_s;
+ JsonBuilder *b;
+ const gchar *uid = "000000";
+
+ n = remmina_info_stats_get_uid();
+ if (n != NULL){
+ uid = json_node_get_string(n);
+ }
+
+ b = json_builder_new();
+ if (b == NULL) {
+ return;
+ }
+
+ json_builder_begin_object(b);
+ json_builder_set_member_name(b, "keyversion");
+ json_builder_add_int_value(b, 1);
+ json_builder_set_member_name(b, "UID");
+ json_builder_add_string_value(b, uid);
+ json_builder_set_member_name(b, "news_enabled");
+ json_builder_add_int_value(b, info_disable_news ? 0 : 1);
+ json_builder_set_member_name(b, "stats_enabled");
+ json_builder_add_int_value(b, info_disable_stats ? 0 : 1);
+ json_builder_set_member_name(b, "tip_enabled");
+ json_builder_add_int_value(b, info_disable_tip ? 0 : 1);
+
+
+ json_builder_end_object(b);
+ n = json_builder_get_root(b);
+
+ g_object_unref(b);
+
+
+ g = json_generator_new();
+ json_generator_set_root(g, n);
+ enc_s = json_generator_to_data(g, NULL);
+ g_object_unref(g);
+ remmina_curl_compose_message(enc_s, "POST", INFO_REQUEST_URL, NULL);
+ json_node_unref(n);
+}
+
+gboolean remmina_info_periodic_check(gpointer user_data)
+{
+ remmina_info_request(user_data);
+ return G_SOURCE_CONTINUE;
+}
diff --git a/src/remmina_info.h b/src/remmina_info.h
new file mode 100644
index 0000000..09fe698
--- /dev/null
+++ b/src/remmina_info.h
@@ -0,0 +1,71 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-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.
+ *
+ */
+
+#pragma once
+
+#include <glib.h>
+#include "json-glib/json-glib.h"
+
+G_BEGIN_DECLS
+
+JsonNode *remmina_info_stats_get_all(void);
+JsonNode *remmina_info_stats_get_os_info(void);
+JsonNode *remmina_info_stats_get_python(void);
+JsonNode *remmina_info_stats_get_uid(void);
+gboolean remmina_info_show_response(gpointer user_data);
+gpointer remmina_info_stats_collector();
+gboolean remmina_info_periodic_check(gpointer user_data);
+void remmina_info_schedule(void);
+
+typedef struct _RemminaInfoDialog
+{
+ GtkBuilder *builder;
+ GtkDialog *dialog;
+
+ GtkTextView *remmina_info_text_view;
+ GtkLabel *remmina_info_label;
+ GtkButton *remmina_info_button_close;
+
+ gint retval;
+} RemminaInfoDialog;
+
+typedef struct _RemminaInfoMessage
+{
+ const gchar *info_string;
+ const gchar *title_string;
+
+} RemminaInfoMessage;
+
+G_END_DECLS
diff --git a/src/remmina_key_chooser.c b/src/remmina_key_chooser.c
new file mode 100644
index 0000000..69a87c7
--- /dev/null
+++ b/src/remmina_key_chooser.c
@@ -0,0 +1,146 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 <glib/gi18n.h>
+#include "remmina_key_chooser.h"
+#include "remmina_public.h"
+#include "remmina/remmina_trace_calls.h"
+
+/* Handle key-presses on the GtkEventBox */
+static gboolean remmina_key_chooser_dialog_on_key_press(GtkWidget *widget, GdkEventKey *event, RemminaKeyChooserArguments *arguments)
+{
+ TRACE_CALL(__func__);
+ if (!arguments->use_modifiers || !event->is_modifier) {
+ arguments->state = event->state;
+ arguments->keyval = gdk_keyval_to_lower(event->keyval);
+ gtk_dialog_response(GTK_DIALOG(gtk_widget_get_toplevel(widget)),
+ event->keyval == GDK_KEY_Escape ? GTK_RESPONSE_CANCEL : GTK_RESPONSE_OK);
+ }
+ return TRUE;
+}
+
+/* User option to use key modifiers when selecting keyboard shortcuts */
+void remmina_key_chooser_dialog_set_option_modifier(GtkWidget *widget, gboolean state, RemminaKeyChooserArguments *arguments)
+{
+ TRACE_CALL(__func__);
+ gtk_switch_set_state(GTK_SWITCH(widget), state);
+ arguments->use_modifiers = state;
+}
+
+/* Show a key chooser dialog and return the keyval for the selected key */
+RemminaKeyChooserArguments* remmina_key_chooser_new(GtkWindow *parent_window, gboolean use_modifiers)
+{
+ TRACE_CALL(__func__);
+ GtkBuilder *builder = remmina_public_gtk_builder_new_from_resource ("/org/remmina/Remmina/src/../data/ui/remmina_key_chooser.glade");
+ GtkDialog *dialog;
+ RemminaKeyChooserArguments *arguments;
+ arguments = g_new0(RemminaKeyChooserArguments, 1);
+ arguments->state = 0;
+ arguments->use_modifiers = use_modifiers;
+
+ /* Setup the dialog */
+ dialog = GTK_DIALOG(gtk_builder_get_object(builder, "KeyChooserDialog"));
+ gtk_window_set_transient_for(GTK_WINDOW(dialog), parent_window);
+
+ /* Connect the key modifier switch signal */
+ g_signal_connect(gtk_builder_get_object(builder, "switch_option_key_modifier"), "state-set",
+ G_CALLBACK(remmina_key_chooser_dialog_set_option_modifier), arguments);
+
+ /* Connect the GtkEventBox signal */
+ g_signal_connect(gtk_builder_get_object(builder, "eventbox_key_chooser"), "key-press-event",
+ G_CALLBACK(remmina_key_chooser_dialog_on_key_press), arguments);
+
+ /* Show the dialog and destroy it after the use */
+ arguments->response = gtk_dialog_run(dialog);
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+ /* The delete button set the keyval 0 */
+ if (arguments->response == GTK_RESPONSE_REJECT)
+ arguments->keyval = 0;
+ return arguments;
+}
+
+/* Get the uppercase character value of a keyval */
+gchar* remmina_key_chooser_get_value(guint keyval, guint state)
+{
+ TRACE_CALL(__func__);
+
+ if (!keyval)
+ return g_strdup(KEY_CHOOSER_NONE);
+
+ return g_strdup_printf("%s%s%s%s%s%s%s",
+ (state & GDK_SHIFT_MASK) ? KEY_MODIFIER_SHIFT : "",
+ (state & GDK_CONTROL_MASK) ? KEY_MODIFIER_CTRL : "",
+ (state & GDK_MOD1_MASK) ? KEY_MODIFIER_ALT : "",
+ (state & GDK_SUPER_MASK) ? KEY_MODIFIER_SUPER : "",
+ (state & GDK_HYPER_MASK) ? KEY_MODIFIER_HYPER : "",
+ (state & GDK_META_MASK) ? KEY_MODIFIER_META : "",
+ gdk_keyval_name(gdk_keyval_to_upper(keyval)));
+}
+
+/* Get the keyval of a (lowercase) character value */
+guint remmina_key_chooser_get_keyval(const gchar *value)
+{
+ TRACE_CALL(__func__);
+ gchar *patterns[] =
+ {
+ KEY_MODIFIER_SHIFT,
+ KEY_MODIFIER_CTRL,
+ KEY_MODIFIER_ALT,
+ KEY_MODIFIER_SUPER,
+ KEY_MODIFIER_HYPER,
+ KEY_MODIFIER_META,
+ NULL
+ };
+ gint i;
+ gchar *tmpvalue;
+ gchar *newvalue;
+ guint keyval;
+
+ if (g_strcmp0(value, KEY_CHOOSER_NONE) == 0)
+ return 0;
+
+ /* Remove any modifier text before to get the keyval */
+ newvalue = g_strdup(value);
+ for (i = 0; i < g_strv_length(patterns); i++) {
+ tmpvalue = remmina_public_str_replace(newvalue, patterns[i], "");
+ g_free(newvalue);
+ newvalue = g_strdup(tmpvalue);
+ g_free(tmpvalue);
+ }
+ keyval = gdk_keyval_to_lower(gdk_keyval_from_name(newvalue));
+ g_free(newvalue);
+ return keyval;
+}
diff --git a/src/remmina_key_chooser.h b/src/remmina_key_chooser.h
new file mode 100644
index 0000000..a236932
--- /dev/null
+++ b/src/remmina_key_chooser.h
@@ -0,0 +1,64 @@
+/*
+ * 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
+
+#include <gtk/gtk.h>
+
+#define KEY_MODIFIER_SHIFT _("Shift+")
+#define KEY_MODIFIER_CTRL _("Ctrl+")
+#define KEY_MODIFIER_ALT _("Alt+")
+#define KEY_MODIFIER_SUPER _("Super+")
+#define KEY_MODIFIER_HYPER _("Hyper+")
+#define KEY_MODIFIER_META _("Meta+")
+#define KEY_CHOOSER_NONE _("<None>")
+
+typedef struct _RemminaKeyChooserArguments {
+ guint keyval;
+ guint state;
+ gboolean use_modifiers;
+ gint response;
+} RemminaKeyChooserArguments;
+
+G_BEGIN_DECLS
+
+/* Show a key chooser dialog and return the keyval for the selected key */
+RemminaKeyChooserArguments *remmina_key_chooser_new(GtkWindow *parent_window, gboolean use_modifiers);
+/* Get the uppercase character value of a keyval */
+gchar *remmina_key_chooser_get_value(guint keyval, guint state);
+/* Get the keyval of a (lowercase) character value */
+guint remmina_key_chooser_get_keyval(const gchar *value);
+
+G_END_DECLS
diff --git a/src/remmina_log.c b/src/remmina_log.c
new file mode 100644
index 0000000..be855cc
--- /dev/null
+++ b/src/remmina_log.c
@@ -0,0 +1,505 @@
+/*
+ * 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
+ * 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.
+ *
+ */
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include "remmina_public.h"
+#include "remmina_pref.h"
+#include "remmina_log.h"
+#include "remmina_info.h"
+#include "remmina/remmina_trace_calls.h"
+
+gboolean logstart;
+
+/***** Define the log window GUI *****/
+#define REMMINA_TYPE_LOG_WINDOW (remmina_log_window_get_type())
+#define REMMINA_LOG_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_LOG_WINDOW, RemminaLogWindow))
+#define REMMINA_LOG_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_LOG_WINDOW, RemminaLogWindowClass))
+#define REMMINA_IS_LOG_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_LOG_WINDOW))
+#define REMMINA_IS_LOG_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_LOG_WINDOW))
+#define REMMINA_LOG_WINDOW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_LOG_WINDOW, RemminaLogWindowClass))
+
+typedef struct _RemminaLogWindow {
+ GtkWindow window;
+
+ GtkWidget *log_view;
+ GtkTextBuffer *log_buffer;
+} RemminaLogWindow;
+
+typedef struct _RemminaLogWindowClass {
+ GtkWindowClass parent_class;
+} RemminaLogWindowClass;
+
+GType remmina_log_window_get_type(void)
+G_GNUC_CONST;
+
+G_DEFINE_TYPE(RemminaLogWindow, remmina_log_window, GTK_TYPE_WINDOW)
+
+void remmina_log_stats()
+{
+ TRACE_CALL(__func__);
+ JsonNode *n;
+
+ n = remmina_info_stats_get_all();
+ if (n != NULL) {
+
+ JsonGenerator *g = json_generator_new();
+ json_generator_set_pretty (g, TRUE);
+ json_generator_set_root(g, n);
+ json_node_unref(n);
+ g_autofree gchar *s = json_generator_to_data(g, NULL); // s=serialized stats
+ REMMINA_DEBUG("STATS: JSON data%s\n", s);
+ g_object_unref(g);
+ }
+}
+
+static void remmina_log_window_class_init(RemminaLogWindowClass *klass)
+{
+ TRACE_CALL(__func__);
+}
+
+/* We will always only have one log window per instance */
+static GtkWidget *log_window = NULL;
+
+static GtkWidget*
+remmina_log_window_new(void)
+{
+ TRACE_CALL(__func__);
+ return GTK_WIDGET(g_object_new(REMMINA_TYPE_LOG_WINDOW, NULL));
+}
+
+static void remmina_log_end(GtkWidget *widget, gpointer data)
+{
+ TRACE_CALL(__func__);
+ log_window = NULL;
+}
+
+static void remmina_log_start_stop (GtkSwitch *logswitch, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ logstart = !logstart;
+}
+
+void remmina_log_start(void)
+{
+ TRACE_CALL(__func__);
+ if (log_window) {
+ gtk_window_present(GTK_WINDOW(log_window));
+ }else {
+ log_window = remmina_log_window_new();
+ gtk_window_set_default_size(GTK_WINDOW(log_window), 640, 480);
+ gtk_window_set_resizable (GTK_WINDOW(log_window), TRUE);
+ gtk_window_set_decorated (GTK_WINDOW(log_window), TRUE);
+
+ /* Header bar */
+ GtkWidget *header = gtk_header_bar_new ();
+ gtk_header_bar_set_show_close_button (GTK_HEADER_BAR (header), TRUE);
+ gtk_header_bar_set_title (GTK_HEADER_BAR (header), _("Remmina debugging window"));
+ gtk_header_bar_set_has_subtitle (GTK_HEADER_BAR (header), FALSE);
+ /* Stats */
+ GtkWidget *getstat = gtk_button_new ();
+ gtk_widget_set_tooltip_text (getstat, _("Paste system info in the Remmina debugging window"));
+ GIcon *icon = g_themed_icon_new ("edit-paste-symbolic");
+ GtkWidget *image = gtk_image_new_from_gicon (icon, GTK_ICON_SIZE_BUTTON);
+ g_object_unref (icon);
+ gtk_container_add (GTK_CONTAINER (getstat), image);
+ gtk_header_bar_pack_start (GTK_HEADER_BAR (header), getstat);
+ /* Start logging */
+ GtkWidget *start = gtk_switch_new ();
+ logstart = TRUE;
+ gtk_switch_set_active (GTK_SWITCH(start), logstart);
+ gtk_widget_set_valign (start, GTK_ALIGN_CENTER);
+ gtk_header_bar_pack_start (GTK_HEADER_BAR (header), start);
+
+ gtk_window_set_titlebar (GTK_WINDOW (log_window), header);
+
+ g_signal_connect(getstat, "button-press-event", G_CALLBACK(remmina_log_stats), NULL);
+ g_signal_connect(start, "notify::active", G_CALLBACK(remmina_log_start_stop), NULL);
+ g_signal_connect(G_OBJECT(log_window), "destroy", G_CALLBACK(remmina_log_end), NULL);
+ gtk_widget_show_all(log_window);
+ }
+
+ remmina_log_print(_("This window can help you find connection problems.\n"
+ "You can stop and start the logging at any moment using the On/Off switch.\n"
+ "The stats button (Ctrl+T), can be useful to gather system info you may share when reporting a bug.\n"
+ "There is more info about debugging Remmina on https://gitlab.com/Remmina/Remmina/-/wikis/Usage/Remmina-debugging\n"
+ ));
+}
+
+gboolean remmina_log_running(void)
+{
+ TRACE_CALL(__func__);
+ return (log_window != NULL);
+}
+
+static gboolean remmina_log_scroll_to_end(gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkTextIter iter;
+
+ if (log_window) {
+ gtk_text_buffer_get_end_iter(REMMINA_LOG_WINDOW(log_window)->log_buffer, &iter);
+ gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(REMMINA_LOG_WINDOW(log_window)->log_view), &iter, 0.0, FALSE, 0.0,
+ 0.0);
+ }
+ return FALSE;
+}
+
+static gboolean remmina_log_print_real(gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkTextIter iter;
+
+ if (log_window && logstart) {
+ gtk_text_buffer_get_end_iter(REMMINA_LOG_WINDOW(log_window)->log_buffer, &iter);
+ gtk_text_buffer_insert(REMMINA_LOG_WINDOW(log_window)->log_buffer, &iter, (const gchar*)data, -1);
+ IDLE_ADD(remmina_log_scroll_to_end, NULL);
+ }
+ g_free(data);
+ return FALSE;
+}
+
+// Only prints into Remmina's own debug window. (Not stdout!)
+// See _remmina_{debug, info, error, critical, warning}
+void remmina_log_print(const gchar *text)
+{
+ TRACE_CALL(__func__);
+ if (!log_window)
+ return;
+
+ IDLE_ADD(remmina_log_print_real, g_strdup(text));
+}
+
+void remmina_log_file_append(gchar *text) {
+ gchar *log_filename = g_build_filename(g_get_tmp_dir(), LOG_FILE_NAME, NULL);
+ FILE *log_file = fopen(log_filename, "a");
+ g_free(log_filename);
+
+ if (log_file != NULL)
+ {
+ gchar* text_log = g_strconcat(text, "\n", NULL);
+ fwrite(text_log, sizeof(char), strlen(text_log), log_file);
+ fclose(log_file);
+ g_free(text_log);
+ }
+}
+
+void _remmina_info(const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+
+ va_list args;
+ g_autofree gchar *text;
+ va_start(args, fmt);
+ text = g_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ // Append text to remmina_log_file.log
+ remmina_log_file_append(text);
+
+ // always appends newline
+ g_info ("%s", text);
+
+ g_autofree gchar *buf_tmp = g_strconcat(text, "\n", NULL);
+ /* freed in remmina_log_print_real */
+ gchar *bufn = g_strconcat("(INFO) - ", buf_tmp, NULL);
+
+ if (!log_window) {
+ free(bufn);
+ return;
+ }
+ IDLE_ADD(remmina_log_print_real, bufn);
+}
+
+void _remmina_message(const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+
+ va_list args;
+ g_autofree gchar *text;
+ va_start(args, fmt);
+ text = g_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ // Append text to remmina_log_file.log
+ remmina_log_file_append(text);
+
+ // always appends newline
+ g_message ("%s", text);
+
+ if (!log_window) {
+ return;
+ }
+
+ g_autofree gchar *buf_tmp = g_strconcat(text, "\n", NULL);
+ /* freed in remmina_log_print_real */
+ gchar *bufn = g_strconcat("(MESSAGE) - ", buf_tmp, NULL);
+
+ IDLE_ADD(remmina_log_print_real, bufn);
+}
+
+/**
+ * Print a string in the Remmina Debug Windows and in the terminal.
+ * The string will be visible in the terminal if G_MESSAGES_DEBUG=remmina
+ * Variadic function of REMMINA_DEBUG
+ */
+void _remmina_debug(const gchar *fun, const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+
+ va_list args;
+ gchar *text;
+ va_start(args, fmt);
+ text = g_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ // Append text to remmina_log_file.log
+ remmina_log_file_append(text);
+
+ g_autofree gchar *buf = g_strconcat("(", fun, ") - ", text, NULL);
+ g_free(text);
+
+ // always appends newline
+ g_debug ("%s", buf);
+
+ if (!log_window) {
+ return;
+ }
+
+ g_autofree gchar *buf_tmp = g_strconcat(buf, "\n", NULL);
+ /* freed in remmina_log_print_real */
+ gchar *bufn = g_strconcat("(DEBUG) - ", buf_tmp, NULL);
+
+ IDLE_ADD(remmina_log_print_real, bufn);
+}
+
+void _remmina_warning(const gchar *fun, const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+
+ va_list args;
+ gchar *text;
+ va_start(args, fmt);
+ text = g_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ // Append text to remmina_log_file.log
+ remmina_log_file_append(text);
+
+ g_autofree gchar *buf = g_strconcat("(", fun, ") - ", text, NULL);
+ g_free(text);
+
+ // always appends newline
+ g_warning ("%s", buf);
+
+ if (!log_window) {
+ return;
+ }
+
+ g_autofree gchar *buf_tmp = g_strconcat(buf, "\n", NULL);
+ /* freed in remmina_log_print_real */
+ gchar *bufn = g_strconcat("(WARN) - ", buf_tmp, NULL);
+
+ IDLE_ADD(remmina_log_print_real, bufn);
+}
+
+void _remmina_audit(const gchar *fun, const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+ va_list args;
+ va_start(args, fmt);
+ gchar *text = g_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ // Append text to remmina_log_file.log
+ remmina_log_file_append(text);
+
+#if GLIB_CHECK_VERSION(2,62,0)
+ GDateTime* tv = g_date_time_new_now_local();
+ gchar *isodate = g_date_time_format_iso8601(tv);
+ g_date_time_unref(tv);
+#else
+ GTimeVal tv;
+ g_get_current_time(&tv);
+ gchar *isodate = g_time_val_to_iso8601(&tv);
+#endif
+
+ g_autofree gchar *buf = g_strdup("");
+
+ if (isodate) {
+
+ buf = g_strconcat(
+ "[", isodate, "] - ",
+ g_get_host_name (),
+ " - ",
+ g_get_user_name (),
+ " - ",
+ text,
+ NULL);
+
+ }
+
+ g_free(text);
+ if (remmina_pref_get_boolean("audit"))
+ _remmina_message(buf);
+ else
+ _remmina_debug(fun, buf);
+}
+
+// !!! Calling this function will crash Remmina !!!
+// !!! purposefully and send a trap signal !!!
+void _remmina_error(const gchar *fun, const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+
+ va_list args;
+ gchar *text;
+ va_start(args, fmt);
+ text = g_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ // Append text to remmina_log_file.log
+ remmina_log_file_append(text);
+
+ g_autofree gchar *buf = g_strconcat("(", fun, ") - ", text, NULL);
+ g_free(text);
+
+ // always appends newline
+ g_error ("%s", buf);
+
+ if (!log_window) {
+ return;
+ }
+
+ g_autofree gchar *buf_tmp = g_strconcat(buf, "\n", NULL);
+ /* freed in remmina_log_print_real */
+ gchar *bufn = g_strconcat("(ERROR) - ", buf_tmp, NULL);
+
+ IDLE_ADD(remmina_log_print_real, bufn);
+}
+
+void _remmina_critical(const gchar *fun, const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+
+ va_list args;
+ gchar *text;
+ va_start(args, fmt);
+ text = g_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ // Append text to remmina_log_file.log
+ remmina_log_file_append(text);
+
+ g_autofree gchar *buf = g_strconcat("(", fun, ") - ", text, NULL);
+ g_free(text);
+
+ // always appends newline
+ g_critical ("%s", buf);
+
+ if (!log_window) {
+ return;
+ }
+
+ g_autofree gchar *buf_tmp = g_strconcat(buf, "\n", NULL);
+ /* freed in remmina_log_print_real */
+ gchar *bufn = g_strconcat("(CRIT) - ", buf_tmp, NULL);
+
+ IDLE_ADD(remmina_log_print_real, bufn);
+}
+
+// Only prints into Remmina's own debug window. (Not stdout!)
+// See _remmina_{message, info, debug warning, error, critical}
+void remmina_log_printf(const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+ va_list args;
+ gchar *text;
+
+ if (!log_window) return;
+
+ va_start(args, fmt);
+ text = g_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ IDLE_ADD(remmina_log_print_real, text);
+}
+
+static gboolean remmina_log_on_keypress(GtkWidget *widget, GdkEvent *event, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ if (!log_window)
+ return FALSE;
+
+ GdkEventKey *e = (GdkEventKey *)event;
+
+ if ((e->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK) {
+ if (e->keyval == GDK_KEY_t) {
+ remmina_log_stats();
+ }
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void remmina_log_window_init(RemminaLogWindow *logwin)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *scrolledwindow;
+ GtkWidget *widget;
+
+ gtk_container_set_border_width(GTK_CONTAINER(logwin), 4);
+
+ scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_show(scrolledwindow);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
+ gtk_container_add(GTK_CONTAINER(logwin), scrolledwindow);
+
+ widget = gtk_text_view_new();
+ gtk_widget_show(widget);
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget), GTK_WRAP_WORD_CHAR);
+ gtk_text_view_set_editable(GTK_TEXT_VIEW(widget), FALSE);
+ gtk_text_view_set_monospace(GTK_TEXT_VIEW(widget), TRUE);
+ gtk_container_add(GTK_CONTAINER(scrolledwindow), widget);
+ logwin->log_view = widget;
+ logwin->log_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
+
+ g_signal_connect(G_OBJECT(logwin->log_view), "key-press-event", G_CALLBACK(remmina_log_on_keypress), (gpointer)logwin);
+}
+
diff --git a/src/remmina_log.h b/src/remmina_log.h
new file mode 100644
index 0000000..d3d4a75
--- /dev/null
+++ b/src/remmina_log.h
@@ -0,0 +1,66 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2010 Vic Lee
+ * Copyright (C) 2017-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.
+ *
+ */
+
+#pragma once
+
+#include <stdarg.h>
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+#define LOG_FILE_NAME "remmina_log_file.log"
+#define REMMINA_INFO(fmt, ...) _remmina_info(fmt, ## __VA_ARGS__)
+#define REMMINA_MESSAGE(fmt, ...) _remmina_message(fmt, ## __VA_ARGS__)
+#define REMMINA_DEBUG(fmt, ...) _remmina_debug(__func__, fmt, ## __VA_ARGS__)
+#define REMMINA_WARNING(fmt, ...) _remmina_warning(__func__, fmt, ## __VA_ARGS__)
+#define REMMINA_AUDIT(fmt, ...) _remmina_audit(__func__, fmt, ## __VA_ARGS__)
+#define REMMINA_ERROR(fmt, ...) _remmina_error(__func__, fmt, ## __VA_ARGS__)
+#define REMMINA_CRITICAL(fmt, ...) _remmina_critical(__func__, fmt, ## __VA_ARGS__)
+
+void remmina_log_start(void);
+gboolean remmina_log_running(void);
+void remmina_log_print(const gchar *text);
+void remmina_log_file_append(gchar *text);
+void _remmina_info(const gchar *fmt, ...);
+void _remmina_message(const gchar *fmt, ...);
+void _remmina_debug(const gchar *fun, const gchar *fmt, ...);
+void _remmina_warning(const gchar *fun, const gchar *fmt, ...);
+void _remmina_audit(const gchar *fun, const gchar *fmt, ...);
+void _remmina_error(const gchar *fun, const gchar *fmt, ...);
+void _remmina_critical(const gchar *fun, const gchar *fmt, ...);
+void remmina_log_printf(const gchar *fmt, ...);
+
+G_END_DECLS
diff --git a/src/remmina_main.c b/src/remmina_main.c
new file mode 100644
index 0000000..81c78f3
--- /dev/null
+++ b/src/remmina_main.c
@@ -0,0 +1,1844 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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
+ * 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.
+ *
+ */
+
+#include "config.h"
+#include <ctype.h>
+#include <gio/gio.h>
+#ifndef __APPLE__
+#include <gio/gdesktopappinfo.h>
+#endif
+#include <gdk/gdkkeysyms.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "remmina.h"
+#include "remmina_string_array.h"
+#include "remmina_public.h"
+#include "remmina_file.h"
+#include "remmina_file_manager.h"
+#include "remmina_file_editor.h"
+#include "rcw.h"
+#include "remmina_about.h"
+#include "remmina_pref.h"
+#include "remmina_pref_dialog.h"
+#include "remmina_widget_pool.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_bug_report.h"
+#include "remmina_log.h"
+#include "remmina_icon.h"
+#include "remmina_main.h"
+#include "remmina_exec.h"
+#include "remmina_mpchange.h"
+#include "remmina_external_tools.h"
+#include "remmina_unlock.h"
+#include "remmina/remmina_trace_calls.h"
+
+static RemminaMain *remminamain;
+
+#define RM_GET_OBJECT(object_name) gtk_builder_get_object(remminamain->builder, object_name)
+
+enum {
+ PROTOCOL_COLUMN,
+ NAME_COLUMN,
+ GROUP_COLUMN,
+ SERVER_COLUMN,
+ PLUGIN_COLUMN,
+ DATE_COLUMN,
+ FILENAME_COLUMN,
+ LABELS_COLUMN,
+ NOTES_COLUMN,
+ N_COLUMNS
+};
+
+static
+const gchar *supported_mime_types[] = {
+ "x-scheme-handler/rdp",
+ "x-scheme-handler/spice",
+ "x-scheme-handler/vnc",
+ "x-scheme-handler/remmina",
+ "application/x-remmina",
+ NULL
+};
+
+static GActionEntry app_actions[] = {
+ { "about", remmina_main_on_action_application_about, NULL, NULL, NULL },
+ { "default", remmina_main_on_action_application_default, NULL, NULL, NULL },
+ { "mpchange", remmina_main_on_action_application_mpchange, NULL, NULL, NULL },
+ { "plugins", remmina_main_on_action_application_plugins, NULL, NULL, NULL },
+ { "preferences", remmina_main_on_action_application_preferences, "i", NULL, NULL },
+ { "bug_report", remmina_main_on_action_application_bug_report, NULL, NULL, NULL},
+ { "dark", remmina_main_on_action_application_dark_theme, NULL, NULL, NULL },
+ { "debug", remmina_main_on_action_help_debug, NULL, NULL, NULL },
+ { "community", remmina_main_on_action_help_community, NULL, NULL, NULL },
+ { "donations", remmina_main_on_action_help_donations, NULL, NULL, NULL },
+ { "homepage", remmina_main_on_action_help_homepage, NULL, NULL, NULL },
+ { "wiki", remmina_main_on_action_help_wiki, NULL, NULL, NULL },
+ { "quit", remmina_main_on_action_application_quit, NULL, NULL, NULL },
+};
+
+static GActionEntry main_actions[] = {
+ { "connect", remmina_main_on_action_connection_connect, NULL, NULL, NULL },
+ { "copy", remmina_main_on_action_connection_copy, NULL, NULL, NULL },
+ { "delete", remmina_main_on_action_connection_delete, NULL, NULL, NULL },
+ { "delete_multiple", remmina_main_on_action_connection_delete_multiple, NULL, NULL, NULL },
+ { "edit", remmina_main_on_action_connection_edit, NULL, NULL, NULL },
+ { "exttools", remmina_main_on_action_connection_external_tools, NULL, NULL, NULL },
+ { "new", remmina_main_on_action_connection_new, NULL, NULL, NULL },
+ { "export", remmina_main_on_action_tools_export, NULL, NULL, NULL },
+ { "import", remmina_main_on_action_tools_import, NULL, NULL, NULL },
+ { "expand", remmina_main_on_action_expand, NULL, NULL, NULL },
+ { "collapse", remmina_main_on_action_collapse, NULL, NULL, NULL },
+ { "search", remmina_main_on_action_search_toggle, NULL, NULL, NULL },
+};
+
+static GtkTargetEntry remmina_drop_types[] =
+{
+ { "text/uri-list", 0, 1 }
+};
+
+static char *quick_connect_plugin_list[] =
+{
+ "RDP", "VNC", "SSH", "NX", "SPICE", "X2GO"
+};
+
+/**
+ * Save the Remmina Main Window size to assure the main geometry at each restart
+ */
+static void remmina_main_save_size(void)
+{
+ TRACE_CALL(__func__);
+ if ((gdk_window_get_state(gtk_widget_get_window(GTK_WIDGET(remminamain->window))) & GDK_WINDOW_STATE_MAXIMIZED) == 0) {
+ gtk_window_get_size(remminamain->window, &remmina_pref.main_width, &remmina_pref.main_height);
+ remmina_pref.main_maximize = FALSE;
+ } else {
+ remmina_pref.main_maximize = TRUE;
+ }
+}
+
+static void remmina_main_save_expanded_group_func(GtkTreeView *tree_view, GtkTreePath *path, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+ gchar *group;
+
+ gtk_tree_model_get_iter(remminamain->priv->file_model_sort, &iter, path);
+ gtk_tree_model_get(remminamain->priv->file_model_sort, &iter, GROUP_COLUMN, &group, -1);
+ if (group) {
+ remmina_string_array_add(remminamain->priv->expanded_group, group);
+ g_free(group);
+ }
+}
+
+static void remmina_main_save_expanded_group(void)
+{
+ TRACE_CALL(__func__);
+ if (GTK_IS_TREE_STORE(remminamain->priv->file_model)) {
+ if (remminamain->priv->expanded_group)
+ remmina_string_array_free(remminamain->priv->expanded_group);
+ remminamain->priv->expanded_group = remmina_string_array_new();
+ gtk_tree_view_map_expanded_rows(remminamain->tree_files_list,
+ (GtkTreeViewMappingFunc)remmina_main_save_expanded_group_func, NULL);
+ }
+}
+
+/**
+ * Save the Remmina Main Window size and the expanded group before to close Remmina.
+ * This function uses remmina_main_save_size and remmina_main_save_expanded_group.
+ */
+void remmina_main_save_before_destroy()
+{
+ TRACE_CALL(__func__);
+ if (!remminamain || !remminamain->window)
+ return;
+
+ remmina_main_save_size();
+ remmina_main_save_expanded_group();
+ g_free(remmina_pref.expanded_group);
+ remmina_pref.expanded_group = remmina_string_array_to_string(remminamain->priv->expanded_group);
+ remmina_pref_save();
+}
+
+void remmina_main_destroy()
+{
+ TRACE_CALL(__func__);
+
+ if (remminamain) {
+ if (remminamain->window)
+ gtk_widget_destroy(GTK_WIDGET(remminamain->window));
+
+ g_object_unref(remminamain->builder);
+ remmina_string_array_free(remminamain->priv->expanded_group);
+ remminamain->priv->expanded_group = NULL;
+ if (remminamain->priv->file_model)
+ g_object_unref(G_OBJECT(remminamain->priv->file_model));
+ g_object_unref(G_OBJECT(remminamain->priv->file_model_filter));
+ g_free(remminamain->priv->selected_filename);
+ g_free(remminamain->priv->selected_name);
+ g_free(remminamain->priv);
+ g_free(remminamain);
+ remminamain = NULL;
+ }
+}
+
+/**
+ * Try to exit remmina after a delete window event
+ */
+static gboolean remmina_main_dexit(gpointer data)
+{
+ TRACE_CALL(__func__);
+ remmina_application_condexit(REMMINA_CONDEXIT_ONMAINWINDELETE);
+ return FALSE;
+}
+
+gboolean remmina_main_on_delete_event(GtkWidget *widget, GdkEvent *event, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ remmina_main_save_before_destroy();
+
+ g_idle_add(remmina_main_dexit, NULL);
+
+ return FALSE;
+}
+
+gboolean remmina_main_idle_destroy(gpointer data)
+{
+ TRACE_CALL(__func__);
+
+ if (remminamain)
+ remmina_main_destroy();
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * Called when the remminamain->window widget is destroyed (glade event handler)
+ */
+void remmina_main_on_destroy_event()
+{
+ TRACE_CALL(__func__);
+
+ if (remminamain) {
+ /* Invalidate remminamain->window to avoid multiple destructions */
+ remminamain->window = NULL;
+ /* Destroy remminamain struct, later. We can't destroy
+ * important objects like the builder now */
+ g_idle_add(remmina_main_idle_destroy, NULL);
+ }
+}
+
+static void remmina_main_clear_selection_data(void)
+{
+ TRACE_CALL(__func__);
+ g_free(remminamain->priv->selected_filename);
+ g_free(remminamain->priv->selected_name);
+ remminamain->priv->selected_filename = NULL;
+ remminamain->priv->selected_name = NULL;
+}
+
+#ifdef SNAP_BUILD
+
+static void remmina_main_show_snap_welcome()
+{
+ GtkBuilder *dlgbuilder = NULL;
+ GtkWidget *dlg;
+ GtkWindow *parent;
+ int result;
+ static gboolean shown_once = FALSE;
+ gboolean need_snap_interface_connections = FALSE;
+ GtkWidget *dsa;
+ RemminaSecretPlugin *remmina_secret_plugin;
+
+ if (shown_once)
+ return;
+ else
+ shown_once = TRUE;
+
+ g_print("Remmina is compiled as a SNAP package.\n");
+ remmina_secret_plugin = remmina_plugin_manager_get_secret_plugin();
+ if (remmina_secret_plugin == NULL) {
+ g_print(" but we can’t find the secret plugin inside the SNAP.\n");
+ need_snap_interface_connections = TRUE;
+ } else {
+ if (!remmina_secret_plugin->is_service_available(remmina_secret_plugin)) {
+ g_print(" but we can’t access a secret service. Secret service or SNAP interface connection is missing.\n");
+ need_snap_interface_connections = TRUE;
+ }
+ }
+
+ if (need_snap_interface_connections && !remmina_pref.prevent_snap_welcome_message) {
+ dlgbuilder = remmina_public_gtk_builder_new_from_resource("/org/remmina/Remmina/src/../data/ui/remmina_snap_info_dialog.glade");
+ dsa = GTK_WIDGET(gtk_builder_get_object(dlgbuilder, "dontshowagain"));
+ if (dlgbuilder) {
+ parent = remmina_main_get_window();
+ dlg = GTK_WIDGET(gtk_builder_get_object(dlgbuilder, "SnapInfoDlg"));
+ if (parent)
+ gtk_window_set_transient_for(GTK_WINDOW(dlg), parent);
+ gtk_builder_connect_signals(dlgbuilder, NULL);
+ result = gtk_dialog_run(GTK_DIALOG(dlg));
+ if (result == 1) {
+ remmina_pref.prevent_snap_welcome_message = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(dsa));
+ remmina_pref_save();
+ }
+ gtk_widget_destroy(dlg);
+ g_object_unref(dlgbuilder);
+ }
+ }
+}
+#endif
+
+
+static gboolean remmina_main_selection_func(GtkTreeSelection *selection, GtkTreeModel *model, GtkTreePath *path,
+ gboolean path_currently_selected, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ guint context_id;
+ GtkTreeIter iter;
+ gchar buf[1000];
+
+ if (path_currently_selected)
+ return TRUE;
+
+ if (!gtk_tree_model_get_iter(model, &iter, path))
+ return TRUE;
+
+ remmina_main_clear_selection_data();
+
+ gtk_tree_model_get(model, &iter,
+ NAME_COLUMN, &remminamain->priv->selected_name,
+ FILENAME_COLUMN, &remminamain->priv->selected_filename,
+ -1);
+
+ context_id = gtk_statusbar_get_context_id(remminamain->statusbar_main, "status");
+ gtk_statusbar_pop(remminamain->statusbar_main, context_id);
+ if (remminamain->priv->selected_filename) {
+ g_snprintf(buf, sizeof(buf), "%s (%s)", remminamain->priv->selected_name, remminamain->priv->selected_filename);
+ gtk_statusbar_push(remminamain->statusbar_main, context_id, buf);
+ } else {
+ gtk_statusbar_push(remminamain->statusbar_main, context_id, remminamain->priv->selected_name);
+ }
+
+ return TRUE;
+}
+
+static void remmina_main_load_file_list_callback(RemminaFile *remminafile, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+ GtkListStore *store;
+
+ store = GTK_LIST_STORE(user_data);
+ gchar *datetime;
+
+ datetime = remmina_file_get_datetime(remminafile);
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter,
+ PROTOCOL_COLUMN, remmina_file_get_icon_name(remminafile),
+ NAME_COLUMN, remmina_file_get_string(remminafile, "name"),
+ NOTES_COLUMN, g_uri_unescape_string(remmina_file_get_string(remminafile, "notes_text"), NULL),
+ GROUP_COLUMN, remmina_file_get_string(remminafile, "group"),
+ SERVER_COLUMN, remmina_file_get_string(remminafile, "server"),
+ PLUGIN_COLUMN, remmina_file_get_string(remminafile, "protocol"),
+ DATE_COLUMN, datetime,
+ FILENAME_COLUMN, remmina_file_get_filename(remminafile),
+ LABELS_COLUMN, remmina_file_get_string(remminafile, "labels"),
+ -1);
+ g_free(datetime);
+}
+
+static gboolean remmina_main_load_file_tree_traverse(GNode *node, GtkTreeStore *store, GtkTreeIter *parent)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter *iter;
+ RemminaGroupData *data;
+ GNode *child;
+
+ iter = NULL;
+ if (node->data) {
+ data = (RemminaGroupData *)node->data;
+ iter = g_new0(GtkTreeIter, 1);
+ gtk_tree_store_append(store, iter, parent);
+ gtk_tree_store_set(store, iter,
+ PROTOCOL_COLUMN, "folder-symbolic",
+ NAME_COLUMN, data->name,
+ GROUP_COLUMN, data->group,
+ DATE_COLUMN, data->datetime,
+ FILENAME_COLUMN, NULL,
+ LABELS_COLUMN, data->labels,
+ -1);
+ }
+ for (child = g_node_first_child(node); child; child = g_node_next_sibling(child))
+ remmina_main_load_file_tree_traverse(child, store, iter);
+ g_free(iter);
+ return FALSE;
+}
+
+static void remmina_main_load_file_tree_group(GtkTreeStore *store)
+{
+ TRACE_CALL(__func__);
+ GNode *root;
+
+ root = remmina_file_manager_get_group_tree();
+ remmina_main_load_file_tree_traverse(root, store, NULL);
+ remmina_file_manager_free_group_tree(root);
+}
+
+static void remmina_main_expand_group_traverse(GtkTreeIter *iter)
+{
+ TRACE_CALL(__func__);
+ GtkTreeModel *tree;
+ gboolean ret;
+ gchar *group, *filename;
+ GtkTreeIter child;
+ GtkTreePath *path;
+
+ tree = remminamain->priv->file_model_sort;
+ ret = TRUE;
+ while (ret) {
+ gtk_tree_model_get(tree, iter, GROUP_COLUMN, &group, FILENAME_COLUMN, &filename, -1);
+ if (filename == NULL) {
+ if (remmina_string_array_find(remminamain->priv->expanded_group, group) >= 0) {
+ path = gtk_tree_model_get_path(tree, iter);
+ gtk_tree_view_expand_row(remminamain->tree_files_list, path, FALSE);
+ gtk_tree_path_free(path);
+ }
+ if (gtk_tree_model_iter_children(tree, &child, iter))
+ remmina_main_expand_group_traverse(&child);
+ }
+ g_free(group);
+ g_free(filename);
+
+ ret = gtk_tree_model_iter_next(tree, iter);
+ }
+}
+
+static void remmina_main_expand_group(void)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+
+ if (gtk_tree_model_get_iter_first(remminamain->priv->file_model_sort, &iter))
+ remmina_main_expand_group_traverse(&iter);
+}
+
+static gboolean remmina_main_load_file_tree_find(GtkTreeModel *tree, GtkTreeIter *iter, const gchar *match_group)
+{
+ TRACE_CALL(__func__);
+ gboolean ret, match;
+ gchar *group, *filename;
+ GtkTreeIter child;
+
+ match = FALSE;
+ ret = TRUE;
+ while (ret) {
+ gtk_tree_model_get(tree, iter, GROUP_COLUMN, &group, FILENAME_COLUMN, &filename, -1);
+ match = (filename == NULL && g_strcmp0(group, match_group) == 0);
+ g_free(group);
+ g_free(filename);
+ if (match)
+ break;
+ if (gtk_tree_model_iter_children(tree, &child, iter)) {
+ match = remmina_main_load_file_tree_find(tree, &child, match_group);
+ if (match) {
+ memcpy(iter, &child, sizeof(GtkTreeIter));
+ break;
+ }
+ }
+ ret = gtk_tree_model_iter_next(tree, iter);
+ }
+ return match;
+}
+
+static void remmina_main_load_file_tree_callback(RemminaFile *remminafile, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter, child;
+ GtkTreeStore *store;
+ gboolean found;
+ gchar *datetime = NULL;
+
+ store = GTK_TREE_STORE(user_data);
+
+ found = FALSE;
+ if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter))
+ found = remmina_main_load_file_tree_find(GTK_TREE_MODEL(store), &iter,
+ remmina_file_get_string(remminafile, "group"));
+
+ datetime = remmina_file_get_datetime(remminafile);
+ //REMMINA_DEBUG("The date is %s", datetime);
+ gtk_tree_store_append(store, &child, (found ? &iter : NULL));
+ gtk_tree_store_set(store, &child,
+ PROTOCOL_COLUMN, remmina_file_get_icon_name(remminafile),
+ NAME_COLUMN, remmina_file_get_string(remminafile, "name"),
+ NOTES_COLUMN, g_uri_unescape_string(remmina_file_get_string(remminafile, "notes_text"), NULL),
+ GROUP_COLUMN, remmina_file_get_string(remminafile, "group"),
+ SERVER_COLUMN, remmina_file_get_string(remminafile, "server"),
+ PLUGIN_COLUMN, remmina_file_get_string(remminafile, "protocol"),
+ DATE_COLUMN, datetime,
+ FILENAME_COLUMN, remmina_file_get_filename(remminafile),
+ LABELS_COLUMN, remmina_file_get_string(remminafile, "labels"),
+ -1);
+ g_free(datetime);
+}
+
+static void remmina_main_file_model_on_sort(GtkTreeSortable *sortable, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gint columnid;
+ GtkSortType order;
+
+ gtk_tree_sortable_get_sort_column_id(sortable, &columnid, &order);
+ remmina_pref.main_sort_column_id = columnid;
+ remmina_pref.main_sort_order = order;
+ remmina_pref_save();
+}
+
+static gboolean remmina_main_filter_visible_func(GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gchar *text;
+ gchar *protocol, *name, *labels, *group, *server, *plugin, *date, *s;
+ gboolean result = TRUE;
+
+ text = g_ascii_strdown(gtk_entry_get_text(remminamain->entry_quick_connect_server), -1);
+ if (text && text[0]) {
+ gtk_tree_model_get(model, iter,
+ PROTOCOL_COLUMN, &protocol,
+ NAME_COLUMN, &name,
+ GROUP_COLUMN, &group,
+ SERVER_COLUMN, &server,
+ PLUGIN_COLUMN, &plugin,
+ DATE_COLUMN, &date,
+ LABELS_COLUMN, &labels,
+ -1);
+ if (g_strcmp0(protocol, "folder-symbolic") != 0) {
+ s = g_ascii_strdown(name ? name : "", -1);
+ g_free(name);
+ name = s;
+ s = g_ascii_strdown(group ? group : "", -1);
+ g_free(group);
+ group = s;
+ s = g_ascii_strdown(server ? server : "", -1);
+ g_free(server);
+ server = s;
+ s = g_ascii_strdown(plugin ? plugin : "", -1);
+ g_free(plugin);
+ plugin = s;
+ s = g_ascii_strdown(date ? date : "", -1);
+ g_free(date);
+ date = s;
+ result = (strstr(name, text) || strstr(group, text) || strstr(server, text) || strstr(plugin, text) || strstr(date, text));
+
+ // Filter by labels
+
+ s = g_ascii_strdown(labels ? labels : "", -1);
+ g_free(labels);
+ labels = s;
+
+ if (strlen(labels) > 0) {
+ gboolean labels_result = TRUE;
+ gchar **labels_array = g_strsplit(labels, ",", -1);
+ gchar **text_array = g_strsplit(text, ",", -1);
+
+ for (int t = 0; (NULL != text_array[t]); t++) {
+ if (0 == strlen(text_array[t])) {
+ continue;
+ }
+
+ gboolean text_result = FALSE;
+
+ for (int l = 0; (NULL != labels_array[l]); l++) {
+ if (0 == strlen(labels_array[l])) {
+ continue;
+ }
+
+ text_result = (text_result || strstr(labels_array[l], text_array[t]));
+
+ if (text_result) {
+ break;
+ }
+ }
+
+ labels_result = (labels_result && text_result);
+
+ if (!labels_result) {
+ break;
+ }
+ }
+
+ result = (result || labels_result);
+
+ g_strfreev(labels_array);
+ g_strfreev(text_array);
+ }
+ }
+ g_free(protocol);
+ g_free(name);
+ g_free(labels);
+ g_free(group);
+ g_free(server);
+ g_free(plugin);
+ g_free(date);
+ }
+ g_free(text);
+ return result;
+}
+
+static void remmina_main_select_file(const gchar *filename)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+ GtkTreePath *path;
+ gchar *item_filename;
+ gboolean cmp;
+
+ if (!gtk_tree_model_get_iter_first(remminamain->priv->file_model_sort, &iter))
+ return;
+
+ while (TRUE) {
+ gtk_tree_model_get(remminamain->priv->file_model_sort, &iter, FILENAME_COLUMN, &item_filename, -1);
+ cmp = g_strcmp0(item_filename, filename);
+ g_free(item_filename);
+ if (cmp == 0) {
+ gtk_tree_selection_select_iter(gtk_tree_view_get_selection(remminamain->tree_files_list),
+ &iter);
+ path = gtk_tree_model_get_path(remminamain->priv->file_model_sort, &iter);
+ gtk_tree_view_scroll_to_cell(remminamain->tree_files_list, path, NULL, TRUE, 0.5, 0.0);
+ gtk_tree_path_free(path);
+ return;
+ }
+ if (!gtk_tree_model_iter_next(remminamain->priv->file_model_sort, &iter))
+ return;
+ }
+}
+
+static void remmina_main_load_files()
+{
+ TRACE_CALL(__func__);
+ gint items_count;
+ gchar buf[200];
+ guint context_id;
+ gint view_file_mode;
+ gboolean always_show_notes;
+ char *save_selected_filename;
+ GtkTreeModel *newmodel;
+ const gchar *neticon;
+ const gchar *connection_tooltip;
+
+ save_selected_filename = g_strdup(remminamain->priv->selected_filename);
+ remmina_main_save_expanded_group();
+
+ view_file_mode = remmina_pref.view_file_mode;
+ if (remminamain->priv->override_view_file_mode_to_list)
+ view_file_mode = REMMINA_VIEW_FILE_LIST;
+
+ switch (remmina_pref.view_file_mode) {
+ case REMMINA_VIEW_FILE_TREE:
+ gtk_toggle_button_set_active(remminamain->view_toggle_button, FALSE);
+ break;
+ case REMMINA_VIEW_FILE_LIST:
+ default:
+ gtk_toggle_button_set_active(remminamain->view_toggle_button, TRUE);
+ break;
+ }
+
+ switch (view_file_mode) {
+ case REMMINA_VIEW_FILE_TREE:
+ /* Create new GtkTreeStore model */
+ newmodel = GTK_TREE_MODEL(gtk_tree_store_new(9, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING));
+ /* Hide the Group column in the tree view mode */
+ gtk_tree_view_column_set_visible(remminamain->column_files_list_group, FALSE);
+ /* Load groups first */
+ remmina_main_load_file_tree_group(GTK_TREE_STORE(newmodel));
+ /* Load files list */
+ items_count = remmina_file_manager_iterate((GFunc)remmina_main_load_file_tree_callback, (gpointer)newmodel);
+ break;
+
+ case REMMINA_VIEW_FILE_LIST:
+ default:
+ /* Create new GtkListStore model */
+ newmodel = GTK_TREE_MODEL(gtk_list_store_new(9, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING));
+ /* Show the Group column in the list view mode */
+ gtk_tree_view_column_set_visible(remminamain->column_files_list_group, TRUE);
+ /* Load files list */
+ items_count = remmina_file_manager_iterate((GFunc)remmina_main_load_file_list_callback, (gpointer)newmodel);
+ break;
+ }
+
+ /* Set note column visibility*/
+ always_show_notes = remmina_pref.always_show_notes;
+ if (!always_show_notes){
+ gtk_tree_view_column_set_visible(remminamain->column_files_list_notes, FALSE);
+ }
+
+ /* Unset old model */
+ gtk_tree_view_set_model(remminamain->tree_files_list, NULL);
+
+ /* Destroy the old model and save the new one */
+ remminamain->priv->file_model = newmodel;
+
+ /* Create a sorted filtered model based on newmodel and apply it to the TreeView */
+ remminamain->priv->file_model_filter = gtk_tree_model_filter_new(remminamain->priv->file_model, NULL);
+ gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(remminamain->priv->file_model_filter),
+ (GtkTreeModelFilterVisibleFunc)remmina_main_filter_visible_func, NULL, NULL);
+ remminamain->priv->file_model_sort = gtk_tree_model_sort_new_with_model(remminamain->priv->file_model_filter);
+ gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(remminamain->priv->file_model_sort),
+ remmina_pref.main_sort_column_id,
+ remmina_pref.main_sort_order);
+ gtk_tree_view_set_model(remminamain->tree_files_list, remminamain->priv->file_model_sort);
+ g_signal_connect(G_OBJECT(remminamain->priv->file_model_sort), "sort-column-changed",
+ G_CALLBACK(remmina_main_file_model_on_sort), NULL);
+ remmina_main_expand_group();
+ /* Select the file previously selected */
+ if (save_selected_filename) {
+ remmina_main_select_file(save_selected_filename);
+ g_free(save_selected_filename);
+ }
+ gtk_tree_view_column_set_widget(remminamain->column_files_list_date, NULL);
+
+ GtkWidget *label = gtk_tree_view_column_get_button(remminamain->column_files_list_date);
+
+ gtk_widget_set_tooltip_text(GTK_WIDGET(label),
+ _("The latest successful connection attempt, or a pre-computed date"));
+ /* Show in the status bar the total number of connections found */
+ g_snprintf(buf, sizeof(buf), ngettext("Total %i item.", "Total %i items.", items_count), items_count);
+ context_id = gtk_statusbar_get_context_id(remminamain->statusbar_main, "status");
+ gtk_statusbar_pop(remminamain->statusbar_main, context_id);
+ gtk_statusbar_push(remminamain->statusbar_main, context_id, buf);
+
+ remmina_network_monitor_status (remminamain->monitor);
+ if (remminamain->monitor->connected){
+ neticon = g_strdup("network-transmit-receive-symbolic");
+ connection_tooltip = g_strdup(_("Network status: fully online"));
+ } else {
+ neticon = g_strdup("network-offline-symbolic");
+ connection_tooltip = g_strdup(_("Network status: offline"));
+ }
+
+ if (GTK_IS_WIDGET(remminamain->network_icon))
+ gtk_widget_destroy(remminamain->network_icon);
+ GIcon *icon = g_themed_icon_new (neticon);
+ remminamain->network_icon = gtk_image_new_from_gicon (icon, GTK_ICON_SIZE_BUTTON);
+ gtk_widget_set_tooltip_text (remminamain->network_icon, connection_tooltip);
+
+ g_object_unref (icon);
+
+ gtk_box_pack_start (GTK_BOX(remminamain->statusbar_main), remminamain->network_icon, FALSE, FALSE, 0);
+ gtk_widget_show (remminamain->network_icon);
+
+}
+
+void remmina_main_load_files_cb(GtkEntry *entry, char *string, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ remmina_main_load_files();
+}
+
+void remmina_main_on_action_connection_connect(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+
+ RemminaFile *remminafile;
+
+ if (!remminamain->priv->selected_filename)
+ return;
+
+ remminafile = remmina_file_load(remminamain->priv->selected_filename);
+
+ if (remminafile == NULL)
+ return;
+
+ if (remmina_pref_get_boolean("use_primary_password")
+ && remmina_pref_get_boolean("lock_connect")
+ && remmina_unlock_new(remminamain->window) == 0)
+ return;
+ if (remmina_file_get_int (remminafile, "profile-lock", FALSE) == 1
+ && remmina_unlock_new(remminamain->window) == 0)
+ return;
+
+ remmina_file_touch(remminafile);
+ rcw_open_from_filename(remminamain->priv->selected_filename);
+
+ remmina_file_free(remminafile);
+}
+
+void remmina_main_on_action_connection_external_tools(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ if (!remminamain->priv->selected_filename)
+ return;
+
+ remmina_external_tools_from_filename(remminamain, remminamain->priv->selected_filename);
+}
+
+static void remmina_main_file_editor_destroy(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ if (!remminamain)
+ return;
+ remmina_main_load_files();
+}
+
+void remmina_main_on_action_application_mpchange(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+
+ const gchar *username;
+ const gchar *domain;
+ const gchar *group;
+ const gchar *gatewayusername;
+ const gchar *gatewaydomain;
+
+ username = domain = group = gatewayusername = gatewaydomain = "";
+
+ remminafile = NULL;
+
+ if (remmina_pref_get_boolean("use_primary_password")
+ && remmina_pref_get_boolean("lock_edit")
+ && remmina_unlock_new(remminamain->window) == 0)
+ return;
+
+ if (remminamain->priv->selected_filename) {
+ remminafile = remmina_file_load(remminamain->priv->selected_filename);
+ if (remminafile != NULL) {
+ username = remmina_file_get_string(remminafile, "username");
+ domain = remmina_file_get_string(remminafile, "domain");
+ group = remmina_file_get_string(remminafile, "group");
+ gatewayusername = remmina_file_get_string(remminafile, "gateway_username");
+ gatewaydomain = remmina_file_get_string(remminafile, "gateway_domain");
+ }
+ }
+
+ remmina_mpchange_schedule(TRUE, group, domain, username, "", gatewayusername, gatewaydomain, "");
+
+ if (remminafile != NULL)
+ remmina_file_free(remminafile);
+}
+
+void remmina_main_on_action_connection_new(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ if (kioskmode && kioskmode == TRUE)
+ return;
+ GtkWidget *widget;
+
+ remmina_plugin_manager_get_available_plugins();
+ if (remmina_pref_get_boolean("use_primary_password")
+ && remmina_pref_get_boolean("lock_edit")
+ && remmina_unlock_new(remminamain->window) == 0)
+ return;
+
+ widget = remmina_file_editor_new();
+ g_signal_connect(G_OBJECT(widget), "destroy", G_CALLBACK(remmina_main_file_editor_destroy), remminamain);
+ gtk_window_set_transient_for(GTK_WINDOW(widget), remminamain->window);
+ gtk_widget_show(widget);
+ remmina_main_load_files();
+}
+
+static gboolean remmina_main_search_key_event(GtkWidget *search_entry, GdkEventKey *event, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ if (event->keyval == GDK_KEY_Escape) {
+ gtk_entry_set_text(remminamain->entry_quick_connect_server, "");
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(RM_GET_OBJECT("search_toggle")), FALSE);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gboolean remmina_main_tree_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ if (gtk_tree_view_row_expanded(tree, path))
+ gtk_tree_view_collapse_row(tree, path);
+ else
+ gtk_tree_view_expand_row(tree, path, FALSE);
+ return TRUE;
+}
+
+void remmina_main_on_view_toggle()
+{
+ if (gtk_toggle_button_get_active(remminamain->view_toggle_button)) {
+ if (remmina_pref.view_file_mode != REMMINA_VIEW_FILE_LIST) {
+ remmina_pref.view_file_mode = REMMINA_VIEW_FILE_LIST;
+ gtk_entry_set_text(remminamain->entry_quick_connect_server, "");
+ remmina_pref_save();
+ remmina_main_load_files();
+ }
+ } else {
+ if (remmina_pref.view_file_mode != REMMINA_VIEW_FILE_TREE) {
+ remmina_pref.view_file_mode = REMMINA_VIEW_FILE_TREE;
+ gtk_entry_set_text(remminamain->entry_quick_connect_server, "");
+ remmina_pref_save();
+ remmina_main_load_files();
+ }
+ }
+}
+
+void remmina_main_on_action_connection_copy(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+
+ if (remmina_pref_get_boolean("use_primary_password")
+ && remmina_unlock_new(remminamain->window) == 0)
+ return;
+
+ if (!remminamain->priv->selected_filename)
+ return;
+
+ RemminaFile *remminafile = remmina_file_load(remminamain->priv->selected_filename);
+
+ if (((remmina_pref_get_boolean("lock_edit")
+ && remmina_pref_get_boolean("use_primary_password"))
+ || remmina_file_get_int (remminafile, "profile-lock", FALSE))
+ && remmina_unlock_new(remminamain->window) == 0)
+ return;
+
+ if (remminafile) {
+ remmina_file_free(remminafile);
+ remminafile = NULL;
+ }
+
+ widget = remmina_file_editor_new_copy(remminamain->priv->selected_filename);
+ if (widget) {
+ g_signal_connect(G_OBJECT(widget), "destroy", G_CALLBACK(remmina_main_file_editor_destroy), remminamain);
+ gtk_window_set_transient_for(GTK_WINDOW(widget), remminamain->window);
+ gtk_widget_show(widget);
+ }
+ /* Select the file previously selected */
+ if (remminamain->priv->selected_filename)
+ remmina_main_select_file(remminamain->priv->selected_filename);
+}
+
+void remmina_main_on_action_connection_edit(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+
+ if (!remminamain->priv->selected_filename)
+ return;
+
+ RemminaFile *remminafile = remmina_file_load(remminamain->priv->selected_filename);
+
+ if (remmina_pref_get_boolean("use_primary_password")
+ && (remmina_pref_get_boolean("lock_edit")
+ || remmina_file_get_int (remminafile, "profile-lock", FALSE))
+ && remmina_unlock_new(remminamain->window) == 0)
+ return;
+
+ if (remminafile) {
+ remmina_file_free(remminafile);
+ remminafile = NULL;
+ }
+
+ widget = remmina_file_editor_new_from_filename(remminamain->priv->selected_filename);
+ if (widget) {
+ gtk_window_set_transient_for(GTK_WINDOW(widget), remminamain->window);
+ gtk_widget_show(widget);
+ }
+/* Select the file previously selected */
+ if (remminamain->priv->selected_filename)
+ remmina_main_select_file(remminamain->priv->selected_filename);
+}
+
+void remmina_main_on_action_connection_delete(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *dialog;
+
+ if (!remminamain->priv->selected_filename)
+ return;
+
+ RemminaFile *remminafile = remmina_file_load(remminamain->priv->selected_filename);
+
+ if (((remmina_pref_get_boolean("lock_edit")
+ && remmina_pref_get_boolean("use_primary_password"))
+ || remmina_file_get_int (remminafile, "profile-lock", FALSE))
+ && remmina_unlock_new(remminamain->window) == 0)
+ return;
+
+ if (remminafile) {
+ remmina_file_free(remminafile);
+ remminafile = NULL;
+ }
+
+ dialog = gtk_message_dialog_new(remminamain->window, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
+ _("Are you sure you want to delete “%s”?"), remminamain->priv->selected_name);
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES) {
+ gchar *delfilename = g_strdup(remminamain->priv->selected_filename);
+ remmina_file_delete(delfilename);
+ g_free(delfilename), delfilename = NULL;
+ remmina_icon_populate_menu();
+ remmina_main_load_files();
+ }
+ gtk_widget_destroy(dialog);
+ remmina_main_clear_selection_data();
+}
+
+void remmina_main_on_action_connection_delete_multiple(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *dialog;
+ GtkTreeSelection *sel = gtk_tree_view_get_selection(remminamain->tree_files_list);
+ GtkTreeModel *model = gtk_tree_view_get_model(remminamain->tree_files_list);
+ GList *list = gtk_tree_selection_get_selected_rows(sel, &model);
+ gchar *file_to_delete;
+
+ dialog = gtk_message_dialog_new(remminamain->window, GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
+ _("Are you sure you want to delete the selected files?"));
+
+ // Delete files if Yes is clicked
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES) {
+ while (list) {
+ GtkTreePath *path = list->data;
+ GtkTreeIter iter;
+
+ if (!gtk_tree_model_get_iter(model, &iter, path)) {
+ GtkWidget *dialog_warning;
+ dialog_warning = gtk_message_dialog_new(remminamain->window, GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK,
+ _("Failed to delete files!"));
+ gtk_dialog_run(GTK_DIALOG(dialog_warning));
+ gtk_widget_destroy(dialog_warning);
+ gtk_widget_destroy(dialog);
+ remmina_main_clear_selection_data();
+ return;
+ }
+
+ gtk_tree_model_get(model, &iter,
+ FILENAME_COLUMN, &file_to_delete, -1);
+
+ RemminaFile *remminafile = remmina_file_load(file_to_delete);
+
+ if (((remmina_pref_get_boolean("lock_edit")
+ && remmina_pref_get_boolean("use_primary_password"))
+ || remmina_file_get_int (remminafile, "profile-lock", FALSE))
+ && remmina_unlock_new(remminamain->window) == 0)
+ return;
+
+ if (remminafile) {
+ remmina_file_free(remminafile);
+ remminafile = NULL;
+ }
+
+ gchar *delfilename = g_strdup(file_to_delete);
+ remmina_file_delete(delfilename);
+ g_free(delfilename), delfilename = NULL;
+ remmina_icon_populate_menu();
+ remmina_main_load_files();
+ list = g_list_next(list);
+ }
+ }
+
+ gtk_widget_destroy(dialog);
+ remmina_main_clear_selection_data();
+}
+
+void remmina_main_on_accel_application_preferences(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GVariant *v = g_variant_new("i", 0);
+
+ remmina_main_on_action_application_preferences(NULL, v, NULL);
+}
+
+void remmina_main_reload_preferences()
+{
+ GtkSettings *settings;
+ settings = gtk_settings_get_default();
+ g_object_set(settings, "gtk-application-prefer-dark-theme", remmina_pref.dark_theme, NULL);
+ if (remminamain) {
+ if(remmina_pref.hide_searchbar){
+ gtk_toggle_button_set_active(remminamain->search_toggle, FALSE);
+ }
+ else{
+ gtk_toggle_button_set_active(remminamain->search_toggle, TRUE);
+ }
+ gtk_tree_view_column_set_visible(remminamain->column_files_list_notes, remmina_pref.always_show_notes);
+ }
+}
+
+void remmina_main_on_action_application_preferences(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+
+ REMMINA_DEBUG("Opening the preferences");
+ gint32 tab_num;
+
+ if (param) {
+ REMMINA_DEBUG("Parameter passed to preferences of type %s", g_variant_get_type_string(param));
+ tab_num = g_variant_get_int32(param);
+ REMMINA_DEBUG("We got a parameter for the preferences: %d", tab_num);
+ } else {
+ tab_num = 0;
+ }
+
+ if (remmina_pref_get_boolean("use_primary_password")
+ && remmina_unlock_new(remminamain->window) == 0)
+ return;
+
+ GtkWidget *widget = remmina_pref_dialog_new(tab_num, remminamain->window);
+
+ gtk_widget_show(widget);
+}
+
+void remmina_main_on_action_application_default(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+#ifndef __APPLE__
+ g_autoptr(GError) error = NULL;
+ GDesktopAppInfo *desktop_info;
+ GAppInfo *info = NULL;
+ g_autofree gchar *id = g_strconcat(REMMINA_APP_ID, ".desktop", NULL);
+ int i;
+
+ desktop_info = g_desktop_app_info_new(id);
+ if (!desktop_info)
+ return;
+
+ info = G_APP_INFO(desktop_info);
+
+ for (i = 0; supported_mime_types[i]; i++) {
+ if (!g_app_info_set_as_default_for_type(info, supported_mime_types[i], &error))
+ g_warning("Failed to set '%s' as the default application for secondary content type '%s': %s",
+ g_app_info_get_name(info), supported_mime_types[i], error->message);
+ else
+ g_debug("Set '%s' as the default application for '%s'",
+ g_app_info_get_name(info),
+ supported_mime_types[i]);
+ }
+#endif
+}
+
+void remmina_main_on_action_application_quit(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ // Called by quit signal in remmina_main.glade
+ TRACE_CALL(__func__);
+ g_debug("Quit intercept");
+ remmina_application_condexit(REMMINA_CONDEXIT_ONQUIT);
+}
+
+void remmina_main_on_date_column_sort_clicked()
+{
+ if (remmina_pref.view_file_mode != REMMINA_VIEW_FILE_LIST) {
+ remmina_pref.view_file_mode = REMMINA_VIEW_FILE_LIST;
+ gtk_entry_set_text(remminamain->entry_quick_connect_server, "");
+ remmina_pref_save();
+ remmina_main_load_files();
+ }
+}
+
+void remmina_main_toggle_password_view(GtkWidget *widget, gpointer data)
+{
+ GtkWindow *mainwindow;
+ gboolean visible = gtk_entry_get_visibility(GTK_ENTRY(widget));
+
+ mainwindow = remmina_main_get_window();
+ if (remmina_pref_get_boolean("use_primary_password") && remmina_pref_get_boolean("lock_view_passwords") && remmina_unlock_new(mainwindow) == 0)
+ return;
+
+ if (visible) {
+ gtk_entry_set_visibility(GTK_ENTRY(widget), FALSE);
+ gtk_entry_set_icon_from_icon_name(GTK_ENTRY(widget), GTK_ENTRY_ICON_SECONDARY, "org.remmina.Remmina-password-reveal-symbolic");
+ } else {
+ gtk_entry_set_visibility(GTK_ENTRY(widget), TRUE);
+ gtk_entry_set_icon_from_icon_name(GTK_ENTRY(widget), GTK_ENTRY_ICON_SECONDARY, "org.remmina.Remmina-password-conceal-symbolic");
+ }
+}
+
+static void remmina_main_import_file_list(GSList *files)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *dlg;
+ GSList *element;
+ gchar *path;
+ RemminaFilePlugin *plugin;
+ GString *err;
+ RemminaFile *remminafile = NULL;
+ gboolean imported;
+
+ err = g_string_new(NULL);
+ imported = FALSE;
+ for (element = files; element; element = element->next) {
+ path = (gchar *)element->data;
+ plugin = remmina_plugin_manager_get_import_file_handler(path);
+ if (plugin && (remminafile = plugin->import_func(plugin, path)) != NULL && remmina_file_get_string(remminafile, "name")) {
+ remmina_file_generate_filename(remminafile);
+ remmina_file_save(remminafile);
+ imported = TRUE;
+ } else {
+ g_string_append(err, path);
+ g_string_append_c(err, '\n');
+ }
+ if (remminafile) {
+ remmina_file_free(remminafile);
+ remminafile = NULL;
+ }
+ g_free(path);
+ }
+ g_slist_free(files);
+ if (err->len > 0) {
+ // TRANSLATORS: The placeholder %s is an error message
+ dlg = gtk_message_dialog_new(remminamain->window, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
+ _("Unable to import:\n%s"), err->str);
+ g_signal_connect(G_OBJECT(dlg), "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_widget_show(dlg);
+ }
+ g_string_free(err, TRUE);
+ if (imported)
+ remmina_main_load_files();
+}
+
+static void remmina_main_action_tools_import_on_response(GtkNativeDialog *dialog, gint response_id, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GSList *files;
+
+ if (response_id == GTK_RESPONSE_ACCEPT) {
+ files = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
+ remmina_main_import_file_list(files);
+ }
+ gtk_native_dialog_destroy(dialog);
+}
+
+static void remmina_set_file_chooser_filters(GtkFileChooser *chooser)
+{
+ GtkFileFilter *filter;
+
+ g_return_if_fail(GTK_IS_FILE_CHOOSER(chooser));
+
+ filter = gtk_file_filter_new();
+ gtk_file_filter_set_name(filter, _("RDP Files"));
+ gtk_file_filter_add_pattern(filter, "*.rdp");
+ gtk_file_filter_add_pattern(filter, "*.rdpx");
+ gtk_file_filter_add_pattern(filter, "*.RDP");
+ gtk_file_filter_add_pattern(filter, "*.RDPX");
+ gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(chooser), filter);
+ gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(chooser), filter);
+
+ filter = gtk_file_filter_new();
+ gtk_file_filter_set_name(filter, _("All Files"));
+ gtk_file_filter_add_pattern(filter, "*");
+ gtk_file_chooser_add_filter(chooser, filter);
+}
+
+void remmina_main_on_action_tools_import(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkFileChooserNative *chooser;
+
+ chooser = gtk_file_chooser_native_new(_("Import"), remminamain->window,
+ GTK_FILE_CHOOSER_ACTION_OPEN, _("Import"), _("_Cancel"));
+ gtk_native_dialog_set_modal(GTK_NATIVE_DIALOG(chooser), TRUE);
+ remmina_set_file_chooser_filters(GTK_FILE_CHOOSER(chooser));
+ gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(chooser), TRUE);
+ g_signal_connect(chooser, "response", G_CALLBACK(remmina_main_action_tools_import_on_response), NULL);
+ gtk_native_dialog_show(GTK_NATIVE_DIALOG(chooser));
+}
+
+static void on_export_save_response (GtkFileChooserNative *dialog, int response, RemminaFile *remminafile)
+{
+ if (response == GTK_RESPONSE_ACCEPT) {
+ RemminaFilePlugin *plugin = remmina_plugin_manager_get_export_file_handler(remminafile);
+ if (plugin){
+ gchar *path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+ plugin->export_func(plugin, remminafile, path);
+ g_free(path);
+ }
+ }
+ remmina_file_free(remminafile);
+ gtk_native_dialog_destroy(GTK_NATIVE_DIALOG(dialog));
+}
+
+void remmina_main_on_action_tools_export(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaFilePlugin *plugin;
+ RemminaFile *remminafile;
+ GtkWidget *dialog;
+ GtkFileChooserNative *chooser;
+ gchar *export_name;
+
+ if (!remminamain->priv->selected_filename) {
+ dialog = gtk_message_dialog_new(remminamain->window, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
+ _("Select the connection profile."));
+ g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_widget_show(dialog);
+ return;
+ }
+
+ remminafile = remmina_file_load(remminamain->priv->selected_filename);
+ if (remminafile == NULL) {
+ dialog = gtk_message_dialog_new(remminamain->window, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
+ _("Remmina couldn't export."));
+ g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_widget_show(dialog);
+ return;
+ }
+
+ plugin = remmina_plugin_manager_get_export_file_handler(remminafile);
+ if (plugin) {
+ chooser = gtk_file_chooser_native_new(plugin->export_hints, remminamain->window,
+ GTK_FILE_CHOOSER_ACTION_SAVE, _("_Save"), _("_Cancel"));
+ gtk_native_dialog_set_modal(GTK_NATIVE_DIALOG(chooser), TRUE);
+ remmina_set_file_chooser_filters(GTK_FILE_CHOOSER(chooser));
+ gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(chooser), TRUE);
+ export_name = g_strdup_printf("%s.rdp", remminamain->priv->selected_name);
+ gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(chooser), export_name);
+ g_free(export_name);
+ g_signal_connect(chooser, "response", G_CALLBACK(on_export_save_response), remminafile);
+ gtk_native_dialog_show(GTK_NATIVE_DIALOG(chooser));
+ } else
+ {
+ remmina_file_free(remminafile);
+ dialog = gtk_message_dialog_new(remminamain->window, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
+ _("This protocol does not support exporting."));
+ g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_widget_show(dialog);
+ return;
+ }
+}
+
+void remmina_main_on_action_application_plugins(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_manager_get_available_plugins();
+ remmina_plugin_manager_show(remminamain->window);
+}
+
+void remmina_main_on_action_application_dark_theme(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkSettings *settings;
+
+ settings = gtk_settings_get_default();
+
+ if (gtk_switch_get_active(remminamain->switch_dark_mode))
+ remmina_pref.dark_theme = 1;
+ else
+ remmina_pref.dark_theme = 0;
+ remmina_pref_save();
+
+ g_object_set(settings, "gtk-application-prefer-dark-theme", remmina_pref.dark_theme, NULL);
+}
+
+void remmina_main_on_action_help_homepage(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ g_app_info_launch_default_for_uri("https://www.remmina.org", NULL, NULL);
+}
+
+void remmina_main_on_action_help_wiki(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ g_app_info_launch_default_for_uri("https://gitlab.com/Remmina/Remmina/wikis/home", NULL, NULL);
+}
+
+void remmina_main_on_action_help_community(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ g_app_info_launch_default_for_uri("https://remmina.org/community", NULL, NULL);
+}
+
+void remmina_main_on_action_help_donations(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ g_app_info_launch_default_for_uri("https://www.remmina.org/donations", NULL, NULL);
+}
+
+void remmina_main_on_action_help_debug(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ remmina_log_start();
+}
+
+void remmina_main_on_action_application_about(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ remmina_about_open(remminamain->window);
+};
+
+void remmina_main_on_action_application_bug_report(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ remmina_bug_report_open(remminamain->window);
+};
+
+static gboolean is_empty(const gchar *s)
+{
+ if (s == NULL)
+ return TRUE;
+ while (*s != 0) {
+ if (!isspace((unsigned char)*s))
+ return FALSE;
+ s++;
+ }
+ return TRUE;
+}
+
+static gboolean remmina_main_quickconnect(void)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+ gchar *server;
+ gchar *server_trimmed;
+ gchar *qcp;
+
+
+ /* Save quick connect protocol if different from the previous one */
+ qcp = gtk_combo_box_text_get_active_text(remminamain->combo_quick_connect_protocol);
+ if (qcp && strcmp(qcp, remmina_pref.last_quickconnect_protocol) != 0) {
+ g_free(remmina_pref.last_quickconnect_protocol);
+ remmina_pref.last_quickconnect_protocol = g_strdup(qcp);
+ remmina_pref_save();
+ }
+
+ remminafile = remmina_file_new();
+ server = g_strdup(gtk_entry_get_text(remminamain->entry_quick_connect_server));
+ if (g_hostname_to_ascii(server) == NULL)
+ return FALSE;
+ /* If server contain /, e.g. vnc://, it won't connect
+ * We could search for an array of invalid characters, but
+ * it's better to find a way to correctly parse and validate addresses
+ */
+ if (g_strrstr(server, "/") != NULL)
+ return FALSE;
+ //if (g_str_has_suffix (server, "/"))
+ //return FALSE;
+ if (is_empty(server))
+ return FALSE;
+
+ /* check if server is an IP address and trim whitespace if so */
+ server_trimmed = g_strdup(server);
+ g_strstrip(server_trimmed);
+ gchar **strings = g_strsplit(server_trimmed, ":", 2);
+
+ if (strings[0] != NULL)
+ if (g_hostname_is_ip_address(strings[0]))
+ g_stpcpy(server, server_trimmed);
+
+ remmina_file_set_string(remminafile, "sound", "off");
+ remmina_file_set_string(remminafile, "server", server);
+ remmina_file_set_string(remminafile, "name", server);
+ remmina_file_set_string(remminafile, "protocol", qcp);
+ g_free(server);
+ g_free(server_trimmed);
+ g_free(qcp);
+
+ rcw_open_from_file(remminafile);
+
+ return FALSE;
+}
+
+gboolean remmina_main_quickconnect_on_click(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ if (!kioskmode && kioskmode == FALSE)
+ return remmina_main_quickconnect();
+ return FALSE;
+}
+
+/* Select all the text inside the quick search box if there is anything */
+void remmina_main_quick_search_enter(GtkWidget *widget, gpointer user_data)
+{
+ if (gtk_entry_get_text(remminamain->entry_quick_connect_server))
+ gtk_editable_select_region(GTK_EDITABLE(remminamain->entry_quick_connect_server), 0, -1);
+}
+
+void remmina_main_on_action_collapse(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ gtk_tree_view_collapse_all(remminamain->tree_files_list);
+}
+
+void remmina_main_on_action_search_toggle(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ REMMINA_DEBUG("Search toggle triggered");
+
+ gboolean toggle_status = gtk_toggle_button_get_active(remminamain->search_toggle);
+
+ gtk_search_bar_set_search_mode(remminamain->search_bar, toggle_status);
+ if (toggle_status) {
+ REMMINA_DEBUG("Search toggle is active");
+ gtk_widget_grab_focus(GTK_WIDGET(remminamain->entry_quick_connect_server));
+ } else {
+ REMMINA_DEBUG("Search toggle is not active, focus is tree_files_list");
+ gtk_widget_grab_focus(GTK_WIDGET(remminamain->tree_files_list));
+ }
+}
+
+void remmina_main_on_accel_search_toggle(RemminaMain *remminamain)
+{
+ TRACE_CALL(__func__);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remminamain->search_toggle), TRUE);
+}
+
+void remmina_main_on_action_expand(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ gtk_tree_view_expand_all(remminamain->tree_files_list);
+}
+
+/* Handle double click on a row in the connections list */
+void remmina_main_file_list_on_row_activated(GtkTreeView *tree, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+/* If a connection was selected then execute the default action */
+ if (remminamain->priv->selected_filename) {
+ switch (remmina_pref.default_action) {
+ case REMMINA_ACTION_EDIT:
+ remmina_main_on_action_connection_edit(NULL, NULL, NULL);
+ break;
+ case REMMINA_ACTION_CONNECT:
+ default:
+ remmina_main_on_action_connection_connect(NULL, NULL, NULL);
+ break;
+ }
+ }
+}
+
+/* Show the popup menu by the right button mouse click */
+gboolean remmina_main_file_list_on_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ if (event->button == MOUSE_BUTTON_RIGHT) {
+ if (!kioskmode && kioskmode == FALSE) {
+#if GTK_CHECK_VERSION(3, 22, 0)
+ // For now, if more than one selected row, display only a delete menu option
+ if (gtk_tree_selection_count_selected_rows(gtk_tree_view_get_selection(remminamain->tree_files_list)) > 1) {
+ gtk_menu_popup_at_pointer(GTK_MENU(remminamain->menu_popup_delete_rc), (GdkEvent *)event);
+ return GDK_EVENT_STOP;
+ }
+ else {
+ gtk_menu_popup_at_pointer(GTK_MENU(remminamain->menu_popup), (GdkEvent *)event);
+ }
+#else
+ gtk_menu_popup(remminamain->menu_popup, NULL, NULL, NULL, NULL, event->button, event->time);
+#endif
+ }
+ }
+ return FALSE;
+}
+
+/* Show the popup menu by the menu key */
+gboolean remmina_main_file_list_on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ if (event->keyval == GDK_KEY_Menu) {
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gtk_menu_popup_at_widget(GTK_MENU(remminamain->menu_popup), widget,
+ GDK_GRAVITY_CENTER, GDK_GRAVITY_CENTER,
+ (GdkEvent *)event);
+#else
+ gtk_menu_popup(remminamain->menu_popup, NULL, NULL, NULL, NULL, 0, event->time);
+#endif
+ }
+ return FALSE;
+}
+
+void remmina_main_quick_search_on_icon_press(GtkEntry *entry, GtkEntryIconPosition icon_pos, GdkEvent *event, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ if (icon_pos == GTK_ENTRY_ICON_SECONDARY)
+ gtk_entry_set_text(entry, "");
+}
+
+void remmina_main_quick_search_on_changed(GtkEditable *editable, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ /* If a search text was input then temporary set the file mode to list */
+ if (gtk_entry_get_text_length(remminamain->entry_quick_connect_server)) {
+ if (GTK_IS_TREE_STORE(remminamain->priv->file_model)) {
+ /* File view mode changed, put it to override and reload list */
+ remminamain->priv->override_view_file_mode_to_list = TRUE;
+ remmina_main_load_files();
+ }
+ } else {
+ if (remminamain->priv->override_view_file_mode_to_list) {
+ /* File view mode changed, put it to default (disable override) and reload list */
+ remminamain->priv->override_view_file_mode_to_list = FALSE;
+ remmina_main_load_files();
+ }
+ }
+ gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(remminamain->priv->file_model_filter));
+}
+
+void remmina_main_on_drag_data_received(GtkWidget *widget, GdkDragContext *drag_context, gint x, gint y,
+ GtkSelectionData *data, guint info, guint time, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gchar **uris;
+ GSList *files = NULL;
+ gint i;
+
+ uris = g_uri_list_extract_uris((const gchar *)gtk_selection_data_get_data(data));
+ for (i = 0; uris[i]; i++) {
+ if (strncmp(uris[i], "file://", 7) != 0)
+ continue;
+ files = g_slist_append(files, g_strdup(uris[i] + 7));
+ }
+ g_strfreev(uris);
+ remmina_main_import_file_list(files);
+}
+
+/* Add a new menuitem to the Tools menu */
+static gboolean remmina_main_add_tool_plugin(gchar *name, RemminaPlugin *plugin, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaToolPlugin *tool_plugin = (RemminaToolPlugin *)plugin;
+ GtkWidget *menuitem = gtk_menu_item_new_with_label(plugin->description);
+
+ gtk_widget_show(menuitem);
+ gtk_menu_shell_append(GTK_MENU_SHELL(remminamain->menu_popup_full), menuitem);
+ g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(tool_plugin->exec_func), tool_plugin);
+ return FALSE;
+}
+
+gboolean remmina_main_on_window_state_event(GtkWidget *widget, GdkEventWindowState *event, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ return FALSE;
+}
+
+/* Remmina main window initialization */
+static void remmina_main_init(void)
+{
+ TRACE_CALL(__func__);
+ int i, qcp_idx, qcp_actidx;
+ char *name;
+ GtkSettings *settings;
+
+ REMMINA_DEBUG("Initializing the Remmina main window");
+ /* Switch to a dark theme if the user enabled it */
+ settings = gtk_settings_get_default();
+ g_object_set(settings, "gtk-application-prefer-dark-theme", remmina_pref.dark_theme, NULL);
+
+ REMMINA_DEBUG ("Initializing monitor");
+ remminamain->monitor = remmina_network_monitor_new();
+
+ remminamain->priv->expanded_group = remmina_string_array_new_from_string(remmina_pref.expanded_group);
+ if (!kioskmode && kioskmode == FALSE)
+ gtk_window_set_title(remminamain->window, _("Remmina Remote Desktop Client"));
+ else
+ gtk_window_set_title(remminamain->window, _("Remmina Kiosk"));
+ if (!kioskmode && kioskmode == FALSE) {
+ gtk_window_set_default_size(remminamain->window, remmina_pref.main_width, remmina_pref.main_height);
+ if (remmina_pref.main_maximize)
+ gtk_window_maximize(remminamain->window);
+ }
+ /* Honor global preferences Search Bar visibility */
+ if (remmina_pref.hide_searchbar)
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(RM_GET_OBJECT("search_toggle")), FALSE);
+
+ /* Add a GtkMenuItem to the Tools menu for each plugin of type REMMINA_PLUGIN_TYPE_TOOL */
+ remmina_plugin_manager_for_each_plugin(REMMINA_PLUGIN_TYPE_TOOL, remmina_main_add_tool_plugin, remminamain);
+
+ /* Add available quick connect protocols to remminamain->combo_quick_connect_protocol */
+ qcp_idx = qcp_actidx = 0;
+ for (i = 0; i < sizeof(quick_connect_plugin_list) / sizeof(quick_connect_plugin_list[0]); i++) {
+ name = quick_connect_plugin_list[i];
+ if (remmina_plugin_manager_get_plugin(REMMINA_PLUGIN_TYPE_PROTOCOL, name)) {
+ gtk_combo_box_text_append(remminamain->combo_quick_connect_protocol, name, name);
+ if (remmina_pref.last_quickconnect_protocol != NULL && strcmp(name, remmina_pref.last_quickconnect_protocol) == 0)
+ qcp_actidx = qcp_idx;
+ qcp_idx++;
+ }
+ }
+ gtk_combo_box_set_active(GTK_COMBO_BOX(remminamain->combo_quick_connect_protocol), qcp_actidx);
+
+ /* Connect the group accelerators to the GtkWindow */
+ //gtk_window_add_accel_group(remminamain->window, remminamain->accelgroup_shortcuts);
+ /* Set the Quick Connection */
+ gtk_entry_set_activates_default(remminamain->entry_quick_connect_server, TRUE);
+ /* Set the TreeView for the files list */
+ gtk_tree_selection_set_select_function(
+ gtk_tree_view_get_selection(remminamain->tree_files_list),
+ remmina_main_selection_func, NULL, NULL);
+ /** @todo Set entry_quick_connect_server as default search entry. Weirdly. This does not work yet. */
+ gtk_tree_view_set_search_entry(remminamain->tree_files_list, GTK_ENTRY(remminamain->entry_quick_connect_server));
+ if (remmina_pref.hide_searchbar)
+ gtk_widget_grab_focus(GTK_WIDGET(remminamain->tree_files_list));
+ /* Load the files list */
+ remmina_main_load_files();
+
+ /* Drag-n-drop support */
+ gtk_drag_dest_set(GTK_WIDGET(remminamain->window), GTK_DEST_DEFAULT_ALL, remmina_drop_types, 1, GDK_ACTION_COPY);
+
+ /* Finish initialization */
+ remminamain->priv->initialized = TRUE;
+
+ /* Register the window in remmina_widget_pool with GType=GTK_WINDOW and TAG=remmina-main-window */
+ g_object_set_data(G_OBJECT(remminamain->window), "tag", "remmina-main-window");
+ remmina_widget_pool_register(GTK_WIDGET(remminamain->window));
+}
+
+/* Signal handler for "show" on remminamain->window */
+void remmina_main_on_show(GtkWidget *w, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+#ifdef SNAP_BUILD
+ remmina_main_show_snap_welcome();
+#endif
+}
+
+/* RemminaMain instance */
+GtkWidget *remmina_main_new(void)
+{
+ TRACE_CALL(__func__);
+ GSimpleActionGroup *actions;
+ GtkAccelGroup *accel_group = NULL;
+
+ remminamain = g_new0(RemminaMain, 1);
+ remminamain->priv = g_new0(RemminaMainPriv, 1);
+ /* Assign UI widgets to the private members */
+ remminamain->builder = remmina_public_gtk_builder_new_from_resource("/org/remmina/Remmina/src/../data/ui/remmina_main.glade");
+ remminamain->window = GTK_WINDOW(RM_GET_OBJECT("RemminaMain"));
+ if (kioskmode && kioskmode == TRUE) {
+ gtk_window_set_position(remminamain->window, GTK_WIN_POS_CENTER_ALWAYS);
+ gtk_window_set_default_size(remminamain->window, 800, 400);
+ gtk_window_set_resizable(remminamain->window, FALSE);
+ }
+ /* New Button */
+ remminamain->button_new = GTK_BUTTON(RM_GET_OBJECT("button_new"));
+ if (kioskmode && kioskmode == TRUE)
+ gtk_widget_set_sensitive(GTK_WIDGET(remminamain->button_new), FALSE);
+ /* Search bar */
+ remminamain->search_toggle = GTK_TOGGLE_BUTTON(RM_GET_OBJECT("search_toggle"));
+ remminamain->search_bar = GTK_SEARCH_BAR(RM_GET_OBJECT("search_bar"));
+ /* view mode list/tree */
+ remminamain->view_toggle_button = GTK_TOGGLE_BUTTON(RM_GET_OBJECT("view_toggle_button"));
+ if (kioskmode && kioskmode == TRUE)
+ gtk_widget_set_sensitive(GTK_WIDGET(remminamain->view_toggle_button), FALSE);
+
+ /* Menu widgets */
+ remminamain->menu_popup = GTK_MENU(RM_GET_OBJECT("menu_popup"));
+ remminamain->menu_header_button = GTK_MENU_BUTTON(RM_GET_OBJECT("menu_header_button"));
+ remminamain->menu_popup_full = GTK_MENU(RM_GET_OBJECT("menu_popup_full"));
+ remminamain->menu_popup_delete_rc = GTK_MENU(RM_GET_OBJECT("menu_popup_delete_rc"));
+ if (kioskmode && kioskmode == TRUE) {
+ gtk_widget_set_sensitive(GTK_WIDGET(remminamain->menu_popup_full), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remminamain->menu_header_button), FALSE);
+ }
+ /* View mode radios */
+ remminamain->menuitem_view_mode_list = GTK_RADIO_MENU_ITEM(RM_GET_OBJECT("menuitem_view_mode_list"));
+ remminamain->menuitem_view_mode_tree = GTK_RADIO_MENU_ITEM(RM_GET_OBJECT("menuitem_view_mode_tree"));
+ /* Quick connect objects */
+ remminamain->box_quick_connect = GTK_BOX(RM_GET_OBJECT("box_quick_connect"));
+ remminamain->combo_quick_connect_protocol = GTK_COMBO_BOX_TEXT(RM_GET_OBJECT("combo_quick_connect_protocol"));
+ if (kioskmode && kioskmode == TRUE)
+ gtk_widget_set_sensitive(GTK_WIDGET(remminamain->combo_quick_connect_protocol), FALSE);
+ remminamain->entry_quick_connect_server = GTK_ENTRY(RM_GET_OBJECT("entry_quick_connect_server"));
+ /* Other widgets */
+ remminamain->tree_files_list = GTK_TREE_VIEW(RM_GET_OBJECT("tree_files_list"));
+ remminamain->column_files_list_name = GTK_TREE_VIEW_COLUMN(RM_GET_OBJECT("column_files_list_name"));
+ remminamain->column_files_list_group = GTK_TREE_VIEW_COLUMN(RM_GET_OBJECT("column_files_list_group"));
+ remminamain->column_files_list_server = GTK_TREE_VIEW_COLUMN(RM_GET_OBJECT("column_files_list_server"));
+ remminamain->column_files_list_plugin = GTK_TREE_VIEW_COLUMN(RM_GET_OBJECT("column_files_list_plugin"));
+ remminamain->column_files_list_date = GTK_TREE_VIEW_COLUMN(RM_GET_OBJECT("column_files_list_date"));
+ remminamain->column_files_list_notes = GTK_TREE_VIEW_COLUMN(RM_GET_OBJECT("column_files_list_notes"));
+ gtk_tree_view_column_set_fixed_width(remminamain->column_files_list_notes, 100);
+ remminamain->statusbar_main = GTK_STATUSBAR(RM_GET_OBJECT("statusbar_main"));
+ /* signals */
+ g_signal_connect(remminamain->entry_quick_connect_server, "key-release-event", G_CALLBACK(remmina_main_search_key_event), NULL);
+ g_signal_connect(remminamain->tree_files_list, "row-activated", G_CALLBACK(remmina_main_tree_row_activated), NULL);
+ /* Non widget objects */
+ actions = g_simple_action_group_new();
+ g_action_map_add_action_entries(G_ACTION_MAP(actions), app_actions, G_N_ELEMENTS(app_actions), remminamain->window);
+ gtk_widget_insert_action_group(GTK_WIDGET(remminamain->window), "app", G_ACTION_GROUP(actions));
+ g_action_map_add_action_entries(G_ACTION_MAP(actions), main_actions, G_N_ELEMENTS(main_actions), remminamain->window);
+ gtk_widget_insert_action_group(GTK_WIDGET(remminamain->window), "main", G_ACTION_GROUP(actions));
+ g_object_unref(actions);
+ /* Accelerators */
+ accel_group = gtk_accel_group_new();
+ gtk_window_add_accel_group(remminamain->window, accel_group);
+ gtk_accel_group_connect(accel_group, GDK_KEY_Q, GDK_CONTROL_MASK, 0,
+ g_cclosure_new_swap(G_CALLBACK(remmina_main_on_action_application_quit), NULL, NULL));
+ // TODO: This crash remmina because the function doesn't receive the parameter we expect
+ gtk_accel_group_connect(accel_group, GDK_KEY_P, GDK_CONTROL_MASK, 0,
+ g_cclosure_new_swap(G_CALLBACK(remmina_main_on_accel_application_preferences), NULL, NULL));
+ gtk_accel_group_connect(accel_group, GDK_KEY_F, GDK_CONTROL_MASK, 0,
+ g_cclosure_new_swap(G_CALLBACK(remmina_main_on_accel_search_toggle), remminamain, NULL));
+
+ /* Connect signals */
+ gtk_builder_connect_signals(remminamain->builder, NULL);
+ /* Initialize the window and load the preferences */
+ remmina_main_init();
+ return GTK_WIDGET(remminamain->window);
+}
+
+GtkWindow *remmina_main_get_window()
+{
+ if (!remminamain)
+ return NULL;
+ if (!remminamain->priv)
+ return NULL;
+ if (!remminamain->priv->initialized)
+ return NULL;
+ remminamain->window = GTK_WINDOW(RM_GET_OBJECT("RemminaMain"));
+ return remminamain->window;
+}
+
+void remmina_main_update_file_datetime(RemminaFile *file)
+{
+ if (!remminamain)
+ return;
+ remmina_main_load_files();
+}
+
+void remmina_main_show_dialog(GtkMessageType msg, GtkButtonsType buttons, const gchar* message) {
+ GtkWidget *dialog;
+
+ if (remminamain->window) {
+ dialog = gtk_message_dialog_new(remminamain->window, GTK_DIALOG_MODAL, msg, buttons, "%s", message);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ }
+}
+
+void remmina_main_show_warning_dialog(const gchar *message) {
+ GtkWidget *dialog;
+
+ if (remminamain->window) {
+ dialog = gtk_message_dialog_new(remminamain->window, GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE,
+ message, g_get_application_name());
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ }
+} \ No newline at end of file
diff --git a/src/remmina_main.h b/src/remmina_main.h
new file mode 100644
index 0000000..f112b77
--- /dev/null
+++ b/src/remmina_main.h
@@ -0,0 +1,146 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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
+ * 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.
+ *
+ */
+
+#pragma once
+
+#include "remmina_file.h"
+#include "remmina_monitor.h"
+#include <gtk/gtk.h>
+
+#include "remmina_string_array.h"
+
+typedef struct _RemminaMainPriv RemminaMainPriv;
+
+typedef struct _RemminaMain {
+ GtkBuilder * builder;
+ GtkWindow * window;
+ /* Menu widgets */
+ GtkMenu * menu_popup;
+ GtkMenuButton * menu_header_button;
+ GtkMenu * menu_popup_full;
+ GtkMenu * menu_popup_delete_rc;
+ GtkRadioMenuItem * menuitem_view_mode_list;
+ GtkRadioMenuItem * menuitem_view_mode_tree;
+ GtkMenuItem * menuitem_connection_quit;
+ /* Button new */
+ GtkButton * button_new;
+ GtkButton * button_make_default;
+ /* Search bar objects */
+ GtkToggleButton * search_toggle;
+ GtkSwitch * switch_dark_mode;
+ GtkToggleButton * view_toggle_button;
+ GtkToggleButton * ustats_toggle;
+ GtkSearchBar * search_bar;
+ /* Quick connect objects */
+ GtkBox * box_quick_connect;
+ GtkComboBoxText * combo_quick_connect_protocol;
+ GtkEntry * entry_quick_connect_server;
+ GtkButton * button_quick_connect;
+ /* Other widgets */
+ GtkTreeView * tree_files_list;
+ GtkTreeViewColumn * column_files_list_name;
+ GtkTreeViewColumn * column_files_list_group;
+ GtkTreeViewColumn * column_files_list_server;
+ GtkTreeViewColumn * column_files_list_plugin;
+ GtkTreeViewColumn * column_files_list_date;
+ GtkTreeViewColumn * column_files_list_notes;
+ GtkStatusbar * statusbar_main;
+ GtkWidget * network_icon;
+ /* Non widget objects */
+ GtkAccelGroup * accelgroup_shortcuts;
+ RemminaMainPriv * priv;
+ RemminaMonitor * monitor;
+} RemminaMain;
+
+struct _RemminaMainPriv {
+ GtkTreeModel * file_model;
+ GtkTreeModel * file_model_filter;
+ GtkTreeModel * file_model_sort;
+
+ gboolean initialized;
+
+ gchar * selected_filename;
+ gchar * selected_name;
+ gboolean override_view_file_mode_to_list;
+ RemminaStringArray * expanded_group;
+};
+
+G_BEGIN_DECLS
+
+/* Create the remminamain struct and the remmina main Remmina window */
+GtkWidget *remmina_main_new(void);
+/* Get the current main GTK window or NULL if not initialized */
+GtkWindow *remmina_main_get_window(void);
+
+void remmina_main_update_file_datetime(RemminaFile *file);
+
+void remmina_main_destroy(void);
+void remmina_main_on_destroy_event(void);
+void remmina_main_save_before_destroy(void);
+
+void remmina_main_show_dialog(GtkMessageType msg, GtkButtonsType buttons, const gchar* message);
+void remmina_main_show_warning_dialog(const gchar *message);
+void remmina_main_on_action_application_about(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_application_bug_report(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_application_default(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_application_mpchange(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_application_plugins(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_application_dark_theme(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_application_preferences(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_application_quit(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_connection_connect(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_connection_copy(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_connection_delete(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_connection_delete_multiple(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_connection_edit(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_connection_external_tools(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_connection_new(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_help_community(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_help_debug(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_help_donations(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_help_homepage(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_help_wiki(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_tools_export(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_tools_import(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_expand(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_collapse(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_on_action_search_toggle(GSimpleAction *action, GVariant *param, gpointer data);
+void remmina_main_toggle_password_view(GtkWidget *widget, gpointer data);
+void remmina_main_reload_preferences();
+
+G_END_DECLS
diff --git a/src/remmina_marshals.c b/src/remmina_marshals.c
new file mode 100644
index 0000000..2fe355e
--- /dev/null
+++ b/src/remmina_marshals.c
@@ -0,0 +1,161 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-2011 Vic Lee
+ * 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.
+ *
+ */
+
+#include <glib-object.h>
+#include "remmina/remmina_trace_calls.h"
+
+#ifdef G_ENABLE_DEBUG
+#define g_marshal_value_peek_boolean(v) g_value_get_boolean(v)
+#define g_marshal_value_peek_char(v) g_value_get_char(v)
+#define g_marshal_value_peek_uchar(v) g_value_get_uchar(v)
+#define g_marshal_value_peek_int(v) g_value_get_int(v)
+#define g_marshal_value_peek_uint(v) g_value_get_uint(v)
+#define g_marshal_value_peek_long(v) g_value_get_long(v)
+#define g_marshal_value_peek_ulong(v) g_value_get_ulong(v)
+#define g_marshal_value_peek_int64(v) g_value_get_int64(v)
+#define g_marshal_value_peek_uint64(v) g_value_get_uint64(v)
+#define g_marshal_value_peek_enum(v) g_value_get_enum(v)
+#define g_marshal_value_peek_flags(v) g_value_get_flags(v)
+#define g_marshal_value_peek_float(v) g_value_get_float(v)
+#define g_marshal_value_peek_double(v) g_value_get_double(v)
+#define g_marshal_value_peek_string(v) (char*)g_value_get_string(v)
+#define g_marshal_value_peek_param(v) g_value_get_param(v)
+#define g_marshal_value_peek_boxed(v) g_value_get_boxed(v)
+#define g_marshal_value_peek_pointer(v) g_value_get_pointer(v)
+#define g_marshal_value_peek_object(v) g_value_get_object(v)
+#else /* !G_ENABLE_DEBUG */
+/* WARNING: This code accesses GValues directly, which is UNSUPPORTED API.
+ * Do not access GValues directly in your code. Instead, use the
+ * g_value_get_*() functions
+ */
+#define g_marshal_value_peek_boolean(v) (v)->data[0].v_int
+#define g_marshal_value_peek_char(v) (v)->data[0].v_int
+#define g_marshal_value_peek_uchar(v) (v)->data[0].v_uint
+#define g_marshal_value_peek_int(v) (v)->data[0].v_int
+#define g_marshal_value_peek_uint(v) (v)->data[0].v_uint
+#define g_marshal_value_peek_long(v) (v)->data[0].v_long
+#define g_marshal_value_peek_ulong(v) (v)->data[0].v_ulong
+#define g_marshal_value_peek_int64(v) (v)->data[0].v_int64
+#define g_marshal_value_peek_uint64(v) (v)->data[0].v_uint64
+#define g_marshal_value_peek_enum(v) (v)->data[0].v_long
+#define g_marshal_value_peek_flags(v) (v)->data[0].v_ulong
+#define g_marshal_value_peek_float(v) (v)->data[0].v_float
+#define g_marshal_value_peek_double(v) (v)->data[0].v_double
+#define g_marshal_value_peek_string(v) (v)->data[0].v_pointer
+#define g_marshal_value_peek_param(v) (v)->data[0].v_pointer
+#define g_marshal_value_peek_boxed(v) (v)->data[0].v_pointer
+#define g_marshal_value_peek_pointer(v) (v)->data[0].v_pointer
+#define g_marshal_value_peek_object(v) (v)->data[0].v_pointer
+#endif /* !G_ENABLE_DEBUG */
+
+/* BOOLEAN:INT (remminamarshals.list:4) */
+void
+remmina_marshal_BOOLEAN__INT(GClosure *closure,
+ GValue *return_value G_GNUC_UNUSED,
+ guint n_param_values,
+ const GValue *param_values,
+ gpointer invocation_hint G_GNUC_UNUSED,
+ gpointer marshal_data)
+{
+ TRACE_CALL(__func__);
+ typedef gboolean (*GMarshalFunc_BOOLEAN__INT) (gpointer data1,
+ gint arg_1,
+ gpointer data2);
+ register GMarshalFunc_BOOLEAN__INT callback;
+ register GCClosure *cc = (GCClosure*)closure;
+ register gpointer data1, data2;
+ gboolean v_return;
+
+ g_return_if_fail(return_value != NULL);
+ g_return_if_fail(n_param_values == 2);
+
+ if (G_CCLOSURE_SWAP_DATA(closure)) {
+ data1 = closure->data;
+ data2 = g_value_peek_pointer(param_values + 0);
+ }else {
+ data1 = g_value_peek_pointer(param_values + 0);
+ data2 = closure->data;
+ }
+ callback = (GMarshalFunc_BOOLEAN__INT)(marshal_data ? marshal_data : cc->callback);
+
+ v_return = callback(data1,
+ g_marshal_value_peek_int(param_values + 1)
+ ,
+ data2);
+
+ g_value_set_boolean(return_value, v_return);
+}
+
+/* BOOLEAN:INT,STRING (remminamarshals.list:5) */
+void
+remmina_marshal_BOOLEAN__INT_STRING(GClosure *closure,
+ GValue *return_value G_GNUC_UNUSED,
+ guint n_param_values,
+ const GValue *param_values,
+ gpointer invocation_hint G_GNUC_UNUSED,
+ gpointer marshal_data)
+{
+ TRACE_CALL(__func__);
+ typedef gboolean (*GMarshalFunc_BOOLEAN__INT_STRING) (gpointer data1,
+ gint arg_1,
+ gpointer arg_2,
+ gpointer data2);
+ register GMarshalFunc_BOOLEAN__INT_STRING callback;
+ register GCClosure *cc = (GCClosure*)closure;
+ register gpointer data1, data2;
+ gboolean v_return;
+
+ g_return_if_fail(return_value != NULL);
+ g_return_if_fail(n_param_values == 3);
+
+ if (G_CCLOSURE_SWAP_DATA(closure)) {
+ data1 = closure->data;
+ data2 = g_value_peek_pointer(param_values + 0);
+ }else {
+ data1 = g_value_peek_pointer(param_values + 0);
+ data2 = closure->data;
+ }
+ callback = (GMarshalFunc_BOOLEAN__INT_STRING)(marshal_data ? marshal_data : cc->callback);
+
+ v_return = callback(data1,
+ g_marshal_value_peek_int(param_values + 1),
+ g_marshal_value_peek_string(param_values + 2),
+ data2);
+
+ g_value_set_boolean(return_value, v_return);
+}
+
diff --git a/src/remmina_marshals.h b/src/remmina_marshals.h
new file mode 100644
index 0000000..4cd1d98
--- /dev/null
+++ b/src/remmina_marshals.h
@@ -0,0 +1,50 @@
+/*
+ * 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 __remmina_marshal_MARSHAL_H__
+#define __remmina_marshal_MARSHAL_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/* BOOLEAN:INT (remminamarshals.list:4) */
+extern void remmina_marshal_BOOLEAN__INT(GClosure *closure, GValue *return_value, guint n_param_values, const GValue *param_values, gpointer invocation_hint, gpointer marshal_data);
+
+/* BOOLEAN:INT,STRING (remminamarshals.list:5) */
+extern void remmina_marshal_BOOLEAN__INT_STRING(GClosure *closure, GValue *return_value, guint n_param_values, const GValue *param_values, gpointer invocation_hint, gpointer marshal_data);
+
+G_END_DECLS
+
+#endif /* __remmina_marshal_MARSHAL_H__ */
diff --git a/src/remmina_marshals.list b/src/remmina_marshals.list
new file mode 100644
index 0000000..ba3e432
--- /dev/null
+++ b/src/remmina_marshals.list
@@ -0,0 +1,6 @@
+# Use glib-genmarshal to generate remminamarshals.h and remminamarshals.c:
+# $ glib-genmarshal --header --prefix=remmina_marshal remminamarshals.list > remminamarshals.h
+# $ glib-genmarshal --body --prefix=remmina_marshal remminamarshals.list > remminamarshals.c
+BOOLEAN:INT
+BOOLEAN:INT,STRING
+
diff --git a/src/remmina_masterthread_exec.c b/src/remmina_masterthread_exec.c
new file mode 100644
index 0000000..97c2ffe
--- /dev/null
+++ b/src/remmina_masterthread_exec.c
@@ -0,0 +1,151 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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.
+ *
+ */
+
+/* Support for execution on main thread of some GTK related
+ * functions (due to threads deprecations in GTK) */
+
+#include <gtk/gtk.h>
+
+#include "remmina_masterthread_exec.h"
+
+static pthread_t gMainThreadID;
+
+static gboolean remmina_masterthread_exec_callback(RemminaMTExecData *d)
+{
+
+ /* This function is called on main GTK Thread via gdk_threads_add_idlde()
+ * from remmina_masterthread_exec_and_wait() */
+
+ if (!d->cancelled) {
+ switch (d->func) {
+ case FUNC_INIT_SAVE_CRED:
+ remmina_protocol_widget_save_cred(d->p.init_save_creds.gp);
+ break;
+ case FUNC_CHAT_RECEIVE:
+ remmina_protocol_widget_chat_receive(d->p.chat_receive.gp, d->p.chat_receive.text);
+ break;
+ case FUNC_FILE_GET_STRING:
+ d->p.file_get_string.retval = remmina_file_get_string( d->p.file_get_string.remminafile, d->p.file_get_string.setting );
+ break;
+ case FUNC_FILE_SET_STRING:
+ remmina_file_set_string( d->p.file_set_string.remminafile, d->p.file_set_string.setting, d->p.file_set_string.value );
+ break;
+ case FUNC_GTK_LABEL_SET_TEXT:
+ gtk_label_set_text( d->p.gtk_label_set_text.label, d->p.gtk_label_set_text.str );
+ break;
+ case FUNC_FTP_CLIENT_UPDATE_TASK:
+ remmina_ftp_client_update_task( d->p.ftp_client_update_task.client, d->p.ftp_client_update_task.task );
+ break;
+ case FUNC_FTP_CLIENT_GET_WAITING_TASK:
+ d->p.ftp_client_get_waiting_task.retval = remmina_ftp_client_get_waiting_task( d->p.ftp_client_get_waiting_task.client );
+ break;
+ case FUNC_PROTOCOLWIDGET_EMIT_SIGNAL:
+ remmina_protocol_widget_emit_signal(d->p.protocolwidget_emit_signal.gp, d->p.protocolwidget_emit_signal.signal_name);
+ break;
+ case FUNC_PROTOCOLWIDGET_MPPROGRESS:
+ d->p.protocolwidget_mpprogress.ret_mp = remmina_protocol_widget_mpprogress(d->p.protocolwidget_mpprogress.cnnobj, d->p.protocolwidget_mpprogress.message,
+ d->p.protocolwidget_mpprogress.response_callback, d->p.protocolwidget_mpprogress.response_callback_data);
+ break;
+ case FUNC_PROTOCOLWIDGET_MPDESTROY:
+ remmina_protocol_widget_mpdestroy(d->p.protocolwidget_mpdestroy.cnnobj, d->p.protocolwidget_mpdestroy.mp);
+ break;
+ case FUNC_PROTOCOLWIDGET_MPSHOWRETRY:
+ remmina_protocol_widget_panel_show_retry(d->p.protocolwidget_mpshowretry.gp);
+ break;
+ case FUNC_PROTOCOLWIDGET_PANELSHOWLISTEN:
+ remmina_protocol_widget_panel_show_listen(d->p.protocolwidget_panelshowlisten.gp, d->p.protocolwidget_panelshowlisten.port);
+ break;
+ case FUNC_SFTP_CLIENT_CONFIRM_RESUME:
+#ifdef HAVE_LIBSSH
+ d->p.sftp_client_confirm_resume.retval = remmina_sftp_client_confirm_resume( d->p.sftp_client_confirm_resume.client,
+ d->p.sftp_client_confirm_resume.path );
+#endif
+ break;
+ case FUNC_VTE_TERMINAL_SET_ENCODING_AND_PTY:
+#if defined (HAVE_LIBSSH) && defined (HAVE_LIBVTE)
+ remmina_plugin_ssh_vte_terminal_set_encoding_and_pty( d->p.vte_terminal_set_encoding_and_pty.terminal,
+ d->p.vte_terminal_set_encoding_and_pty.codeset,
+ d->p.vte_terminal_set_encoding_and_pty.master,
+ d->p.vte_terminal_set_encoding_and_pty.slave);
+#endif
+ break;
+
+ }
+ pthread_mutex_lock(&d->pt_mutex);
+ d->complete = TRUE;
+ pthread_cond_signal(&d->pt_cond);
+ pthread_mutex_unlock(&d->pt_mutex);
+ }else {
+ /* thread has been cancelled, so we must free d memory here */
+ g_free(d);
+ }
+ return G_SOURCE_REMOVE;
+}
+
+static void remmina_masterthread_exec_cleanup_handler(gpointer data)
+{
+ RemminaMTExecData *d = data;
+
+ d->cancelled = TRUE;
+}
+
+void remmina_masterthread_exec_and_wait(RemminaMTExecData *d)
+{
+ d->cancelled = FALSE;
+ d->complete = FALSE;
+ pthread_cleanup_push(remmina_masterthread_exec_cleanup_handler, (void*)d);
+ pthread_mutex_init(&d->pt_mutex, NULL);
+ pthread_cond_init(&d->pt_cond, NULL);
+ gdk_threads_add_idle((GSourceFunc)remmina_masterthread_exec_callback, (gpointer)d);
+ pthread_mutex_lock(&d->pt_mutex);
+ while (!d->complete)
+ pthread_cond_wait(&d->pt_cond, &d->pt_mutex);
+ pthread_cleanup_pop(0);
+ pthread_mutex_destroy(&d->pt_mutex);
+ pthread_cond_destroy(&d->pt_cond);
+}
+
+void remmina_masterthread_exec_save_main_thread_id()
+{
+ /* To be called from main thread at startup */
+ gMainThreadID = pthread_self();
+}
+
+gboolean remmina_masterthread_exec_is_main_thread()
+{
+ return pthread_equal(gMainThreadID, pthread_self()) != 0;
+}
+
diff --git a/src/remmina_masterthread_exec.h b/src/remmina_masterthread_exec.h
new file mode 100644
index 0000000..622f4af
--- /dev/null
+++ b/src/remmina_masterthread_exec.h
@@ -0,0 +1,136 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 <pthread.h>
+#include "remmina_protocol_widget.h"
+#include "remmina_sftp_client.h"
+#include "remmina_ftp_client.h"
+#include "remmina_ssh_plugin.h"
+
+typedef struct remmina_masterthread_exec_data {
+ enum { FUNC_GTK_LABEL_SET_TEXT,
+ FUNC_INIT_SAVE_CRED, FUNC_CHAT_RECEIVE,
+ FUNC_FILE_GET_STRING, FUNC_FILE_SET_STRING,
+ FUNC_FTP_CLIENT_UPDATE_TASK, FUNC_FTP_CLIENT_GET_WAITING_TASK,
+ FUNC_SFTP_CLIENT_CONFIRM_RESUME,
+ FUNC_PROTOCOLWIDGET_EMIT_SIGNAL,
+ FUNC_PROTOCOLWIDGET_MPPROGRESS,
+ FUNC_PROTOCOLWIDGET_MPDESTROY,
+ FUNC_PROTOCOLWIDGET_MPSHOWRETRY,
+ FUNC_PROTOCOLWIDGET_PANELSHOWLISTEN,
+ FUNC_VTE_TERMINAL_SET_ENCODING_AND_PTY } func;
+
+ union {
+ struct {
+ GtkLabel * label;
+ const gchar * str;
+ } gtk_label_set_text;
+ struct {
+ RemminaProtocolWidget *gp;
+ } init_save_creds;
+ struct {
+ RemminaProtocolWidget * gp;
+ const gchar * text;
+ } chat_receive;
+ struct {
+ RemminaFile * remminafile;
+ const gchar * setting;
+ const gchar * retval;
+ } file_get_string;
+ struct {
+ RemminaFile * remminafile;
+ const gchar * setting;
+ const gchar * value;
+ } file_set_string;
+ struct {
+ RemminaFTPClient * client;
+ RemminaFTPTask * task;
+ } ftp_client_update_task;
+ struct {
+ RemminaFTPClient * client;
+ RemminaFTPTask * retval;
+ } ftp_client_get_waiting_task;
+ struct {
+ RemminaProtocolWidget * gp;
+ const gchar * signal_name;
+ } protocolwidget_emit_signal;
+ struct {
+ RemminaConnectionObject * cnnobj;
+ const gchar * message;
+ RemminaMessagePanelCallback response_callback;
+ gpointer response_callback_data;
+ RemminaMessagePanel * ret_mp;
+ } protocolwidget_mpprogress;
+ struct {
+ RemminaConnectionObject * cnnobj;
+ RemminaMessagePanel * mp;
+ } protocolwidget_mpdestroy;
+ struct {
+ RemminaProtocolWidget *gp;
+ } protocolwidget_mpshowretry;
+ struct {
+ RemminaProtocolWidget * gp;
+ int port;
+ } protocolwidget_panelshowlisten;
+#ifdef HAVE_LIBSSH
+ struct {
+ RemminaSFTPClient * client;
+ const gchar * path;
+ gint retval;
+ } sftp_client_confirm_resume;
+#endif
+#ifdef HAVE_LIBVTE
+ struct {
+ VteTerminal * terminal;
+ const char * codeset;
+ int master;
+ int slave;
+ } vte_terminal_set_encoding_and_pty;
+#endif
+ } p;
+
+ /* Mutex for thread synchronization */
+ pthread_mutex_t pt_mutex;
+ pthread_cond_t pt_cond;
+ /* Flag to catch cancellations */
+ gboolean cancelled;
+ gboolean complete;
+} RemminaMTExecData;
+
+void remmina_masterthread_exec_and_wait(RemminaMTExecData *d);
+
+void remmina_masterthread_exec_save_main_thread_id(void);
+gboolean remmina_masterthread_exec_is_main_thread(void);
diff --git a/src/remmina_message_panel.c b/src/remmina_message_panel.c
new file mode 100644
index 0000000..460f212
--- /dev/null
+++ b/src/remmina_message_panel.c
@@ -0,0 +1,801 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 <glib/gi18n.h>
+#include "config.h"
+#include "remmina_public.h"
+#include "remmina_widget_pool.h"
+#include "remmina_main.h"
+#include "remmina_message_panel.h"
+#include "remmina_masterthread_exec.h"
+#include "remmina/remmina_trace_calls.h"
+
+
+typedef struct
+{
+
+ RemminaMessagePanelCallback response_callback;
+ void *response_callback_data;
+ GtkWidget *w[REMMINA_MESSAGE_PANEL_MAXWIDGETID];
+
+} RemminaMessagePanelPrivate;
+G_DEFINE_TYPE_WITH_PRIVATE (RemminaMessagePanel, remmina_message_panel, GTK_TYPE_BOX)
+
+
+enum {
+ RESPONSE,
+ LAST_SIGNAL
+};
+
+static guint messagepanel_signals[LAST_SIGNAL];
+
+static const gchar btn_response_key[] = "btn_response";
+
+static void remmina_message_panel_init (RemminaMessagePanel *mp)
+{
+ TRACE_CALL(__func__);
+}
+
+static void remmina_message_panel_class_init(RemminaMessagePanelClass *class)
+{
+ TRACE_CALL(__func__);
+ // class->transform_text = my_app_label_real_transform_text;
+
+ messagepanel_signals[RESPONSE] =
+ g_signal_new ("response",
+ G_OBJECT_CLASS_TYPE (class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (RemminaMessagePanelClass, response),
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE, 1,
+ G_TYPE_INT);
+}
+
+RemminaMessagePanel *remmina_message_panel_new()
+{
+ TRACE_CALL(__func__);
+ RemminaMessagePanelPrivate *priv;
+ RemminaMessagePanel* mp;
+ mp = (RemminaMessagePanel*)g_object_new(REMMINA_TYPE_MESSAGE_PANEL,
+ "orientation", GTK_ORIENTATION_VERTICAL,
+ "spacing", 0,
+ NULL);
+
+ priv = remmina_message_panel_get_instance_private(mp);
+
+ priv->response_callback = NULL;
+ priv->response_callback_data = NULL;
+
+ /* Set widget class, for CSS styling */
+ // gtk_widget_set_name(GTK_WIDGET(mp), "remmina-cw-message-panel");
+ gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(mp)), "message_panel");
+ gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(mp)), "background");
+
+ return mp;
+}
+
+static void remmina_message_panel_button_clicked_callback(
+ GtkButton *button, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaMessagePanel *mp = (RemminaMessagePanel*)user_data;
+ RemminaMessagePanelPrivate *priv = remmina_message_panel_get_instance_private(mp);
+
+ gint btn_data;
+
+ btn_data = (gint)((gint64)g_object_get_data(G_OBJECT(button), btn_response_key));
+
+ /* Calls the callback, if defined */
+ if (priv->response_callback != NULL)
+ (*priv->response_callback)(priv->response_callback_data, btn_data);
+
+}
+
+void remmina_message_panel_setup_progress(RemminaMessagePanel *mp, const gchar *message, RemminaMessagePanelCallback response_callback, gpointer response_callback_data)
+{
+ /*
+ * Setup a message panel to show a spinner, a message like "Connecting…",
+ * and a button to cancel the action in progress
+ *
+ */
+
+ TRACE_CALL(__func__);
+ GtkBox *hbox;
+ GtkWidget *w;
+ RemminaMessagePanelPrivate *priv = remmina_message_panel_get_instance_private(mp);
+
+ if ( !remmina_masterthread_exec_is_main_thread() ) {
+ printf("WARNING: %s called in a subthread. This should not happen.\n", __func__);
+ raise(SIGINT);
+ }
+
+ hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
+
+ /* A spinner */
+ w = gtk_spinner_new();
+ gtk_box_pack_start(hbox, w, FALSE, FALSE, 0);
+ gtk_spinner_start(GTK_SPINNER(w));
+
+ /* A message */
+ w = gtk_label_new(message);
+ gtk_box_pack_start(hbox, w, TRUE, TRUE, 0);
+
+ priv->response_callback = response_callback;
+ priv->response_callback_data = response_callback_data;
+
+ /* A button to cancel the action. The cancel button is available
+ * only when a response_callback function is defined. */
+ if (response_callback) {
+ w = gtk_button_new_with_label(_("Cancel"));
+ gtk_box_pack_end(hbox, w, FALSE, FALSE, 0);
+ g_object_set_data(G_OBJECT(w), btn_response_key, (void *)GTK_RESPONSE_CANCEL);
+ g_signal_connect(G_OBJECT(w), "clicked", G_CALLBACK(remmina_message_panel_button_clicked_callback), mp);
+ }
+
+ gtk_box_pack_start(GTK_BOX(mp), GTK_WIDGET(hbox), TRUE, TRUE, 0);
+
+ gtk_widget_show_all(GTK_WIDGET(mp));
+
+}
+
+void remmina_message_panel_setup_message(RemminaMessagePanel *mp, const gchar *message, RemminaMessagePanelCallback response_callback, gpointer response_callback_data)
+{
+ /*
+ * Setup a message panel to a message to read like "Cannot connect…",
+ * and a button to close the panel
+ *
+ */
+
+ TRACE_CALL(__func__);
+ GtkBox *hbox;
+ GtkWidget *w;
+ RemminaMessagePanelPrivate *priv = remmina_message_panel_get_instance_private(mp);
+
+ if ( !remmina_masterthread_exec_is_main_thread() ) {
+ printf("WARNING: %s called in a subthread. This should not happen.\n", __func__);
+ }
+
+ hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
+
+ /* A message */
+ w = gtk_label_new(message);
+ gtk_box_pack_start(hbox, w, TRUE, TRUE, 0);
+
+ /* A button to confirm reading */
+ w = gtk_button_new_with_label(_("Close"));
+ gtk_box_pack_end(hbox, w, FALSE, FALSE, 0);
+
+ priv->response_callback = response_callback;
+ priv->response_callback_data = response_callback_data;
+
+ g_object_set_data(G_OBJECT(w), btn_response_key, (void *)GTK_RESPONSE_OK);
+ g_signal_connect(G_OBJECT(w), "clicked", G_CALLBACK(remmina_message_panel_button_clicked_callback), mp);
+
+ gtk_box_pack_start(GTK_BOX(mp), GTK_WIDGET(hbox), TRUE, TRUE, 0);
+
+ gtk_widget_show_all(GTK_WIDGET(mp));
+
+}
+
+void remmina_message_panel_setup_question(RemminaMessagePanel *mp, const gchar *message, RemminaMessagePanelCallback response_callback, gpointer response_callback_data)
+{
+ /*
+ * Setup a message panel to a message to read like "Do you accept ?",
+ * and a pair of button for Yes and No
+ * message is an HTML string
+ * Callback will receive GTK_RESPONSE_NO for No, GTK_RESPONSE_YES for Yes
+ *
+ */
+
+ TRACE_CALL(__func__);
+ GtkWidget *grid;
+ GtkWidget *bbox;
+ GtkWidget *w;
+ RemminaMessagePanelPrivate *priv = remmina_message_panel_get_instance_private(mp);
+
+ if ( !remmina_masterthread_exec_is_main_thread() ) {
+ printf("WARNING: %s called in a subthread. This should not happen. Raising SIGINT for debugging.\n", __func__);
+ raise(SIGINT);
+ }
+
+ /* Create grid */
+ grid = gtk_grid_new();
+ gtk_widget_set_halign(GTK_WIDGET(grid), GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(GTK_WIDGET(grid), GTK_ALIGN_CENTER);
+ gtk_widget_show(grid);
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 6);
+
+ /* A message, in HTML format */
+ w = gtk_label_new(NULL);
+ gtk_label_set_markup(GTK_LABEL(w), message);
+
+ gtk_widget_set_halign(GTK_WIDGET(w), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(w), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(w), 18);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(w), 9);
+ gtk_widget_set_margin_start (GTK_WIDGET(w), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(w), 18);
+ gtk_widget_show(w);
+ gtk_grid_attach(GTK_GRID(grid), w, 0, 0, 2, 1);
+
+ /* A button for yes and one for no */
+ bbox = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
+ gtk_button_box_set_layout(GTK_BUTTON_BOX(bbox), GTK_BUTTONBOX_START);
+ gtk_grid_attach(GTK_GRID(grid), bbox, 0, 1, 1, 1);
+ w = gtk_button_new_with_label(_("Yes"));
+ gtk_widget_set_valign(GTK_WIDGET(w), GTK_ALIGN_CENTER);
+ g_object_set_data(G_OBJECT(w), btn_response_key, (void *)GTK_RESPONSE_YES);
+
+ g_signal_connect(G_OBJECT(w), "clicked", G_CALLBACK(remmina_message_panel_button_clicked_callback), mp);
+ gtk_container_add(GTK_CONTAINER(bbox), w);
+
+ w = gtk_button_new_with_label(_("No"));
+ gtk_widget_set_valign(GTK_WIDGET(w), GTK_ALIGN_CENTER);
+ g_object_set_data(G_OBJECT(w), btn_response_key, (void *)GTK_RESPONSE_NO);
+
+ priv->response_callback = response_callback;
+ priv->response_callback_data = response_callback_data;
+
+ g_signal_connect(G_OBJECT(w), "clicked", G_CALLBACK(remmina_message_panel_button_clicked_callback), mp);
+ gtk_container_add(GTK_CONTAINER(bbox), w);
+
+ gtk_box_pack_start(GTK_BOX(mp), GTK_WIDGET(grid), TRUE, TRUE, 0);
+
+ gtk_widget_show_all(GTK_WIDGET(mp));
+
+}
+
+void remmina_message_panel_setup_auth(RemminaMessagePanel *mp, RemminaMessagePanelCallback response_callback, gpointer response_callback_data, const gchar *title, const gchar *password_prompt, unsigned flags)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *grid;
+ GtkWidget *password_entry;
+ GtkWidget *username_entry;
+ GtkWidget *domain_entry;
+ GtkWidget *save_password_switch;
+ GtkWidget *widget;
+ GtkWidget *bbox;
+ GtkWidget *button_ok;
+ GtkWidget *button_cancel;
+ int grid_row;
+
+ RemminaMessagePanelPrivate *priv = remmina_message_panel_get_instance_private(mp);
+
+ if ( !remmina_masterthread_exec_is_main_thread() ) {
+ printf("WARNING: %s called in a subthread. This should not happen. Raising SIGINT to debug.\n", __func__);
+ raise(SIGINT);
+ }
+
+ /* Create grid */
+ grid = gtk_grid_new();
+ gtk_widget_set_halign(GTK_WIDGET(grid), GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(GTK_WIDGET(grid), GTK_ALIGN_CENTER);
+ gtk_widget_show(grid);
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 6);
+ //gtk_grid_set_column_homogeneous(GTK_GRID(grid), TRUE);
+
+ /* Entries */
+
+ grid_row = 0;
+ widget = gtk_label_new(title);
+ gtk_style_context_add_class(gtk_widget_get_style_context(widget), "title_label");
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(widget), 9);
+ gtk_widget_set_margin_start (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(widget), 18);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, grid_row, 3, 1);
+ grid_row++;
+
+
+ if (flags & REMMINA_MESSAGE_PANEL_FLAG_USERNAME) {
+ widget = gtk_label_new(_("Username"));
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top (GTK_WIDGET(widget), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(widget), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(widget), 6);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, grid_row, 1, 1);
+
+ username_entry = gtk_entry_new();
+ // gtk_style_context_add_class(gtk_widget_get_style_context(username_entry), "panel_entry");
+ gtk_widget_set_halign(GTK_WIDGET(username_entry), GTK_ALIGN_FILL);
+ gtk_widget_set_valign(GTK_WIDGET(username_entry), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(username_entry), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(username_entry), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(username_entry), 6);
+ gtk_widget_set_margin_end (GTK_WIDGET(username_entry), 18);
+ //gtk_entry_set_activates_default (GTK_ENTRY(username_entry), TRUE);
+ gtk_grid_attach(GTK_GRID(grid), username_entry, 1, grid_row, 2, 1);
+ gtk_entry_set_max_length(GTK_ENTRY(username_entry), 100);
+
+ if (flags & REMMINA_MESSAGE_PANEL_FLAG_USERNAME_READONLY) {
+ g_object_set(username_entry, "editable", FALSE, NULL);
+ }
+
+ /*
+ if (default_username && default_username[0] != '\0') {
+ gtk_entry_set_text(GTK_ENTRY(username_entry), default_username);
+ }
+ */
+ grid_row++;
+ } else {
+ username_entry = NULL;
+ }
+
+ /* The password/key field */
+ widget = gtk_label_new(password_prompt);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_margin_top (GTK_WIDGET(widget), 3);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(widget), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(widget), 6);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, grid_row, 1, 1);
+
+ password_entry = gtk_entry_new();
+ gtk_widget_set_halign(GTK_WIDGET(password_entry), GTK_ALIGN_FILL);
+ gtk_widget_set_valign(GTK_WIDGET(password_entry), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(password_entry), 3);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(password_entry), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(password_entry), 6);
+ gtk_widget_set_margin_end (GTK_WIDGET(password_entry), 18);
+ gtk_entry_set_activates_default (GTK_ENTRY(password_entry), TRUE);
+ gtk_grid_attach(GTK_GRID(grid), password_entry, 1, grid_row, 2, 1);
+ gtk_entry_set_max_length(GTK_ENTRY(password_entry), 0);
+ gtk_entry_set_visibility(GTK_ENTRY(password_entry), FALSE);
+ gtk_entry_set_icon_from_icon_name(GTK_ENTRY(password_entry), GTK_ENTRY_ICON_SECONDARY, "org.remmina.Remmina-password-reveal-symbolic");
+ gtk_entry_set_icon_activatable(GTK_ENTRY(password_entry), GTK_ENTRY_ICON_SECONDARY, TRUE);
+ g_signal_connect(password_entry, "icon-press", G_CALLBACK(remmina_main_toggle_password_view), NULL);
+
+ grid_row++;
+
+ if (flags & REMMINA_MESSAGE_PANEL_FLAG_DOMAIN) {
+ widget = gtk_label_new(_("Domain"));
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_margin_top (GTK_WIDGET(widget), 3);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(widget), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(widget), 6);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, grid_row, 1, 1);
+
+ domain_entry = gtk_entry_new();
+ gtk_widget_set_halign(GTK_WIDGET(domain_entry), GTK_ALIGN_FILL);
+ gtk_widget_set_valign(GTK_WIDGET(domain_entry), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(domain_entry), 3);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(domain_entry), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(domain_entry), 6);
+ gtk_widget_set_margin_end (GTK_WIDGET(domain_entry), 18);
+ gtk_entry_set_activates_default (GTK_ENTRY(domain_entry), TRUE);
+ gtk_widget_show(domain_entry);
+ gtk_grid_attach(GTK_GRID(grid), domain_entry, 1, grid_row, 2, 1);
+ gtk_entry_set_max_length(GTK_ENTRY(domain_entry), 100);
+ /* if (default_domain && default_domain[0] != '\0') {
+ gtk_entry_set_text(GTK_ENTRY(domain_entry), default_domain);
+ } */
+ grid_row ++;
+ } else {
+ domain_entry = NULL;
+ }
+
+
+ widget = gtk_label_new(_("Save password"));
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_margin_top (GTK_WIDGET(widget), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(widget), 9);
+ gtk_widget_set_margin_start (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(widget), 6);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, grid_row, 1, 1);
+ save_password_switch = gtk_switch_new();
+ gtk_widget_set_halign(GTK_WIDGET(save_password_switch), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(save_password_switch), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(save_password_switch), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(save_password_switch), 9);
+ gtk_widget_set_margin_start (GTK_WIDGET(save_password_switch), 6);
+ gtk_widget_set_margin_end (GTK_WIDGET(save_password_switch), 18);
+ gtk_grid_attach(GTK_GRID(grid), save_password_switch, 1, grid_row, 2, 1);
+ if (flags & REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD) {
+ gtk_switch_set_active(GTK_SWITCH(save_password_switch), TRUE);
+ }else {
+ gtk_switch_set_active(GTK_SWITCH(save_password_switch), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(save_password_switch), FALSE);
+ }
+ grid_row ++;
+
+ /* Buttons, ok and cancel */
+ bbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
+ gtk_button_box_set_layout (GTK_BUTTON_BOX (bbox), GTK_BUTTONBOX_EDGE);
+ gtk_box_set_spacing (GTK_BOX (bbox), 40);
+ gtk_widget_set_margin_top (GTK_WIDGET(bbox), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(bbox), 18);
+ gtk_widget_set_margin_start (GTK_WIDGET(bbox), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(bbox), 18);
+ button_ok = gtk_button_new_with_label(_("_OK"));
+ gtk_button_set_use_underline(GTK_BUTTON(button_ok), TRUE);
+ gtk_widget_set_can_default(button_ok, TRUE);
+ gtk_container_add (GTK_CONTAINER (bbox), button_ok);
+ /* Buttons, ok and cancel */
+ button_cancel = gtk_button_new_with_label(_("_Cancel"));
+ gtk_button_set_use_underline(GTK_BUTTON(button_cancel), TRUE);
+ gtk_container_add (GTK_CONTAINER (bbox), button_cancel);
+ gtk_grid_attach(GTK_GRID(grid), bbox, 0, grid_row, 3, 1);
+ /* Pack it into the panel */
+ gtk_box_pack_start(GTK_BOX(mp), grid, TRUE, TRUE, 4);
+
+ priv->w[REMMINA_MESSAGE_PANEL_USERNAME] = username_entry;
+ priv->w[REMMINA_MESSAGE_PANEL_PASSWORD] = password_entry;
+ priv->w[REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD] = save_password_switch;
+ priv->w[REMMINA_MESSAGE_PANEL_DOMAIN] = domain_entry;
+ priv->w[REMMINA_MESSAGE_PANEL_BUTTONTOFOCUS] = button_ok;
+
+ priv->response_callback = response_callback;
+ priv->response_callback_data = response_callback_data;
+
+ if (username_entry) g_signal_connect_swapped (username_entry, "activate", (GCallback)gtk_widget_grab_focus, password_entry);
+ g_signal_connect_swapped (password_entry, "activate", (GCallback)gtk_widget_grab_focus, button_ok);
+ g_object_set_data(G_OBJECT(button_cancel), btn_response_key, (void *)GTK_RESPONSE_CANCEL);
+ g_signal_connect(G_OBJECT(button_cancel), "clicked", G_CALLBACK(remmina_message_panel_button_clicked_callback), mp);
+ g_object_set_data(G_OBJECT(button_ok), btn_response_key, (void *)GTK_RESPONSE_OK);
+ g_signal_connect(G_OBJECT(button_ok), "clicked", G_CALLBACK(remmina_message_panel_button_clicked_callback), mp);
+}
+
+void remmina_message_panel_setup_auth_x509(RemminaMessagePanel *mp, RemminaMessagePanelCallback response_callback, gpointer response_callback_data)
+{
+ TRACE_CALL(__func__);
+
+ GtkWidget *grid;
+ GtkWidget *widget;
+ GtkWidget *bbox;
+ GtkWidget *button_ok;
+ GtkWidget *button_cancel;
+ GtkWidget *cacert_file;
+ GtkWidget *cacrl_file;
+ GtkWidget *clientcert_file;
+ GtkWidget *clientkey_file;
+ int grid_row;
+
+ RemminaMessagePanelPrivate *priv = remmina_message_panel_get_instance_private(mp);
+
+ if ( !remmina_masterthread_exec_is_main_thread() ) {
+ printf("WARNING: %s called in a subthread. This should not happen. Raising SIGINT to debug.\n", __func__);
+ raise(SIGINT);
+ }
+
+ /* Create grid */
+ grid = gtk_grid_new();
+ gtk_widget_set_halign(GTK_WIDGET(grid), GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(GTK_WIDGET(grid), GTK_ALIGN_CENTER);
+ gtk_widget_show(grid);
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 6);
+
+ /* Entries */
+ grid_row = 0;
+ widget = gtk_label_new(_("Enter certificate authentication files"));
+ gtk_style_context_add_class(gtk_widget_get_style_context(widget), "title_label");
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(widget), 9);
+ gtk_widget_set_margin_start (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(widget), 18);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, grid_row, 3, 1);
+ grid_row++;
+
+ const gchar *lbl_cacert = _("CA Certificate File");
+ widget = gtk_label_new(lbl_cacert);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top (GTK_WIDGET(widget), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(widget), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(widget), 6);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, grid_row, 1, 1);
+ cacert_file = gtk_file_chooser_button_new(lbl_cacert, GTK_FILE_CHOOSER_ACTION_OPEN);
+ // gtk_style_context_add_class(gtk_widget_get_style_context(username_entry), "panel_entry");
+ gtk_widget_show(cacert_file);
+ gtk_widget_set_halign(GTK_WIDGET(cacert_file), GTK_ALIGN_FILL);
+ gtk_widget_set_valign(GTK_WIDGET(cacert_file), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(cacert_file), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(cacert_file), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(cacert_file), 6);
+ gtk_widget_set_margin_end (GTK_WIDGET(cacert_file), 18);
+ gtk_grid_attach(GTK_GRID(grid), cacert_file, 1, grid_row, 2, 1);
+ grid_row++;
+
+ const gchar *lbl_cacrl = _("CA CRL File");
+ widget = gtk_label_new(lbl_cacrl);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top (GTK_WIDGET(widget), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(widget), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(widget), 6);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, grid_row, 1, 1);
+ cacrl_file = gtk_file_chooser_button_new(lbl_cacrl, GTK_FILE_CHOOSER_ACTION_OPEN);
+ // gtk_style_context_add_class(gtk_widget_get_style_context(username_entry), "panel_entry");
+ gtk_widget_show(cacrl_file);
+ gtk_widget_set_halign(GTK_WIDGET(cacrl_file), GTK_ALIGN_FILL);
+ gtk_widget_set_valign(GTK_WIDGET(cacrl_file), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(cacrl_file), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(cacrl_file), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(cacrl_file), 6);
+ gtk_widget_set_margin_end (GTK_WIDGET(cacrl_file), 18);
+ gtk_grid_attach(GTK_GRID(grid), cacrl_file, 1, grid_row, 2, 1);
+ grid_row++;
+
+ const gchar *lbl_clicert = _("Client Certificate File");
+ widget = gtk_label_new(lbl_clicert);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top (GTK_WIDGET(widget), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(widget), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(widget), 6);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, grid_row, 1, 1);
+ clientcert_file = gtk_file_chooser_button_new(lbl_clicert, GTK_FILE_CHOOSER_ACTION_OPEN);
+ // gtk_style_context_add_class(gtk_widget_get_style_context(username_entry), "panel_entry");
+ gtk_widget_show(clientcert_file);
+ gtk_widget_set_halign(GTK_WIDGET(clientcert_file), GTK_ALIGN_FILL);
+ gtk_widget_set_valign(GTK_WIDGET(clientcert_file), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(clientcert_file), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(clientcert_file), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(clientcert_file), 6);
+ gtk_widget_set_margin_end (GTK_WIDGET(clientcert_file), 18);
+ gtk_grid_attach(GTK_GRID(grid), clientcert_file, 1, grid_row, 2, 1);
+ grid_row++;
+
+ const gchar *lbl_clikey = _("Client Certificate Key");
+ widget = gtk_label_new(lbl_clikey);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top (GTK_WIDGET(widget), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(widget), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(widget), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(widget), 6);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, grid_row, 1, 1);
+ clientkey_file = gtk_file_chooser_button_new(lbl_clikey, GTK_FILE_CHOOSER_ACTION_OPEN);
+ // gtk_style_context_add_class(gtk_widget_get_style_context(username_entry), "panel_entry");
+ gtk_widget_show(clientkey_file);
+ gtk_widget_set_halign(GTK_WIDGET(clientkey_file), GTK_ALIGN_FILL);
+ gtk_widget_set_valign(GTK_WIDGET(clientkey_file), GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (GTK_WIDGET(clientkey_file), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(clientkey_file), 3);
+ gtk_widget_set_margin_start (GTK_WIDGET(clientkey_file), 6);
+ gtk_widget_set_margin_end (GTK_WIDGET(clientkey_file), 18);
+ gtk_grid_attach(GTK_GRID(grid), clientkey_file, 1, grid_row, 2, 1);
+ grid_row++;
+
+ /* Buttons, ok and cancel */
+ bbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
+ gtk_button_box_set_layout (GTK_BUTTON_BOX (bbox), GTK_BUTTONBOX_EDGE);
+ gtk_box_set_spacing (GTK_BOX (bbox), 40);
+ gtk_widget_set_margin_top (GTK_WIDGET(bbox), 9);
+ gtk_widget_set_margin_bottom (GTK_WIDGET(bbox), 18);
+ gtk_widget_set_margin_start (GTK_WIDGET(bbox), 18);
+ gtk_widget_set_margin_end (GTK_WIDGET(bbox), 18);
+ button_ok = gtk_button_new_with_label(_("_OK"));
+ gtk_widget_set_can_default (button_ok, TRUE);
+
+ gtk_button_set_use_underline(GTK_BUTTON(button_ok), TRUE);
+ //gtk_widget_show(button_ok);
+ gtk_container_add (GTK_CONTAINER (bbox), button_ok);
+ //gtk_grid_attach(GTK_GRID(grid), button_ok, 0, grid_row, 1, 1);
+ /* Buttons, ok and cancel */
+ button_cancel = gtk_button_new_with_label(_("_Cancel"));
+ gtk_button_set_use_underline(GTK_BUTTON(button_cancel), TRUE);
+ //gtk_widget_show(button_cancel);
+ gtk_container_add (GTK_CONTAINER (bbox), button_cancel);
+ gtk_grid_attach(GTK_GRID(grid), bbox, 0, grid_row, 3, 1);
+ /* Pack it into the panel */
+ gtk_box_pack_start(GTK_BOX(mp), grid, TRUE, TRUE, 4);
+
+ priv->response_callback = response_callback;
+ priv->response_callback_data = response_callback_data;
+
+ priv->w[REMMINA_MESSAGE_PANEL_CACERTFILE] = cacert_file;
+ priv->w[REMMINA_MESSAGE_PANEL_CACRLFILE] = cacrl_file;
+ priv->w[REMMINA_MESSAGE_PANEL_CLIENTCERTFILE] = clientcert_file;
+ priv->w[REMMINA_MESSAGE_PANEL_CLIENTKEYFILE] = clientkey_file;
+ priv->w[REMMINA_MESSAGE_PANEL_BUTTONTOFOCUS] = button_ok;
+
+ g_object_set_data(G_OBJECT(button_cancel), btn_response_key, (void *)GTK_RESPONSE_CANCEL);
+ g_signal_connect(G_OBJECT(button_cancel), "clicked", G_CALLBACK(remmina_message_panel_button_clicked_callback), mp);
+ g_object_set_data(G_OBJECT(button_ok), btn_response_key, (void *)GTK_RESPONSE_OK);
+ g_signal_connect(G_OBJECT(button_ok), "clicked", G_CALLBACK(remmina_message_panel_button_clicked_callback), mp);
+
+}
+
+void remmina_message_panel_focus_auth_entry(RemminaMessagePanel *mp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaMessagePanelPrivate *priv;
+ GtkWidget *w;
+ const gchar *username;
+
+ if (mp == NULL)
+ return;
+ priv = remmina_message_panel_get_instance_private(mp);
+
+ /* Activate default button */
+ w = priv->w[REMMINA_MESSAGE_PANEL_BUTTONTOFOCUS];
+ if (w && G_TYPE_CHECK_INSTANCE_TYPE(w, gtk_button_get_type()))
+ gtk_widget_grab_default(w);
+
+ w = priv->w[REMMINA_MESSAGE_PANEL_USERNAME];
+ if (w == NULL)
+ {
+ w = priv->w[REMMINA_MESSAGE_PANEL_PASSWORD];
+ }else {
+ username = gtk_entry_get_text(GTK_ENTRY(w));
+ if (username[0] != 0)
+ w = priv->w[REMMINA_MESSAGE_PANEL_PASSWORD];
+ }
+ if (w == NULL)
+ return;
+
+ if (!G_TYPE_CHECK_INSTANCE_TYPE(w, gtk_entry_get_type()))
+ return;
+
+ gtk_widget_grab_focus(w);
+}
+
+void remmina_message_panel_field_set_string(RemminaMessagePanel *mp, int entryid, const gchar *text)
+{
+ RemminaMessagePanelPrivate *priv;
+
+ if (mp == NULL)
+ return;
+ priv = remmina_message_panel_get_instance_private(mp);
+
+ if (priv->w[entryid] == NULL)
+ return;
+ if (!G_TYPE_CHECK_INSTANCE_TYPE(priv->w[entryid], gtk_entry_get_type()))
+ return;
+
+ gtk_entry_set_text(GTK_ENTRY(priv->w[entryid]), text != NULL ? text : "");
+}
+
+gchar* remmina_message_panel_field_get_string(RemminaMessagePanel *mp, int entryid)
+{
+ TRACE_CALL(__func__);
+
+ RemminaMessagePanelPrivate *priv;
+
+ if (mp == NULL)
+ return NULL;
+ priv = remmina_message_panel_get_instance_private(mp);
+
+ if (priv->w[entryid] == NULL)
+ return NULL;
+ if (!G_TYPE_CHECK_INSTANCE_TYPE(priv->w[entryid], gtk_entry_get_type()))
+ return NULL;
+
+ return g_strdup(gtk_entry_get_text(GTK_ENTRY(priv->w[entryid])));
+}
+
+void remmina_message_panel_field_set_switch(RemminaMessagePanel *mp, int entryid, gboolean state)
+{
+ TRACE_CALL(__func__);
+
+ RemminaMessagePanelPrivate *priv;
+
+ if (mp == NULL)
+ return;
+ priv = remmina_message_panel_get_instance_private(mp);
+
+ if (priv->w[entryid] == NULL)
+ return;
+ if (!G_TYPE_CHECK_INSTANCE_TYPE(priv->w[entryid], gtk_switch_get_type()))
+ return;
+
+ gtk_switch_set_state(GTK_SWITCH(priv->w[entryid]), state);
+}
+
+gboolean remmina_message_panel_field_get_switch_state(RemminaMessagePanel *mp, int entryid)
+{
+ TRACE_CALL(__func__);
+
+ RemminaMessagePanelPrivate *priv;
+
+ if (mp == NULL)
+ return FALSE;
+ priv = remmina_message_panel_get_instance_private(mp);
+
+ if (priv->w[entryid] == NULL)
+ return FALSE;
+ if (!G_TYPE_CHECK_INSTANCE_TYPE(priv->w[entryid], gtk_switch_get_type()))
+ return FALSE;
+
+ return gtk_switch_get_state(GTK_SWITCH(priv->w[entryid]));
+}
+
+
+void remmina_message_panel_field_set_filename(RemminaMessagePanel *mp, int entryid, const gchar *filename)
+{
+ TRACE_CALL(__func__);
+
+ RemminaMessagePanelPrivate *priv;
+
+ if (mp == NULL)
+ return;
+ priv = remmina_message_panel_get_instance_private(mp);
+ if (priv->w[entryid] == NULL)
+ return;
+ if (!G_TYPE_CHECK_INSTANCE_TYPE(priv->w[entryid], gtk_file_chooser_button_get_type()))
+ return;
+
+ gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(priv->w[entryid]), filename);
+}
+
+gchar* remmina_message_panel_field_get_filename(RemminaMessagePanel *mp, int entryid)
+{
+ TRACE_CALL(__func__);
+
+ RemminaMessagePanelPrivate *priv;
+
+ if (mp == NULL)
+ return NULL;
+ priv = remmina_message_panel_get_instance_private(mp);
+
+ if (priv->w[entryid] == NULL)
+ return NULL;
+ if (!G_TYPE_CHECK_INSTANCE_TYPE(priv->w[entryid], gtk_file_chooser_button_get_type()))
+ return NULL;
+
+ return gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(priv->w[entryid]));
+}
+
+void remmina_message_panel_response(RemminaMessagePanel *mp, gint response_id)
+{
+ g_signal_emit(mp, messagepanel_signals[RESPONSE], 0, response_id);
+}
+
diff --git a/src/remmina_message_panel.h b/src/remmina_message_panel.h
new file mode 100644
index 0000000..2b85879
--- /dev/null
+++ b/src/remmina_message_panel.h
@@ -0,0 +1,85 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define REMMINA_TYPE_MESSAGE_PANEL (remmina_message_panel_get_type())
+G_DECLARE_DERIVABLE_TYPE(RemminaMessagePanel, remmina_message_panel, REMMINA, MESSAGE_PANEL, GtkBox)
+
+struct _RemminaMessagePanelClass {
+ GtkBoxClass parent_class;
+ void (*response) (RemminaMessagePanel *mp, gint response_id);
+};
+
+
+/* Widgets ID for dialog fields */
+enum {
+ REMMINA_MESSAGE_PANEL_USERNAME=1,
+ REMMINA_MESSAGE_PANEL_PASSWORD,
+ REMMINA_MESSAGE_PANEL_DOMAIN,
+ REMMINA_MESSAGE_PANEL_SAVEPASSWORD,
+ REMMINA_MESSAGE_PANEL_BUTTONTOFOCUS,
+ REMMINA_MESSAGE_PANEL_CACERTFILE,
+ REMMINA_MESSAGE_PANEL_CACRLFILE,
+ REMMINA_MESSAGE_PANEL_CLIENTCERTFILE,
+ REMMINA_MESSAGE_PANEL_CLIENTKEYFILE,
+ REMMINA_MESSAGE_PANEL_MAXWIDGETID
+};
+
+/* Callback function type to receive buttons notification */
+typedef void (*RemminaMessagePanelCallback)(void *user_data, int button);
+
+RemminaMessagePanel *remmina_message_panel_new(void);
+void remmina_message_panel_setup_progress(RemminaMessagePanel *mp, const gchar *message, RemminaMessagePanelCallback response_callback, gpointer response_callback_data);
+void remmina_message_panel_setup_message(RemminaMessagePanel *mp, const gchar *message, RemminaMessagePanelCallback response_callback, gpointer response_callback_data);
+void remmina_message_panel_setup_question(RemminaMessagePanel *mp, const gchar *message, RemminaMessagePanelCallback response_callback, gpointer response_callback_data);
+void remmina_message_panel_setup_auth(RemminaMessagePanel *mp, RemminaMessagePanelCallback response_callback, gpointer response_callback_data, const gchar *title, const gchar *password_prompt, unsigned flags);
+void remmina_message_panel_setup_auth_x509(RemminaMessagePanel *mp, RemminaMessagePanelCallback response_callback, gpointer response_callback_data);
+void remmina_message_panel_focus_auth_entry(RemminaMessagePanel *mp);
+void remmina_message_panel_field_set_string(RemminaMessagePanel *mp, int entryid, const gchar *text);
+gchar *remmina_message_panel_field_get_string(RemminaMessagePanel *mp, int entryid);
+void remmina_message_panel_field_set_switch(RemminaMessagePanel *mp, int entryid, gboolean state);
+gboolean remmina_message_panel_field_get_switch_state(RemminaMessagePanel *mp, int entryid);
+void remmina_message_panel_field_set_filename(RemminaMessagePanel *mp, int entryid, const gchar *filename);
+gchar *remmina_message_panel_field_get_filename(RemminaMessagePanel *mp, int entryid);
+void remmina_message_panel_response(RemminaMessagePanel *mp, gint response_id);
+
+
+G_END_DECLS
diff --git a/src/remmina_monitor.c b/src/remmina_monitor.c
new file mode 100644
index 0000000..2b57e7d
--- /dev/null
+++ b/src/remmina_monitor.c
@@ -0,0 +1,229 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta
+ *
+ * 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 "remmina_monitor.h"
+#include "remmina_log.h"
+#include "remmina_public.h"
+#include "remmina/remmina_trace_calls.h"
+
+RemminaMonitor *rm_monitor;
+
+static void remmina_monitor_can_reach_cb (GNetworkMonitor *netmonitor, GAsyncResult *result, RemminaMonitor *monitor)
+{
+ g_autoptr (GError) error = NULL;
+
+ gchar *status = NULL;
+
+ gboolean is_reachable = g_network_monitor_can_reach_finish (netmonitor, result, &error);
+
+ const gchar *addr_tostr = g_strdup(g_socket_connectable_to_string (monitor->addr));
+ //gchar *value = (gchar *)g_hash_table_lookup (monitor->server_status, addr_tostr);
+
+ if (is_reachable) {
+
+ REMMINA_DEBUG ("Network object %s is reachable", g_socket_connectable_to_string (monitor->addr));
+ status = g_strdup ("online");
+
+ } else {
+
+ REMMINA_DEBUG ("Network object %s is not reachable", g_socket_connectable_to_string (monitor->addr));
+ status = g_strdup ("offline");
+ }
+
+ if (g_hash_table_replace (monitor->server_status, g_strdup(addr_tostr), g_strdup(status))) {
+ REMMINA_DEBUG ("Inserting %s -> %s", addr_tostr, status);
+ } else {
+ REMMINA_DEBUG ("Replacing %s -> %s", addr_tostr, status);
+ }
+
+ /* Cannot use remminafile here because is freed by remmina_file_manager_iterate */
+ //if (remminafile)
+ //remmina_file_set_state_int (remminafile, "reachable", reachable);
+ g_free (status);
+}
+
+gchar *remmina_monitor_can_reach(RemminaFile *remminafile, RemminaMonitor *monitor)
+{
+ TRACE_CALL(__func__);
+
+ const gchar *server;
+ const gchar *ssh_tunnel_server;
+ const gchar *addr_tostr = NULL;
+ gchar *status = NULL;
+ gchar *ssh_tunnel_host, *srv_host;
+ gint netmonit, srv_port, ssh_tunnel_port;
+ const gchar *protocol;
+ gint default_port = 0;
+
+
+ if (!remminafile) {
+ status = g_strdup ("I/O Error");
+ REMMINA_DEBUG (status);
+ return NULL;
+ }
+
+ netmonit = remmina_file_get_int(remminafile, "enable-netmonit", FALSE);
+
+ if (!netmonit) {
+ status = g_strdup ("Monitoring disabled");
+ REMMINA_DEBUG (status);
+ return NULL;
+ }
+
+ protocol = remmina_file_get_string (remminafile, "protocol");
+
+ if (protocol && protocol[0] != '\0') {
+ REMMINA_DEBUG ("Evaluating protocol %s for monitoring", protocol);
+ if (g_strcmp0("RDP", protocol) == 0)
+ default_port = 3389;
+ if (g_strcmp0("VNC", protocol) == 0)
+ default_port = 5900;
+ if (g_strcmp0("GVNC", protocol) == 0)
+ default_port = 5900;
+ if (g_strcmp0("SPICE", protocol) == 0)
+ default_port = 5900;
+ if (g_strcmp0("WWW", protocol) == 0)
+ default_port = 443;
+ if (g_strcmp0("X2GO", protocol) == 0)
+ default_port = 22;
+ if (g_strcmp0("SSH", protocol) == 0)
+ default_port = 22;
+ if (g_strcmp0("SFTP", protocol) == 0)
+ default_port = 22;
+ if (g_strcmp0("EXEC", protocol) == 0)
+ default_port = -1;
+
+ if (default_port == 0) {
+ status = g_strdup ("Unknown protocol");
+ REMMINA_DEBUG (status);
+ return NULL;
+ }
+ if (default_port < 0) {
+ status = g_strdup ("Cannot monitor");
+ REMMINA_DEBUG (status);
+ return NULL;
+ }
+
+ ssh_tunnel_server = remmina_file_get_string(remminafile, "ssh_tunnel_server");
+ if (remmina_file_get_int(remminafile, "ssh_tunnel_enabled", FALSE)) {
+ remmina_public_get_server_port(ssh_tunnel_server, 22, &ssh_tunnel_host, &ssh_tunnel_port);
+ monitor->addr = g_network_address_new (ssh_tunnel_host, ssh_tunnel_port);
+ g_free(ssh_tunnel_host), ssh_tunnel_host = NULL;
+ } else {
+ server = remmina_file_get_string(remminafile, "server");
+ remmina_public_get_server_port(server, default_port, &srv_host, &srv_port);
+ monitor->addr = g_network_address_new (srv_host, srv_port);
+ g_free(srv_host), srv_host = NULL;
+ }
+ addr_tostr = g_strdup(g_socket_connectable_to_string (monitor->addr));
+
+ REMMINA_DEBUG ("addr is %s", addr_tostr);
+ if (monitor->connected && netmonit) {
+ REMMINA_DEBUG ("Testing for %s", addr_tostr);
+ g_network_monitor_can_reach_async (
+ monitor->netmonitor,
+ monitor->addr,
+ NULL,
+ (GAsyncReadyCallback) remmina_monitor_can_reach_cb,
+ monitor);
+ }
+
+
+ status = (gchar *)g_hash_table_lookup (monitor->server_status, addr_tostr);
+ //if (!status)
+ //g_hash_table_insert (monitor->server_status, g_strdup(addr_tostr), "offline");
+
+ }
+
+ if (!status) {
+ return g_strdup(addr_tostr);
+ } else
+ return status;
+
+ //g_free(ssh_tunnel_host), ssh_tunnel_host = NULL;
+ //g_free(srv_host), srv_host = NULL;
+ //g_free(dest), dest = NULL;
+
+}
+
+gboolean remmina_network_monitor_status (RemminaMonitor *rm_monitor)
+{
+ TRACE_CALL(__func__);
+
+ gboolean status = g_network_monitor_get_connectivity (rm_monitor->netmonitor);
+
+ rm_monitor->server_status = g_hash_table_new_full(
+ g_str_hash,
+ g_str_equal,
+ (GDestroyNotify)g_free,
+ (GDestroyNotify)g_free);
+
+ switch (status)
+ {
+ case G_NETWORK_CONNECTIVITY_LOCAL:
+ REMMINA_DEBUG ("G_NETWORK_CONNECTIVITY_LOCAL");
+ rm_monitor->connected = FALSE;
+ break;
+
+ case G_NETWORK_CONNECTIVITY_LIMITED:
+ REMMINA_DEBUG ("G_NETWORK_CONNECTIVITY_LIMITED");
+ rm_monitor->connected = FALSE;
+ break;
+
+ case G_NETWORK_CONNECTIVITY_PORTAL:
+ REMMINA_DEBUG ("G_NETWORK_CONNECTIVITY_PORTAL");
+ rm_monitor->connected = FALSE;
+ break;
+
+ case G_NETWORK_CONNECTIVITY_FULL:
+ REMMINA_DEBUG ("G_NETWORK_CONNECTIVITY_FULL");
+ rm_monitor->connected = TRUE;
+ break;
+ }
+
+ return status;
+}
+
+
+RemminaMonitor *remmina_network_monitor_new ()
+{
+ TRACE_CALL(__func__);
+
+ rm_monitor = g_new0(RemminaMonitor, 1);
+
+ rm_monitor->netmonitor = g_network_monitor_get_default ();
+
+ return rm_monitor;
+}
diff --git a/src/remmina_monitor.h b/src/remmina_monitor.h
new file mode 100644
index 0000000..1ccb47b
--- /dev/null
+++ b/src/remmina_monitor.h
@@ -0,0 +1,57 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-2023 Antenore Gatta
+ *
+ * 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/gi18n.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "remmina_file.h"
+
+typedef struct _RemminaMonitor {
+ GNetworkMonitor * netmonitor;
+ gboolean connected;
+ GSocketConnectable * addr;
+ gboolean reachable;
+ GHashTable * server_status;
+} RemminaMonitor;
+
+G_BEGIN_DECLS
+
+gboolean remmina_network_monitor_status (RemminaMonitor *rm_monitor);
+RemminaMonitor *remmina_network_monitor_new ();
+gchar *remmina_monitor_can_reach(RemminaFile *remminafile, RemminaMonitor *monitor);
+
+G_END_DECLS
diff --git a/src/remmina_mpchange.c b/src/remmina_mpchange.c
new file mode 100644
index 0000000..a5266b8
--- /dev/null
+++ b/src/remmina_mpchange.c
@@ -0,0 +1,518 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include "config.h"
+#include "remmina_mpchange.h"
+#include "remmina_file.h"
+#include "remmina_file_manager.h"
+#include "remmina_pref.h"
+#include "remmina_public.h"
+#include "remmina_main.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_log.h"
+#include "remmina/remmina_trace_calls.h"
+
+#define GET_DIALOG_OBJECT(object_name) gtk_builder_get_object(bu, object_name)
+
+struct mpchanger_params {
+ gchar *username; // New username
+ gchar *domain; // New domain
+ gchar *password; // New password
+ gchar *group;
+ gchar *gatewayusername;
+ gchar *gatewaydomain;
+ gchar *gatewaypassword;
+
+ GtkEntry *eGroup, *eUsername, *eDomain, *ePassword1, *ePassword2;
+ GtkEntry *eGatewayUsername, *eGatewayDomain, *eGatewayPassword1, *eGatewayPassword2;
+ GtkListStore* store;
+ GtkDialog* dialog;
+ GtkTreeView* table;
+ GtkButton* btnDoChange;
+ GtkLabel* statusLabel;
+
+ GtkTreeIter iter;
+ int changed_passwords_count;
+ guint sid;
+ guint searchentrychange_timeout_source_id;
+};
+
+enum {
+ COL_F = 0,
+ COL_NAME,
+ COL_GROUP,
+ COL_USERNAME,
+ COL_GATEWAY_USERNAME,
+ COL_FILENAME,
+ NUM_COLS
+};
+
+static gboolean remmina_mpchange_fieldcompare(const gchar *needle, const gchar *haystack, int *matchcount)
+{
+ TRACE_CALL(__func__);
+
+ if (needle[0] == 0) {
+ (*matchcount)++;
+ return TRUE;
+ }
+
+ if (haystack[0] == 0 || strstr(haystack, needle) == NULL){
+ return FALSE;
+ }
+
+ (*matchcount)++;
+ return TRUE;
+
+}
+
+static void remmina_mpchange_file_list_callback(RemminaFile *remminafile, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkListStore* store;
+ GtkTreeIter iter;
+ int matchcount;
+ const gchar *username, *domain, *group, *gatewayusername, *gatewaydomain;
+
+ gchar* s;
+ gchar* t;
+ struct mpchanger_params* mpcp;
+
+ mpcp = (struct mpchanger_params*)user_data;
+ store = GTK_LIST_STORE(mpcp->store);
+
+
+ username = remmina_file_get_string(remminafile, "username");
+ domain = remmina_file_get_string(remminafile, "domain");
+ group = remmina_file_get_string(remminafile, "group");
+ gatewayusername = remmina_file_get_string(remminafile, "gateway_username");
+ gatewaydomain = remmina_file_get_string(remminafile, "gateway_domain");
+
+ if (username == NULL)
+ username = "";
+
+ if (domain == NULL)
+ domain = "";
+
+ if (group == NULL)
+ group = "";
+
+ if (gatewayusername == NULL)
+ gatewayusername = "";
+
+ if (gatewaydomain == NULL)
+ gatewaydomain = "";
+
+ matchcount = 0;
+ if (!remmina_mpchange_fieldcompare(mpcp->username, username, &matchcount))
+ return;
+ if (!remmina_mpchange_fieldcompare(mpcp->domain, domain, &matchcount))
+ return;
+ if (!remmina_mpchange_fieldcompare(mpcp->group, group, &matchcount))
+ return;
+ if (!remmina_mpchange_fieldcompare(mpcp->gatewayusername, gatewayusername, &matchcount))
+ return;
+ if (!remmina_mpchange_fieldcompare(mpcp->gatewaydomain, gatewaydomain, &matchcount))
+ return;
+
+ gtk_list_store_append(store, &iter);
+ s = g_strdup_printf("%s\\%s", domain, username);
+ t = g_strdup_printf("%s\\%s", gatewaydomain, gatewayusername);
+ gtk_list_store_set(store, &iter,
+ COL_F, matchcount >= 5 ? TRUE : FALSE,
+ COL_NAME, remmina_file_get_string(remminafile, "name"),
+ COL_GROUP, group,
+ COL_USERNAME, s,
+ COL_GATEWAY_USERNAME, t,
+ COL_FILENAME, remminafile->filename,
+ -1);
+ g_free(s);
+ g_free(t);
+
+}
+
+static void remmina_mpchange_checkbox_toggle(GtkCellRendererToggle *cell, gchar *path_string, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+ struct mpchanger_params* mpcp = (struct mpchanger_params*)user_data;
+ GtkTreePath *path;
+
+ gboolean a = gtk_cell_renderer_toggle_get_active(cell);
+ path = gtk_tree_path_new_from_string(path_string);
+ gtk_tree_model_get_iter(GTK_TREE_MODEL(mpcp->store), &iter, path);
+ gtk_tree_path_free(path);
+ gtk_list_store_set(mpcp->store, &iter, COL_F, !a, -1);
+}
+
+static void remmina_mpchange_dochange(gchar* fname, struct mpchanger_params* mpcp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaFile* remminafile;
+
+ remminafile = remmina_file_load(fname);
+ if (remminafile) {
+ if(mpcp->password[0] != 0){
+ remmina_file_store_secret_plugin_password(remminafile, "password", mpcp->password);
+ }
+ if(mpcp->gatewaypassword[0] != 0){
+ remmina_file_store_secret_plugin_password(remminafile, "gateway_password", mpcp->gatewaypassword);
+ }
+ remmina_file_free(remminafile);
+ mpcp->changed_passwords_count++;
+ }
+
+}
+
+static void enable_inputs(struct mpchanger_params* mpcp, gboolean ena)
+{
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->eGroup), ena);
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->eUsername), ena);
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->eDomain), ena);
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->ePassword1), ena);
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->ePassword2), ena);
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->eGatewayUsername), ena);
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->eGatewayDomain), ena);
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->eGatewayPassword1), ena);
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->eGatewayPassword2), ena);
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->btnDoChange), ena);
+ gtk_widget_set_sensitive(GTK_WIDGET(mpcp->table), ena);
+}
+
+static gboolean changenext(gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ struct mpchanger_params* mpcp = (struct mpchanger_params*)user_data;
+ gchar* fname;
+ gboolean sel;
+
+ gtk_tree_model_get(GTK_TREE_MODEL(mpcp->store), &mpcp->iter, COL_F, &sel, -1);
+ gtk_tree_model_get(GTK_TREE_MODEL(mpcp->store), &mpcp->iter, COL_FILENAME, &fname, -1);
+ if (sel) {
+ remmina_mpchange_dochange(fname, mpcp);
+ }
+ g_free(fname);
+
+ if (gtk_tree_model_iter_next(GTK_TREE_MODEL(mpcp->store), &mpcp->iter)) {
+ return G_SOURCE_CONTINUE;
+ }else {
+ gtk_dialog_response(mpcp->dialog, 1);
+ mpcp->sid = 0;
+ return G_SOURCE_REMOVE;
+ }
+}
+
+static void remmina_mpchange_dochange_clicked(GtkButton *btn, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ struct mpchanger_params* mpcp = (struct mpchanger_params*)user_data;
+ const gchar *passwd1, *passwd2, *gatewaypasswd1, *gatewaypasswd2;
+
+ if (mpcp->searchentrychange_timeout_source_id) {
+ g_source_remove(mpcp->searchentrychange_timeout_source_id);
+ mpcp->searchentrychange_timeout_source_id = 0;
+ }
+
+ if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(mpcp->store), &mpcp->iter))
+ return;
+
+ passwd1 = gtk_entry_get_text(mpcp->ePassword1);
+ passwd2 = gtk_entry_get_text(mpcp->ePassword2);
+
+ if (g_strcmp0(passwd1, passwd2) != 0) {
+ GtkWidget *msgDialog;
+ msgDialog = gtk_message_dialog_new(GTK_WINDOW(mpcp->dialog),
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ _("The passwords do not match"));
+ gtk_dialog_run(GTK_DIALOG(msgDialog));
+ gtk_widget_destroy(msgDialog);
+ return;
+ }
+ gatewaypasswd1 = gtk_entry_get_text(mpcp->eGatewayPassword1);
+ gatewaypasswd2 = gtk_entry_get_text(mpcp->eGatewayPassword2);
+ if (g_strcmp0(gatewaypasswd1, gatewaypasswd2) != 0) {
+ GtkWidget *msgDialog;
+ msgDialog = gtk_message_dialog_new(GTK_WINDOW(mpcp->dialog),
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ _("The Gateway passwords do not match"));
+ gtk_dialog_run(GTK_DIALOG(msgDialog));
+ gtk_widget_destroy(msgDialog);
+ return;
+ }
+
+ g_free(mpcp->password);
+ mpcp->password = g_strdup(passwd1);
+ mpcp->changed_passwords_count = 0;
+
+ g_free(mpcp->gatewaypassword);
+ mpcp->gatewaypassword = g_strdup(gatewaypasswd1);
+ mpcp->changed_passwords_count = 0;
+
+ gtk_label_set_text(mpcp->statusLabel, _("Resetting passwords, please wait…"));
+
+ enable_inputs(mpcp, FALSE);
+ mpcp->sid = g_idle_add(changenext, (gpointer)mpcp);
+
+}
+
+static gboolean remmina_mpchange_searchfield_changed_to(gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ struct mpchanger_params *mpcp = (struct mpchanger_params *)user_data;
+ const gchar *s;
+
+ if (mpcp->searchentrychange_timeout_source_id) {
+ g_source_remove(mpcp->searchentrychange_timeout_source_id);
+ mpcp->searchentrychange_timeout_source_id = 0;
+ }
+
+ s = gtk_entry_get_text(mpcp->eGroup);
+ g_free(mpcp->group);
+ mpcp->group = g_strdup(s);
+
+ s = gtk_entry_get_text(mpcp->eDomain);
+ g_free(mpcp->domain);
+ mpcp->domain = g_strdup(s);
+
+ s = gtk_entry_get_text(mpcp->eUsername);
+ g_free(mpcp->username);
+ mpcp->username = g_strdup(s);
+
+ s = gtk_entry_get_text(mpcp->eGatewayDomain);
+ g_free(mpcp->gatewaydomain);
+ mpcp->gatewaydomain = g_strdup(s);
+
+ s = gtk_entry_get_text(mpcp->eGatewayUsername);
+ g_free(mpcp->gatewayusername);
+ mpcp->gatewayusername = g_strdup(s);
+
+ if (mpcp->store != NULL) {
+ gtk_tree_view_set_model(mpcp->table, NULL);
+ }
+ mpcp->store = gtk_list_store_new(NUM_COLS, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+
+ if (mpcp->group[0] != 0 || mpcp->domain[0] != 0 || mpcp->username[0] != 0 || mpcp->gatewayusername[0] != 0 || mpcp->gatewaydomain[0] != 0)
+ remmina_file_manager_iterate((GFunc)remmina_mpchange_file_list_callback, (gpointer)mpcp);
+
+ gtk_tree_view_set_model(mpcp->table, GTK_TREE_MODEL(mpcp->store));
+
+ return G_SOURCE_CONTINUE; // Source already remove at the beginning
+
+}
+
+static void remmina_mpchange_searchfield_changed(GtkSearchEntry *se, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ struct mpchanger_params *mpcp = (struct mpchanger_params *)user_data;
+
+ if (mpcp->searchentrychange_timeout_source_id) {
+ g_source_remove(mpcp->searchentrychange_timeout_source_id);
+ mpcp->searchentrychange_timeout_source_id = 0;
+ }
+
+ mpcp->searchentrychange_timeout_source_id = g_timeout_add(500, remmina_mpchange_searchfield_changed_to, user_data);
+}
+
+
+static void remmina_mpchange_stopsearch(GtkSearchEntry *entry, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ /* The stop-search signal is emitted when pressing Esc on a GtkSearchEntry. We end the dialog. */
+ struct mpchanger_params *mpcp = (struct mpchanger_params *)user_data;
+ gtk_dialog_response(mpcp->dialog, 1);
+}
+
+static gboolean remmina_file_multipasswd_changer_mt(gpointer d)
+{
+ TRACE_CALL(__func__);
+ struct mpchanger_params *mpcp = d;
+ GtkBuilder* bu;
+ GtkDialog* dialog;
+ GtkWindow* mainwindow;
+ GtkCellRendererToggle *toggle;
+ RemminaSecretPlugin *secret_plugin;
+ char *initerror;
+
+ mainwindow = remmina_main_get_window();
+
+ /* The multiple password changer works only when a secrecy plugin is available */
+ initerror = NULL;
+ secret_plugin = remmina_plugin_manager_get_secret_plugin();
+ if (secret_plugin == NULL) {
+ initerror = _("The multi password changer requires a secrecy plugin.\n");
+ }else {
+ if (!secret_plugin->is_service_available(secret_plugin)) {
+ initerror = _("The multi password changer requires a secrecy service.\n");
+ }
+ }
+ if (initerror) {
+ GtkWidget *msgDialog;
+ msgDialog = gtk_message_dialog_new(mainwindow, GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK,
+ "%s", initerror);
+ gtk_dialog_run(GTK_DIALOG(msgDialog));
+ gtk_widget_destroy(msgDialog);
+ return FALSE;
+ }
+
+
+ bu = remmina_public_gtk_builder_new_from_resource("/org/remmina/Remmina/src/../data/ui/remmina_mpc.glade");
+ if (!bu) {
+ REMMINA_DEBUG("Unable to load the multiple password changer Glade file interface\n");
+ return FALSE;
+ }
+
+ dialog = GTK_DIALOG(gtk_builder_get_object(bu, "MPCDialog"));
+ mpcp->dialog = dialog;
+ if (mainwindow)
+ gtk_window_set_transient_for(GTK_WINDOW(dialog), mainwindow);
+
+
+ mpcp->eGroup = GTK_ENTRY(GET_DIALOG_OBJECT("groupEntry"));
+ gtk_entry_set_text(mpcp->eGroup, mpcp->group);
+ g_signal_connect(G_OBJECT(mpcp->eGroup), "changed", G_CALLBACK(remmina_mpchange_searchfield_changed), (gpointer)mpcp);
+ g_signal_connect(G_OBJECT(mpcp->eGroup), "stop-search", G_CALLBACK(remmina_mpchange_stopsearch), (gpointer)mpcp);
+
+ mpcp->eUsername = GTK_ENTRY(GET_DIALOG_OBJECT("usernameEntry"));
+ gtk_entry_set_text(mpcp->eUsername, mpcp->username);
+ g_signal_connect(G_OBJECT(mpcp->eUsername), "changed", G_CALLBACK(remmina_mpchange_searchfield_changed), (gpointer)mpcp);
+
+ mpcp->eGatewayUsername = GTK_ENTRY(GET_DIALOG_OBJECT("gatewayUsernameEntry"));
+ gtk_entry_set_text(mpcp->eGatewayUsername, mpcp->gatewayusername);
+ g_signal_connect(G_OBJECT(mpcp->eGatewayUsername), "changed", G_CALLBACK(remmina_mpchange_searchfield_changed), (gpointer)mpcp);
+
+ mpcp->eDomain = GTK_ENTRY(GET_DIALOG_OBJECT("domainEntry"));
+ gtk_entry_set_text(mpcp->eDomain, mpcp->domain);
+ g_signal_connect(G_OBJECT(mpcp->eDomain), "changed", G_CALLBACK(remmina_mpchange_searchfield_changed), (gpointer)mpcp);
+
+ mpcp->eGatewayDomain = GTK_ENTRY(GET_DIALOG_OBJECT("gatewayDomainEntry"));
+ gtk_entry_set_text(mpcp->eGatewayDomain, mpcp->gatewaydomain);
+ g_signal_connect(G_OBJECT(mpcp->eGatewayDomain), "changed", G_CALLBACK(remmina_mpchange_searchfield_changed), (gpointer)mpcp);
+
+ mpcp->ePassword1 = GTK_ENTRY(GET_DIALOG_OBJECT("password1Entry"));
+ gtk_entry_set_text(mpcp->ePassword1, mpcp->password);
+
+ mpcp->eGatewayPassword1 = GTK_ENTRY(GET_DIALOG_OBJECT("gatewayPassword1Entry"));
+ gtk_entry_set_text(mpcp->eGatewayPassword1, mpcp->gatewaypassword);
+
+ mpcp->ePassword2 = GTK_ENTRY(GET_DIALOG_OBJECT("password2Entry"));
+ gtk_entry_set_text(mpcp->ePassword2, mpcp->password);
+
+ mpcp->eGatewayPassword2 = GTK_ENTRY(GET_DIALOG_OBJECT("gatewayPassword2Entry"));
+ gtk_entry_set_text(mpcp->eGatewayPassword2, mpcp->gatewaypassword);
+
+ mpcp->statusLabel = GTK_LABEL(GET_DIALOG_OBJECT("statusLabel"));
+
+
+
+
+ mpcp->store = NULL;
+
+ mpcp->table = GTK_TREE_VIEW(GET_DIALOG_OBJECT("profchangelist"));
+
+ /* Fire a fake searchfield changed, so a new list store is created */
+ remmina_mpchange_searchfield_changed(NULL, (gpointer)mpcp);
+
+ toggle = GTK_CELL_RENDERER_TOGGLE(GET_DIALOG_OBJECT("cellrenderertoggle1"));
+ g_signal_connect(G_OBJECT(toggle), "toggled", G_CALLBACK(remmina_mpchange_checkbox_toggle), (gpointer)mpcp);
+
+ mpcp->btnDoChange = GTK_BUTTON(GET_DIALOG_OBJECT("btnDoChange"));
+ g_signal_connect(mpcp->btnDoChange, "clicked", G_CALLBACK(remmina_mpchange_dochange_clicked), (gpointer)mpcp);
+
+ gtk_dialog_run(dialog);
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+
+ if (mpcp->sid) {
+ g_source_remove(mpcp->sid);
+ mpcp->sid = 0;
+ }
+
+ if (mpcp->searchentrychange_timeout_source_id) {
+ g_source_remove(mpcp->searchentrychange_timeout_source_id);
+ mpcp->searchentrychange_timeout_source_id = 0;
+ }
+
+ if (mpcp->changed_passwords_count) {
+ GtkWidget *msgDialog;
+ msgDialog = gtk_message_dialog_new(GTK_WINDOW(mpcp->dialog),
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_INFO,
+ GTK_BUTTONS_OK,
+ ngettext("%d password changed.", "%d passwords changed.", mpcp->changed_passwords_count), mpcp->changed_passwords_count);
+ gtk_dialog_run(GTK_DIALOG(msgDialog));
+ gtk_widget_destroy(msgDialog);
+ }
+
+ // Free data
+ g_free(mpcp->username);
+ g_free(mpcp->password);
+ g_free(mpcp->domain);
+ g_free(mpcp->group);
+ g_free(mpcp->gatewayusername);
+ g_free(mpcp->gatewaypassword);
+ g_free(mpcp->gatewaydomain);
+ g_free(mpcp);
+ return FALSE;
+}
+
+
+void
+remmina_mpchange_schedule(gboolean has_domain, const gchar *group, const gchar *domain, const gchar *username, const gchar *password, const gchar *gatewayusername, const gchar *gatewaydomain, const gchar *gatewaypassword)
+{
+ // We could also be called in a subthread after a successful connection
+ // (not currently implemented)
+ // So we just schedule the multipassword changer to be executed on
+ // the main thread
+
+ TRACE_CALL(__func__);
+ struct mpchanger_params *mpcp;
+
+ mpcp = g_malloc0(sizeof(struct mpchanger_params));
+ mpcp->username = g_strdup(username);
+ mpcp->password = g_strdup(password);
+ mpcp->domain = g_strdup(domain);
+ mpcp->group = g_strdup(group);
+ mpcp->gatewayusername = g_strdup(gatewayusername);
+ mpcp->gatewaypassword = g_strdup(gatewaypassword);
+ mpcp->gatewaydomain = g_strdup(gatewaydomain);
+ gdk_threads_add_idle(remmina_file_multipasswd_changer_mt, (gpointer)mpcp);
+
+}
diff --git a/src/remmina_mpchange.h b/src/remmina_mpchange.h
new file mode 100644
index 0000000..95c2417
--- /dev/null
+++ b/src/remmina_mpchange.h
@@ -0,0 +1,45 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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/types.h"
+
+#pragma once
+
+G_BEGIN_DECLS
+
+
+/* Schedule the multipassword change confirmation dialog to be executed ASAP */
+void remmina_mpchange_schedule(gboolean has_domain, const gchar *group, const gchar *domain, const gchar *username, const gchar *password, const gchar *gatewayusername, const gchar *gatewaydomain, const gchar *gatewaypassword); \ No newline at end of file
diff --git a/src/remmina_passwd.c b/src/remmina_passwd.c
new file mode 100644
index 0000000..debc2bb
--- /dev/null
+++ b/src/remmina_passwd.c
@@ -0,0 +1,163 @@
+/*
+ * 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 <stdlib.h>
+
+#include <glib/gi18n.h>
+
+#include "config.h"
+#include "remmina_public.h"
+#include "remmina_log.h"
+#include "remmina_sodium.h"
+#include "remmina_passwd.h"
+#include "remmina/remmina_trace_calls.h"
+
+static RemminaPasswdDialog *remmina_passwd_dialog;
+#define GET_OBJ(object_name) gtk_builder_get_object(remmina_passwd_dialog->builder, object_name)
+
+void remmina_passwd_repwd_on_changed(GtkEditable *editable, RemminaPasswdDialog *dialog)
+{
+ TRACE_CALL(__func__);
+ GtkCssProvider *provider;
+ const gchar *color;
+ const gchar *password;
+ const gchar *verify;
+ gboolean sensitive = FALSE;
+
+ provider = gtk_css_provider_new();
+
+ password = gtk_entry_get_text(remmina_passwd_dialog->entry_password);
+ verify = gtk_entry_get_text(remmina_passwd_dialog->entry_verify);
+ if (g_strcmp0(password, verify) == 0) {
+ color = g_strdup("green");
+ sensitive = TRUE;
+ } else
+ color = g_strdup("red");
+
+ gtk_widget_set_sensitive (GTK_WIDGET(remmina_passwd_dialog->button_submit), sensitive);
+
+ if (verify == NULL || verify[0] == '\0')
+ color = g_strdup("inherit");
+
+ gtk_css_provider_load_from_data(provider,
+ g_strdup_printf(
+ ".entry_verify {\n"
+ " color: %s;\n"
+ "}\n"
+ , color)
+ , -1, NULL);
+ gtk_style_context_add_provider_for_screen(gdk_screen_get_default(),
+ GTK_STYLE_PROVIDER(provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ gtk_widget_queue_draw(GTK_WIDGET(remmina_passwd_dialog->entry_verify));
+ g_object_unref(provider);
+}
+
+static void remmina_passwd_password_activate(GtkEntry *entry, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ gtk_widget_grab_focus(GTK_WIDGET(remmina_passwd_dialog->entry_verify));
+
+}
+
+static void remmina_passwd_cancel_clicked(GtkButton *btn, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gtk_dialog_response (remmina_passwd_dialog->dialog, GTK_RESPONSE_CANCEL);
+}
+
+static void remmina_passwd_submit_clicked(GtkButton *btn, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ remmina_passwd_dialog->password = gtk_entry_get_text(
+ GTK_ENTRY(remmina_passwd_dialog->entry_verify));
+ gtk_dialog_response (remmina_passwd_dialog->dialog, GTK_RESPONSE_ACCEPT);
+}
+
+gboolean remmina_passwd(GtkWindow *parent, gchar **unlock_password)
+{
+ TRACE_CALL(__func__);
+
+ remmina_passwd_dialog = g_new0(RemminaPasswdDialog, 1);
+ gboolean rc = FALSE;
+
+ remmina_passwd_dialog->builder = remmina_public_gtk_builder_new_from_resource("/org/remmina/Remmina/src/../data/ui/remmina_passwd.glade");
+ remmina_passwd_dialog->dialog = GTK_DIALOG(GET_OBJ("RemminaPasswdDialog"));
+ if (parent)
+ gtk_window_set_transient_for(GTK_WINDOW(remmina_passwd_dialog->dialog), parent);
+
+ remmina_passwd_dialog->entry_password = GTK_ENTRY(GET_OBJ("entry_password"));
+ remmina_passwd_dialog->entry_verify = GTK_ENTRY(GET_OBJ("entry_verify"));
+ gtk_entry_set_activates_default(GTK_ENTRY(remmina_passwd_dialog->entry_verify), TRUE);
+ remmina_passwd_dialog->button_submit = GTK_BUTTON(GET_OBJ("button_submit"));
+ gtk_widget_set_can_default(GTK_WIDGET(remmina_passwd_dialog->button_submit), TRUE);
+ gtk_widget_grab_default(GTK_WIDGET(remmina_passwd_dialog->button_submit));
+ remmina_passwd_dialog->button_cancel = GTK_BUTTON(GET_OBJ("button_cancel"));
+
+ g_signal_connect(remmina_passwd_dialog->entry_password, "activate",
+ G_CALLBACK(remmina_passwd_password_activate), (gpointer)remmina_passwd_dialog);
+ g_signal_connect(remmina_passwd_dialog->button_submit, "clicked",
+ G_CALLBACK(remmina_passwd_submit_clicked), (gpointer)remmina_passwd_dialog);
+ g_signal_connect(remmina_passwd_dialog->button_cancel, "clicked",
+ G_CALLBACK(remmina_passwd_cancel_clicked), (gpointer)remmina_passwd_dialog);
+
+ /* Connect signals */
+ gtk_builder_connect_signals(remmina_passwd_dialog->builder, NULL);
+
+ int result = gtk_dialog_run(remmina_passwd_dialog->dialog);
+ switch (result)
+ {
+ case GTK_RESPONSE_ACCEPT:
+#if SODIUM_VERSION_INT >= 90200
+ //REMMINA_DEBUG ("Password before encryption: %s", remmina_passwd_dialog->password);
+ *unlock_password = remmina_sodium_pwhash_str(remmina_passwd_dialog->password);
+#else
+ *unlock_password = g_strdup(remmina_passwd_dialog->password);
+#endif
+ //REMMINA_DEBUG ("Password after encryption is: %s", *unlock_password);
+ remmina_passwd_dialog->password = NULL;
+ rc = TRUE;
+ break;
+ default:
+ remmina_passwd_dialog->password = NULL;
+ *unlock_password = NULL;
+ rc = FALSE;
+ break;
+ }
+ gtk_widget_destroy(GTK_WIDGET(remmina_passwd_dialog->dialog));
+ remmina_passwd_dialog->dialog = NULL;
+ return rc;
+}
diff --git a/src/remmina_passwd.h b/src/remmina_passwd.h
new file mode 100644
index 0000000..6abeb91
--- /dev/null
+++ b/src/remmina_passwd.h
@@ -0,0 +1,58 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * 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 <gtk/gtk.h>
+#include <glib.h>
+
+typedef struct _RemminaPasswdDialog {
+ GtkBuilder * builder;
+ GtkDialog * dialog;
+
+ GtkEntry * entry_password;
+ GtkEntry * entry_verify;
+ GtkButton * button_submit;
+ GtkButton * button_cancel;
+
+ const gchar * password;
+
+} RemminaPasswdDialog;
+
+
+G_BEGIN_DECLS
+
+gboolean remmina_passwd(GtkWindow *parent, gchar **password_setting);
+
+G_END_DECLS
diff --git a/src/remmina_plugin_manager.c b/src/remmina_plugin_manager.c
new file mode 100644
index 0000000..60c0bee
--- /dev/null
+++ b/src/remmina_plugin_manager.c
@@ -0,0 +1,1555 @@
+/*
+ * 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
+ * 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.
+ *
+ */
+
+#include "config.h"
+
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include <gmodule.h>
+#include <json-glib/json-glib.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#elif defined(GDK_WINDOWING_WAYLAND)
+#include <gdk/gdkwayland.h>
+#endif
+#include "remmina_public.h"
+#include "remmina_main.h"
+#include "remmina_file_manager.h"
+#include "remmina_pref.h"
+#include "remmina_info.h"
+#include "remmina_protocol_widget.h"
+#include "remmina_log.h"
+#include "remmina_widget_pool.h"
+#include "rcw.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_plugin_native.h"
+#include "remmina_public.h"
+#include "remmina_masterthread_exec.h"
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_curl_connector.h"
+#include "remmina_utils.h"
+#include "remmina_unlock.h"
+
+static GPtrArray* remmina_plugin_table = NULL;
+
+static GPtrArray* remmina_available_plugin_table = NULL;
+
+static GtkDialog* remmina_plugin_window = NULL;
+
+static SignalData* remmina_plugin_signal_data = NULL;
+
+/* A GHashTable of GHashTables where to store the names of the encrypted settings */
+static GHashTable *encrypted_settings_cache = NULL;
+
+/* There can be only one secret plugin loaded */
+static RemminaSecretPlugin *remmina_secret_plugin = NULL;
+
+
+
+// TRANSLATORS: "Language Wrapper" is a wrapper for plugins written in other programmin languages (Python in this context)
+static const gchar *remmina_plugin_type_name[] =
+{ N_("Protocol"), N_("Entry"), N_("File"), N_("Tool"), N_("Preference"), N_("Secret"), N_("Language Wrapper"), NULL };
+
+static gint remmina_plugin_manager_compare_func(RemminaPlugin **a, RemminaPlugin **b)
+{
+ TRACE_CALL(__func__);
+ return g_strcmp0((*a)->name, (*b)->name);
+}
+
+static void htdestroy(gpointer ht)
+{
+ g_hash_table_unref(ht);
+}
+
+static void init_settings_cache(RemminaPlugin *plugin)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolPlugin *protocol_plugin;
+ const RemminaProtocolSetting* setting_iter;
+ GHashTable *pht;
+
+ /* This code make a encrypted setting cache only for protocol plugins */
+
+ if (plugin->type != REMMINA_PLUGIN_TYPE_PROTOCOL) {
+ return;
+ }
+
+ if (encrypted_settings_cache == NULL)
+ encrypted_settings_cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, htdestroy);
+
+ if (!(pht = g_hash_table_lookup(encrypted_settings_cache, plugin->name))) {
+ pht = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+ g_hash_table_insert(encrypted_settings_cache, g_strdup(plugin->name), pht);
+ }
+
+ /* Some settings are encrypted "by name" */
+ /* g_hash_table_insert(pht, g_strdup("proxy_password"), (gpointer)TRUE); */
+
+ g_hash_table_insert(pht, g_strdup("ssh_tunnel_password"), (gpointer)TRUE);
+ g_hash_table_insert(pht, g_strdup("ssh_tunnel_passphrase"), (gpointer)TRUE);
+
+ /* ssh_password is no longer used starting from remmina 1.4.
+ * But we MUST mark it as encrypted setting, or the migration procedure will fail */
+ g_hash_table_insert(pht, g_strdup("ssh_password"), (gpointer)TRUE);
+
+ /* Other settings are encrypted because of their type */
+ protocol_plugin = (RemminaProtocolPlugin *)plugin;
+ setting_iter = protocol_plugin->basic_settings;
+ if (setting_iter) {
+ while (setting_iter->type != REMMINA_PROTOCOL_SETTING_TYPE_END) {
+ if (setting_iter->type == REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD)
+ g_hash_table_insert(pht, g_strdup(remmina_plugin_manager_get_canonical_setting_name(setting_iter)), (gpointer)TRUE);
+ setting_iter++;
+ }
+ }
+ setting_iter = protocol_plugin->advanced_settings;
+ if (setting_iter) {
+ while (setting_iter->type != REMMINA_PROTOCOL_SETTING_TYPE_END) {
+ if (setting_iter->type == REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD)
+ g_hash_table_insert(pht, g_strdup(remmina_plugin_manager_get_canonical_setting_name(setting_iter)), (gpointer)TRUE);
+ setting_iter++;
+ }
+ }
+
+}
+
+static gboolean remmina_plugin_manager_register_plugin(RemminaPlugin *plugin)
+{
+ TRACE_CALL(__func__);
+ if (plugin->type == REMMINA_PLUGIN_TYPE_SECRET) {
+ REMMINA_DEBUG("Remmina plugin %s (type=%s) has been registered, but is not yet initialized/activated. "
+ "The initialization order is %d.", plugin->name,
+ _(remmina_plugin_type_name[plugin->type]),
+ ((RemminaSecretPlugin*)plugin)->init_order);
+ }
+ init_settings_cache(plugin);
+
+ g_ptr_array_add(remmina_plugin_table, plugin);
+ g_ptr_array_sort(remmina_plugin_table, (GCompareFunc)remmina_plugin_manager_compare_func);
+ return TRUE;
+}
+
+gboolean remmina_gtksocket_available()
+{
+ GdkDisplayManager* dm;
+ GdkDisplay* d;
+ gboolean available;
+
+ dm = gdk_display_manager_get();
+ d = gdk_display_manager_get_default_display(dm);
+ available = FALSE;
+
+#ifdef GDK_WINDOWING_X11
+ if (GDK_IS_X11_DISPLAY(d)) {
+ /* GtkSocket support is available only under X.Org */
+ available = TRUE;
+ }
+#endif
+ return available;
+}
+
+RemminaPluginService remmina_plugin_manager_service =
+{
+ remmina_plugin_manager_register_plugin,
+ remmina_protocol_widget_get_width,
+ remmina_protocol_widget_set_width,
+ remmina_protocol_widget_get_height,
+ remmina_protocol_widget_set_height,
+ remmina_protocol_widget_get_current_scale_mode,
+ remmina_protocol_widget_get_expand,
+ remmina_protocol_widget_set_expand,
+ remmina_protocol_widget_has_error,
+ remmina_protocol_widget_set_error,
+ remmina_protocol_widget_is_closed,
+ remmina_protocol_widget_get_file,
+ remmina_protocol_widget_emit_signal,
+ remmina_protocol_widget_register_hostkey,
+ remmina_protocol_widget_start_direct_tunnel,
+ remmina_protocol_widget_start_reverse_tunnel,
+ remmina_protocol_widget_start_xport_tunnel,
+ remmina_protocol_widget_set_display,
+ remmina_protocol_widget_signal_connection_closed,
+ remmina_protocol_widget_signal_connection_opened,
+ remmina_protocol_widget_update_align,
+ remmina_protocol_widget_lock_dynres,
+ remmina_protocol_widget_unlock_dynres,
+ remmina_protocol_widget_desktop_resize,
+ remmina_protocol_widget_panel_auth,
+ remmina_protocol_widget_panel_new_certificate,
+ remmina_protocol_widget_panel_changed_certificate,
+ remmina_protocol_widget_get_username,
+ remmina_protocol_widget_get_password,
+ remmina_protocol_widget_get_domain,
+ remmina_protocol_widget_get_savepassword,
+ remmina_protocol_widget_panel_authx509,
+ remmina_protocol_widget_get_cacert,
+ remmina_protocol_widget_get_cacrl,
+ remmina_protocol_widget_get_clientcert,
+ remmina_protocol_widget_get_clientkey,
+ remmina_protocol_widget_save_cred,
+ remmina_protocol_widget_panel_show_listen,
+ remmina_protocol_widget_panel_show_retry,
+ remmina_protocol_widget_panel_show,
+ remmina_protocol_widget_panel_hide,
+ remmina_protocol_widget_ssh_exec,
+ remmina_protocol_widget_chat_open,
+ remmina_protocol_widget_chat_close,
+ remmina_protocol_widget_chat_receive,
+ remmina_protocol_widget_send_keys_signals,
+
+ remmina_file_get_datadir,
+
+ remmina_file_new,
+ remmina_file_get_filename,
+ remmina_file_set_string,
+ remmina_file_get_string,
+ remmina_file_get_secret,
+ remmina_file_set_int,
+ remmina_file_get_int,
+ remmina_file_get_double,
+ remmina_file_unsave_passwords,
+
+ remmina_pref_set_value,
+ remmina_pref_get_value,
+ remmina_pref_get_scale_quality,
+ remmina_pref_get_sshtunnel_port,
+ remmina_pref_get_ssh_loglevel,
+ remmina_pref_get_ssh_parseconfig,
+ remmina_pref_keymap_get_table,
+ remmina_pref_keymap_get_keyval,
+
+ _remmina_info,
+ _remmina_message,
+ _remmina_debug,
+ _remmina_warning,
+ _remmina_audit,
+ _remmina_error,
+ _remmina_critical,
+ remmina_log_print,
+ remmina_log_printf,
+
+ remmina_widget_pool_register,
+
+ rcw_open_from_file_full,
+ remmina_public_open_unix_sock,
+ remmina_public_get_server_port,
+ remmina_masterthread_exec_is_main_thread,
+ remmina_gtksocket_available,
+ remmina_protocol_widget_get_profile_remote_width,
+ remmina_protocol_widget_get_profile_remote_height,
+ remmina_protocol_widget_get_name,
+ remmina_protocol_widget_get_width,
+ remmina_protocol_widget_get_height,
+ remmina_protocol_widget_set_width,
+ remmina_protocol_widget_set_height,
+ remmina_protocol_widget_get_current_scale_mode,
+ remmina_protocol_widget_get_expand,
+ remmina_protocol_widget_set_expand,
+ remmina_protocol_widget_set_error,
+ remmina_protocol_widget_has_error,
+ remmina_protocol_widget_gtkviewport,
+ remmina_protocol_widget_is_closed,
+ remmina_protocol_widget_get_file,
+ remmina_protocol_widget_panel_auth,
+ remmina_protocol_widget_register_hostkey,
+ remmina_protocol_widget_start_direct_tunnel,
+ remmina_protocol_widget_start_reverse_tunnel,
+ remmina_protocol_widget_send_keys_signals,
+ remmina_protocol_widget_chat_receive,
+ remmina_protocol_widget_panel_hide,
+ remmina_protocol_widget_chat_open,
+ remmina_protocol_widget_ssh_exec,
+ remmina_protocol_widget_panel_show,
+ remmina_protocol_widget_panel_show_retry,
+ remmina_protocol_widget_start_xport_tunnel,
+ remmina_protocol_widget_set_display,
+ remmina_protocol_widget_signal_connection_closed,
+ remmina_protocol_widget_signal_connection_opened,
+ remmina_protocol_widget_update_align,
+ remmina_protocol_widget_unlock_dynres,
+ remmina_protocol_widget_desktop_resize,
+ remmina_protocol_widget_panel_new_certificate,
+ remmina_protocol_widget_panel_changed_certificate,
+ remmina_protocol_widget_get_username,
+ remmina_protocol_widget_get_password,
+ remmina_protocol_widget_get_domain,
+ remmina_protocol_widget_get_savepassword,
+ remmina_protocol_widget_panel_authx509,
+ remmina_protocol_widget_get_cacert,
+ remmina_protocol_widget_get_cacrl,
+ remmina_protocol_widget_get_clientcert,
+ remmina_protocol_widget_get_clientkey,
+ remmina_protocol_widget_save_cred,
+ remmina_protocol_widget_panel_show_listen,
+ remmina_widget_pool_register,
+ rcw_open_from_file_full,
+ remmina_main_show_dialog,
+ remmina_main_get_window,
+ remmina_unlock_new,
+};
+
+const char *get_filename_ext(const char *filename) {
+ const char* last = strrchr(filename, '/');
+ const char *dot = strrchr(last, '.');
+ if(!dot || dot == filename) return "";
+ return dot + 1;
+}
+
+static gint compare_secret_plugin_init_order(gconstpointer a, gconstpointer b)
+{
+ RemminaSecretPlugin *sa, *sb;
+
+ sa = (RemminaSecretPlugin*)a;
+ sb = (RemminaSecretPlugin*)b;
+
+ if (sa->init_order > sb->init_order)
+ return 1;
+ else if (sa->init_order < sb->init_order)
+ return -1;
+ return 0;
+}
+
+gchar* remmina_plugin_manager_create_alt_plugin_dir()
+{
+ gchar *plugin_dir;
+ plugin_dir = g_build_path("/", g_get_user_config_dir(), "remmina", "plugins", NULL);
+
+ if (g_file_test(plugin_dir, G_FILE_TEST_IS_DIR)) {
+ // Do nothing, directory already exists
+ }
+ else
+ {
+ gint result = g_mkdir_with_parents(plugin_dir, 0755);
+ if (result != 0)
+ {
+ plugin_dir = NULL;
+ }
+ }
+ return plugin_dir;
+}
+
+
+void remmina_plugin_manager_load_plugins(GPtrArray *plugin_dirs, int array_size)
+{
+ TRACE_CALL(__func__);
+ GDir *dir;
+ const gchar *name, *ptr;
+ gchar *fullpath;
+ RemminaPlugin *plugin;
+ RemminaSecretPlugin *sp;
+ guint i, j;
+ GSList *secret_plugins;
+ GSList *sple;
+ GPtrArray *alternative_language_plugins;
+ GPtrArray *loaded_plugins = g_ptr_array_new();
+ alternative_language_plugins = g_ptr_array_new();
+ char* plugin_dir = NULL;
+
+ for (i = 0; i < array_size; i++){
+ plugin_dir = g_ptr_array_remove_index(plugin_dirs, 0);
+ if (plugin_dir){
+ dir = g_dir_open(plugin_dir, 0, NULL);
+
+ if (dir == NULL){
+ continue;
+ }
+ while ((name = g_dir_read_name(dir)) != NULL) {
+ if ((ptr = strrchr(name, '.')) == NULL)
+ continue;
+ ptr++;
+ fullpath = g_strconcat(plugin_dir, "/", name, NULL);
+ if (!remmina_plugin_manager_loader_supported(ptr)) {
+ g_ptr_array_add(alternative_language_plugins, g_strconcat(plugin_dir, "/", name, NULL));
+ g_free(fullpath);
+ continue;
+ }
+ if (!g_ptr_array_find_with_equal_func(loaded_plugins, name, g_str_equal, NULL)){
+ if (remmina_plugin_native_load(&remmina_plugin_manager_service, fullpath)){
+ g_ptr_array_add(loaded_plugins, g_strdup(name));
+ }
+ }
+
+ g_free(fullpath);
+ }
+ g_dir_close(dir);
+ }
+ }
+
+
+ while (alternative_language_plugins->len > 0) {
+ gboolean has_loaded = FALSE;
+ gchar* name = (gchar*)g_ptr_array_remove_index(alternative_language_plugins, 0);
+ const gchar* ext = get_filename_ext(name);
+ char* filename = strrchr(name, '/');
+
+ for (j = 0; j < remmina_plugin_table->len && !has_loaded; j++) {
+ plugin = (RemminaPlugin*)g_ptr_array_index(remmina_plugin_table, j);
+ if (plugin->type == REMMINA_PLUGIN_TYPE_LANGUAGE_WRAPPER) {
+ RemminaLanguageWrapperPlugin* wrapper_plugin = (RemminaLanguageWrapperPlugin*)plugin;
+ const gchar** supported_extention = wrapper_plugin->supported_extentions;
+ while (*supported_extention) {
+ if (g_str_equal(*supported_extention, ext)) {
+ if (!g_ptr_array_find_with_equal_func(loaded_plugins, name, g_str_equal, NULL)){
+ has_loaded = wrapper_plugin->load(wrapper_plugin, name);
+ if (has_loaded) {
+ if (filename){
+ g_ptr_array_add(loaded_plugins, filename);
+ }
+ break;
+ }
+ }
+ }
+ supported_extention++;
+ }
+ if (has_loaded) break;
+ }
+ }
+
+ if (!has_loaded) {
+ g_print("%s: Skip unsupported file type '%s'\n", name, ext);
+ }
+
+ g_free(name);
+ }
+
+ /* Now all secret plugins needs to initialize, following their init_order.
+ * The 1st plugin which will initialize correctly will be
+ * the default remmina_secret_plugin */
+
+ if (remmina_secret_plugin)
+ g_print("Internal ERROR: remmina_secret_plugin must be null here\n");
+
+ secret_plugins = NULL;
+ for (i = 0; i < remmina_plugin_table->len; i++) {
+ plugin = (RemminaPlugin*)g_ptr_array_index(remmina_plugin_table, i);
+ if (plugin->type == REMMINA_PLUGIN_TYPE_SECRET) {
+ secret_plugins = g_slist_insert_sorted(secret_plugins, (gpointer)plugin, compare_secret_plugin_init_order);
+ }
+ }
+
+ sple = secret_plugins;
+ while(sple != NULL) {
+ sp = (RemminaSecretPlugin*)sple->data;
+ if (sp->init(sp)) {
+ REMMINA_DEBUG("The %s secret plugin has been initialized and it will be your default secret plugin",
+ sp->name);
+ remmina_secret_plugin = sp;
+ break;
+ }
+ sple = sple->next;
+ }
+
+ g_slist_free(secret_plugins);
+ g_ptr_array_free(alternative_language_plugins, TRUE);
+ g_ptr_array_free(loaded_plugins, TRUE);
+}
+
+void remmina_plugin_manager_init()
+{
+ TRACE_CALL(__func__);
+
+ gchar* alternative_dir;
+
+ remmina_plugin_table = g_ptr_array_new();
+ remmina_available_plugin_table = g_ptr_array_new();
+ GPtrArray *plugin_dirs = g_ptr_array_new();
+ int array_size = 1;
+
+ if (!g_module_supported()) {
+ g_print("Dynamic loading of plugins is not supported on this platform!\n");
+ return;
+ }
+ alternative_dir = remmina_plugin_manager_create_alt_plugin_dir();
+
+ if(alternative_dir != NULL){
+ g_ptr_array_add(plugin_dirs, alternative_dir);
+ array_size += 1;
+
+ }
+ g_ptr_array_add(plugin_dirs, REMMINA_RUNTIME_PLUGINDIR);
+ remmina_plugin_manager_load_plugins(plugin_dirs, array_size);
+
+
+ if (alternative_dir){
+ g_free(alternative_dir);
+ }
+
+ if (plugin_dirs != NULL) {
+ g_ptr_array_free(plugin_dirs, TRUE);
+ }
+}
+
+/*
+ * Creates a JsonNode with all the installed plugin names and version
+ *
+ * @returns NULL if there is an error creating the json builder object. Otherwise, return the JsonNode object with the plugin data.
+ *
+ */
+JsonNode *remmina_plugin_manager_get_installed_plugins()
+{
+ TRACE_CALL(__func__);
+ JsonBuilder *b;
+ JsonNode *r;
+ RemminaPlugin *plugin;
+ guint i;
+
+ b = json_builder_new();
+ if(b != NULL)
+ {
+ json_builder_begin_object(b);
+
+ for (i = 0; i < remmina_plugin_table->len; i++) {
+ plugin = (RemminaPlugin*)g_ptr_array_index(remmina_plugin_table, i);
+ json_builder_set_member_name(b, plugin->name);
+ json_builder_add_string_value(b, plugin->version);
+ }
+ json_builder_end_object(b);
+ r = json_builder_get_root(b);
+ g_object_unref(b);
+
+ return r;
+ }
+ else
+ {
+ return NULL;
+ }
+}
+
+gboolean remmina_plugin_manager_is_python_wrapper_installed()
+{
+ gchar* name = "Python Wrapper";
+ RemminaPlugin *plugin;
+
+ for(int i = 0; i < remmina_plugin_table->len; i++) {
+ plugin = (RemminaPlugin*)g_ptr_array_index(remmina_plugin_table, i);
+ if(g_strcmp0(plugin->name, name) == 0)
+ {
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+/*
+ * Creates a JsonNode with some environment data as well as the installed plugins
+ *
+ * @returns NULL if there is an error creating the json builder object. Otherwise, return the JsonNode object with the plugin data.
+ *
+ */
+JsonNode *remmina_plugin_manager_plugin_stats_get_all()
+{
+ JsonNode *n;
+ gchar *unenc_p, *enc_p, *uid;
+ JsonGenerator *g;
+ JsonBuilder *b_inner, *b_outer;
+
+ b_outer = json_builder_new();
+ if (b_outer == NULL) {
+ g_object_unref(b_outer);
+ return NULL;
+ }
+ json_builder_begin_object(b_outer);
+
+ b_inner = json_builder_new();
+ if(b_inner != NULL)
+ {
+ json_builder_begin_object(b_inner);
+
+ //get architecture and python version to determine what plugins are compatible
+ n = remmina_info_stats_get_os_info();
+ json_builder_set_member_name(b_inner, "OS_INFO");
+ json_builder_add_value(b_inner, n);
+
+ n = remmina_info_stats_get_python();
+ json_builder_set_member_name(b_inner, "PYTHON");
+ json_builder_add_value(b_inner, n);
+
+ n = remmina_info_stats_get_uid();
+ uid = g_strdup(json_node_get_string(n));
+ json_builder_set_member_name(b_outer, "UID");
+ json_builder_add_string_value(b_outer, uid);
+
+ n = remmina_plugin_manager_get_installed_plugins();
+ json_builder_set_member_name(b_inner, "PLUGINS");
+ json_builder_add_value(b_inner, n);
+
+ json_builder_end_object(b_inner);
+
+ n = json_builder_get_root(b_inner);
+ g_object_unref(b_inner);
+
+ g = json_generator_new();
+ json_generator_set_root(g, n);
+ unenc_p = json_generator_to_data(g, NULL);
+
+
+ enc_p = g_base64_encode((guchar *)unenc_p, strlen(unenc_p));
+ if (enc_p == NULL) {
+ g_object_unref(b_outer);
+ g_object_unref(g);
+ g_free(uid);
+ g_free(unenc_p);
+ return NULL;
+ }
+
+
+ json_builder_set_member_name(b_outer, "encdata");
+ json_builder_add_string_value(b_outer, enc_p);
+
+ if(remmina_plugin_manager_is_python_wrapper_installed() == TRUE) {
+ json_builder_set_member_name(b_outer, "pywrapper_installed");
+ json_builder_add_boolean_value(b_outer, TRUE);
+ } else {
+ json_builder_set_member_name(b_outer, "pywrapper_installed");
+ json_builder_add_boolean_value(b_outer, FALSE);
+ }
+
+ json_builder_end_object(b_outer);
+ n = json_builder_get_root(b_outer);
+ g_object_unref(b_outer);
+
+ g_free(enc_p);
+ g_object_unref(g);
+ g_free(unenc_p);
+ g_free(uid);
+
+ return n;
+ }
+ else
+ {
+ return NULL;
+ }
+}
+
+gboolean remmina_plugin_manager_loader_supported(const char *filetype) {
+ TRACE_CALL(__func__);
+ return g_str_equal(G_MODULE_SUFFIX, filetype);
+}
+
+RemminaPlugin* remmina_plugin_manager_get_plugin(RemminaPluginType type, const gchar *name)
+{
+ TRACE_CALL(__func__);
+ RemminaPlugin *plugin;
+ guint i;
+
+ for (i = 0; i < remmina_plugin_table->len; i++) {
+ plugin = (RemminaPlugin*)g_ptr_array_index(remmina_plugin_table, i);
+ if (plugin->type == type && g_strcmp0(plugin->name, name) == 0) {
+ return plugin;
+ }
+ }
+ return NULL;
+}
+
+const gchar *remmina_plugin_manager_get_canonical_setting_name(const RemminaProtocolSetting* setting)
+{
+ if (setting->name == NULL) {
+ if (setting->type == REMMINA_PROTOCOL_SETTING_TYPE_SERVER)
+ return "server";
+ if (setting->type == REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD)
+ return "password";
+ if (setting->type == REMMINA_PROTOCOL_SETTING_TYPE_RESOLUTION)
+ return "resolution";
+ if (setting->type == REMMINA_PROTOCOL_SETTING_TYPE_ASSISTANCE)
+ return "assistance_mode";
+ return "missing_setting_name_into_plugin_RemminaProtocolSetting";
+ }
+ return setting->name;
+}
+
+void remmina_plugin_manager_for_each_plugin(RemminaPluginType type, RemminaPluginFunc func, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaPlugin *plugin;
+ guint i;
+
+ for (i = 0; i < remmina_plugin_table->len; i++) {
+ plugin = (RemminaPlugin*)g_ptr_array_index(remmina_plugin_table, i);
+ if (plugin->type == type) {
+ func((gchar*)plugin->name, plugin, data);
+ }
+ }
+}
+
+/* A copy of remmina_plugin_manager_show and remmina_plugin_manager_show_for_each
+ * This is because we want to print the list of plugins, and their versions, to the standard output
+ * with the remmina command line option --full-version instead of using the plugins widget
+ ** @todo Investigate to use only GListStore and than pass the elements to be shown to 2 separate
+ * functions
+ * WARNING: GListStore is supported only from GLib 2.44 */
+static gboolean remmina_plugin_manager_show_for_each_stdout(RemminaPlugin *plugin)
+{
+ TRACE_CALL(__func__);
+
+ g_print("%-20s%-16s%-64s%-10s\n", plugin->name,
+ _(remmina_plugin_type_name[plugin->type]),
+ g_dgettext(plugin->domain, plugin->description),
+ plugin->version);
+ return FALSE;
+}
+
+void remmina_plugin_manager_show_stdout()
+{
+ TRACE_CALL(__func__);
+ g_print("%-20s%-16s%-64s%-10s\n", "NAME", "TYPE", "DESCRIPTION", "PLUGIN AND LIBRARY VERSION");
+ g_ptr_array_foreach(remmina_plugin_table, (GFunc)remmina_plugin_manager_show_for_each_stdout, NULL);
+}
+
+static gboolean remmina_plugin_manager_show_for_each(RemminaPlugin *plugin, GtkListStore *store)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, FALSE, 1, plugin->name, 2, _(remmina_plugin_type_name[plugin->type]), 3,
+ g_dgettext(plugin->domain, plugin->description), 4, "Installed", 5, plugin->version, -1);
+
+ return FALSE;
+}
+
+static gboolean remmina_plugin_manager_show_for_each_available(RemminaPlugin *plugin, GtkListStore *store)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+
+ gtk_list_store_append(store, &iter);
+
+ gtk_list_store_set(store, &iter, 0, FALSE, 1, plugin->name, 2, _(remmina_plugin_type_name[plugin->type]), 3,
+ g_dgettext(plugin->domain, plugin->description), 4, "Available", 5, plugin->version, -1);
+
+ return FALSE;
+}
+
+void result_dialog_cleanup(GtkDialog * dialog, gint response_id, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+
+ gtk_widget_hide(remmina_plugin_signal_data->label);
+ gtk_spinner_stop(GTK_SPINNER(remmina_plugin_signal_data->spinner));
+ gtk_widget_set_visible(remmina_plugin_signal_data->spinner, FALSE);
+}
+
+void remmina_plugin_manager_download_result_dialog(GtkDialog * dialog, gchar * message)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *child_dialog, *content_area, *label;
+
+ child_dialog = gtk_dialog_new_with_buttons(_("Plugin Download"), GTK_WINDOW(dialog), GTK_DIALOG_MODAL, _("_OK"), 1, NULL);
+ content_area = gtk_dialog_get_content_area (GTK_DIALOG (child_dialog));
+ label = gtk_label_new(message);
+ remmina_plugin_signal_data->downloading = FALSE;
+
+ gtk_window_set_default_size(GTK_WINDOW(child_dialog), 500, 50);
+ gtk_container_add(GTK_CONTAINER (content_area), label);
+ g_signal_connect(G_OBJECT(child_dialog), "response", G_CALLBACK(result_dialog_cleanup), NULL);
+ gtk_widget_show_all(child_dialog);
+}
+
+void remmina_plugin_manager_toggle_checkbox(GtkCellRendererToggle * cell, gchar * path, GtkListStore * model)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+ gboolean active;
+
+ active = gtk_cell_renderer_toggle_get_active (cell);
+
+ gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (model), &iter, path);
+ if (active) {
+ gtk_cell_renderer_set_alignment(GTK_CELL_RENDERER(cell), 0.5, 0.5);
+ gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0, FALSE, -1);
+ }
+ else {
+ gtk_cell_renderer_set_alignment(GTK_CELL_RENDERER(cell), 0.5, 0.5);
+ gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0, TRUE, -1);
+ }
+
+}
+
+/*
+ * Appends json objects that have been deserialized from the response string.
+ *
+ * @returns void but modifies @data_array by appending plugins to it. It is the caller's responsibility to free @data_array
+ * and its underlying elements.
+ */
+void remmina_append_json_objects_from_response_str(JsonReader *reader, GArray* data_array)
+{
+ TRACE_CALL(__func__);
+
+ int plugin_array_index = 0;
+
+ while (json_reader_read_element(reader, plugin_array_index)) {
+ if (plugin_array_index >= MAX_PLUGINS_ALLOWED_TO_DOWNLOAD) {
+ break;
+ }
+ /* This is the current plugin we are building from the deserialized response string. */
+ RemminaServerPluginResponse *current_plugin = g_malloc(sizeof(RemminaServerPluginResponse));
+ if (current_plugin == NULL) {
+ plugin_array_index += 1;
+ continue;
+ }
+
+ json_reader_read_member(reader, "name");
+
+ current_plugin->name = g_strdup(json_reader_get_string_value(reader));
+ json_reader_end_member(reader);
+ if (g_utf8_strlen(current_plugin->name, -1) > MAX_PLUGIN_NAME_SIZE) { // -1 indicates null terminated str.
+ g_free(current_plugin);
+ plugin_array_index += 1;
+ continue; // continue attempting to deserialize the rest of the server response.
+ }
+
+ json_reader_read_member(reader, "version");
+ current_plugin->version = g_strdup(json_reader_get_string_value(reader));
+ json_reader_end_member(reader);
+
+ json_reader_read_member(reader, "filename");
+ current_plugin->file_name = g_strdup(json_reader_get_string_value(reader));
+ json_reader_end_member(reader);
+
+ json_reader_read_member(reader, "data");
+ const gchar* data = json_reader_get_string_value(reader);
+
+
+ /* Because the data is in the form \'\bdata\' and we only want data, we must remove three characters
+ and then add one more character at the end for NULL, which is (1 + total_length - 3). */
+ current_plugin->data = g_malloc(1 + g_utf8_strlen(data, -1) - 3);
+ if (current_plugin->data == NULL) {
+ g_free(current_plugin);
+ plugin_array_index += 1;
+ continue;
+ }
+
+ /* remove \'\b and \' */
+ g_strlcpy((char*)current_plugin->data, data + 2, g_utf8_strlen(data, -1) - 2);
+ json_reader_end_member(reader);
+
+ json_reader_read_member(reader, "signature");
+ if (json_reader_get_string_value(reader) != NULL)
+ {
+ current_plugin->signature = (guchar*) g_strdup(json_reader_get_string_value(reader));
+ }
+ else
+ {
+ g_free(current_plugin);
+ plugin_array_index += 1;
+ continue; // continue attempting to deserialize the rest of the server response.
+ }
+ json_reader_end_member(reader);
+
+ g_array_append_val(data_array, current_plugin);
+ plugin_array_index += 1;
+
+ json_reader_end_element(reader); // end inspection of this plugin element
+ }
+
+}
+
+GFile* remmina_create_plugin_file(const gchar* plugin_name, const gchar* plugin_version)
+{
+ gchar file_name[MAX_PLUGIN_NAME_SIZE];
+ gchar *plugin_dir;
+
+ if (g_utf8_strlen(plugin_name, -1) > MAX_PLUGIN_NAME_SIZE) {
+ return NULL;
+ }
+
+
+ if (plugin_version != NULL){
+ plugin_dir = g_build_path("/", "/tmp", NULL);
+ snprintf(file_name, MAX_PLUGIN_NAME_SIZE, "%s/%s_%s", plugin_dir, plugin_name, plugin_version);
+ }
+ else{
+ plugin_dir = g_build_path("/", g_get_user_config_dir(), "remmina", "plugins", NULL);
+ snprintf(file_name, MAX_PLUGIN_NAME_SIZE, "%s/%s", plugin_dir, plugin_name);
+ }
+ GFile* plugin_file = g_file_new_for_path(file_name);
+
+ if (plugin_dir != NULL) {
+ g_free(plugin_dir);
+ }
+
+ return plugin_file;
+}
+
+gchar* remmina_plugin_manager_get_selected_plugin_list_str(GArray *selected_plugins)
+{
+ gchar *plugin_list = NULL;
+
+ for(guint i = 0; i < selected_plugins->len; i++)
+ {
+ gchar *plugin = g_array_index(selected_plugins, gchar*, i);
+
+ if (i == 0)
+ {
+ plugin_list = g_strdup(plugin);
+ }
+ else
+ {
+ plugin_list = g_strconcat(plugin_list, ", ", plugin, NULL);
+ }
+
+ }
+ return plugin_list;
+}
+
+gboolean remmina_attempt_to_write_plugin_data_to_disk(RemminaServerPluginResponse *plugin)
+{
+ gsize decoded_len = 0;
+ guchar *decoded = g_base64_decode((gchar*) plugin->data, &decoded_len);
+
+ if (decoded == NULL) {
+ return FALSE;
+ }
+
+ gsize signature_len = 0;
+ guchar * decoded_signature = g_base64_decode((gchar*) plugin->signature, &signature_len);
+ if (decoded_signature == NULL) {
+ g_free(decoded);
+ return FALSE;
+ }
+
+ /* Create path write temporary plugin */
+ GFile* plugin_file = remmina_create_plugin_file(plugin->file_name, plugin->version);
+ if (plugin_file == NULL) {
+ g_free(decoded);
+ g_free(decoded_signature);
+ return FALSE;
+ }
+
+ /* Convert the base64 into inflated memory */
+ gsize bytes_converted = remmina_decompress_from_memory_to_file(decoded, (int) decoded_len, plugin_file);
+
+ /* Don't attempt to write file if we can't inflate anything. */
+ if (bytes_converted <= 0) {
+ g_free(decoded);
+ g_free(decoded_signature);
+ g_file_delete(plugin_file, NULL, NULL);
+ g_object_unref(plugin_file);
+ return FALSE;
+ }
+ /* Verify the signature given to us by the server*/
+ gboolean passed = remmina_verify_plugin_signature(decoded_signature, plugin_file, bytes_converted, signature_len);
+
+ if (!passed) {
+ g_free(decoded);
+ g_free(decoded_signature);
+ g_file_delete(plugin_file, NULL, NULL);
+ g_object_unref(plugin_file);
+ return FALSE;
+ }
+
+ /* Create path to store plugin */
+ GFile* plugin_file_final = remmina_create_plugin_file(plugin->file_name, NULL);
+ if (plugin_file == NULL) {
+ g_free(decoded);
+ g_free(decoded_signature);
+ g_file_delete(plugin_file, NULL, NULL);
+ g_object_unref(plugin_file);
+ return FALSE;
+ }
+
+ GError *error = NULL; // Error for testing
+ GFileOutputStream *g_output_stream = g_file_replace(plugin_file_final, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error);
+ if (error !=NULL) {
+ g_free(decoded);
+ g_free(decoded_signature);
+ g_file_delete(plugin_file, NULL, NULL);
+ g_object_unref(plugin_file);
+ g_file_delete(plugin_file_final, NULL, NULL);
+ g_object_unref(plugin_file_final);
+ return FALSE;
+ }
+
+ GFileIOStream *tmp_file_stream = (GFileIOStream*)g_file_read(plugin_file, NULL, NULL);
+ g_output_stream_splice((GOutputStream*)g_output_stream, (GInputStream*)tmp_file_stream, G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, NULL, NULL);
+ GFileInfo* info = g_file_query_info(plugin_file_final, G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL);
+ gsize bytes_written = g_file_info_get_size(info);
+
+ /* Delete temporary file */
+ g_file_delete(plugin_file, NULL, NULL);
+
+ if (bytes_written != bytes_converted) {
+ g_object_unref(info);
+ g_free(decoded);
+ g_free(decoded_signature);
+ g_file_delete(plugin_file_final, NULL, NULL);
+ g_object_unref(plugin_file_final);
+ return FALSE;
+ }
+
+ if (info != NULL) {
+ g_object_unref(info);
+ }
+ g_free(decoded);
+ g_free(decoded_signature);
+ g_object_unref(g_output_stream);
+ g_object_unref(plugin_file);
+ return TRUE;
+}
+
+
+gboolean remmina_plugin_manager_parse_plugin_list(gpointer user_data)
+{
+ int index = 0;
+ JsonReader* reader = (JsonReader*)user_data;
+ if (!json_reader_read_element(reader, 0)) {
+ return FALSE;
+ }
+ if (json_reader_read_member(reader, "LIST")){
+ while (json_reader_read_element(reader, index)) {
+
+ RemminaPlugin *available_plugin = g_malloc (sizeof(RemminaPlugin));
+
+ json_reader_read_member(reader, "name");
+
+ available_plugin->name = g_strdup(json_reader_get_string_value(reader));
+
+ json_reader_end_member(reader);
+ json_reader_read_member(reader, "description");
+
+ available_plugin->description = g_strdup(json_reader_get_string_value(reader));
+
+ json_reader_end_member(reader);
+ json_reader_read_member(reader, "version");
+
+ available_plugin->version = g_strdup(json_reader_get_string_value(reader));
+
+ json_reader_end_member(reader);
+ json_reader_read_member(reader, "type");
+
+ const char *type = json_reader_get_string_value(reader);
+ if (g_strcmp0(type, "protocol") == 0)
+ {
+ available_plugin->type=REMMINA_PLUGIN_TYPE_PROTOCOL;
+ }
+ else if(g_strcmp0(type, "entry") == 0)
+ {
+ available_plugin->type=REMMINA_PLUGIN_TYPE_ENTRY;
+ }
+ else if(g_strcmp0(type, "tool") == 0)
+ {
+ available_plugin->type=REMMINA_PLUGIN_TYPE_TOOL;
+ }
+ else if(g_strcmp0(type,"pref") == 0)
+ {
+ available_plugin->type=REMMINA_PLUGIN_TYPE_PREF;
+ }
+ else if(g_strcmp0(type, "secret") == 0)
+ {
+ available_plugin->type=REMMINA_PLUGIN_TYPE_SECRET;
+ }
+ else if(g_strcmp0(type, "language_wrapper") == 0)
+ {
+ available_plugin->type=REMMINA_PLUGIN_TYPE_LANGUAGE_WRAPPER;
+ }
+ else if(g_strcmp0(type, "file") == 0)
+ {
+ available_plugin->type=REMMINA_PLUGIN_TYPE_FILE;
+ }
+ else
+ {
+ available_plugin->type=REMMINA_PLUGIN_TYPE_PREF;
+ }
+
+ json_reader_end_member(reader);
+ json_reader_read_member(reader, "domain");
+
+ available_plugin->domain = g_strdup(json_reader_get_string_value(reader));
+
+ json_reader_end_member(reader);
+
+ if(remmina_plugin_manager_verify_duplicate_plugins(available_plugin) == FALSE)
+ {
+ g_ptr_array_add(remmina_available_plugin_table, available_plugin);
+ }
+
+ json_reader_end_element(reader);
+
+ index = index + 1;
+
+ }
+ }
+ json_reader_end_member(reader);
+ json_reader_end_element(reader);
+ g_object_unref(reader);
+ return FALSE;
+}
+
+
+
+gboolean remmina_plugin_manager_download_plugins(gpointer user_data)
+{
+ int success_count = 0;
+ guint i;
+ JsonReader* reader = (JsonReader*)user_data;
+ if (!json_reader_read_element(reader, 0)) {
+ return FALSE;
+ }
+
+ GArray* data_array = g_array_new(FALSE, FALSE, sizeof(RemminaServerPluginResponse *));
+ if (json_reader_read_member(reader, "PLUGINS")){
+ remmina_append_json_objects_from_response_str(reader, data_array);
+
+ for (i = 0; i < data_array->len; i+=1) {
+ RemminaServerPluginResponse *plugin = g_array_index(data_array, RemminaServerPluginResponse *, i);
+ if (remmina_attempt_to_write_plugin_data_to_disk(plugin)) {
+ success_count += 1;
+ }
+ }
+
+ if (remmina_plugin_window != NULL && remmina_plugin_signal_data != NULL && remmina_plugin_signal_data->downloading == TRUE){
+ if(success_count == data_array->len)
+ {
+ remmina_plugin_manager_download_result_dialog(remmina_plugin_window, "Plugin download successful! Please reboot Remmina in order to apply changes.\n");
+ }
+ else if((success_count < data_array->len) && (success_count != 0))
+ {
+ remmina_plugin_manager_download_result_dialog(remmina_plugin_window, "Plugin download partially successful! Please reboot Remmina in order to apply changes.\n");
+ }
+ else
+ {
+ remmina_plugin_manager_download_result_dialog(remmina_plugin_window, "Plugin download failed.\n");
+ }
+
+ }
+
+ }
+
+ g_array_free(data_array, TRUE); //Passing in free_segment TRUE frees the underlying elements as well as the array.
+ json_reader_end_member(reader);
+ json_reader_end_element(reader);
+ g_object_unref(reader);
+
+ return FALSE;
+}
+
+
+/*
+ * Creates request to download selected plugins from the server
+ *
+ */
+void remmina_plugin_manager_plugin_request(GArray *selected_plugins)
+{
+ TRACE_CALL(__func__);
+ gchar *formdata, *encdata, *plugin_list, *uid;
+ JsonGenerator *g;
+ JsonObject *o;
+ JsonBuilder *b;
+
+ JsonNode *n = remmina_plugin_manager_plugin_stats_get_all();
+
+ if (n == NULL || (o = json_node_get_object(n)) == NULL)
+ {
+ formdata = "{\"UID\":\"\"}";
+ }
+ else
+ {
+ b = json_builder_new();
+
+ if(b != NULL)
+ {
+ json_builder_begin_object(b);
+
+ uid = g_strdup(json_object_get_string_member(o, "UID"));
+
+ json_builder_set_member_name(b, "UID");
+ json_builder_add_string_value(b, uid);
+
+ encdata = g_strdup(json_object_get_string_member(o, "encdata"));
+
+ json_builder_set_member_name(b, "encdata");
+ json_builder_add_string_value(b, encdata);
+
+ plugin_list = remmina_plugin_manager_get_selected_plugin_list_str(selected_plugins);
+
+ json_builder_set_member_name(b, "selected_plugins");
+ json_builder_add_string_value(b, plugin_list);
+
+ json_builder_set_member_name(b, "keyversion");
+ json_builder_add_int_value(b, 1);
+
+ json_builder_end_object(b);
+ n = json_builder_get_root(b);
+ g_object_unref(b);
+
+ g = json_generator_new();
+ json_generator_set_root(g, n);
+ formdata = json_generator_to_data(g, NULL);
+
+ g_object_unref(g);
+
+ if(plugin_list != NULL)
+ {
+ g_free(plugin_list);
+ }
+
+ json_node_free(n);
+ g_free(encdata);
+ g_free(uid);
+ }
+ else
+ {
+ json_node_free(n);
+ formdata = "{\"UID\":\"\"}";
+ }
+ }
+ remmina_curl_compose_message(formdata, "POST", DOWNLOAD_URL, NULL);
+
+
+}
+
+
+gboolean remmina_plugin_manager_verify_duplicate_plugins(RemminaPlugin *available_plugin)
+{
+ RemminaPlugin *plugin;
+ gsize i;
+ for (i = 0; i < remmina_plugin_table->len; i++) {
+ plugin = (RemminaPlugin*)g_ptr_array_index(remmina_plugin_table, i);
+ if ((g_strcmp0(plugin->name, available_plugin->name) == 0) && (g_strcmp0(plugin->version, available_plugin->version) == 0))
+ {
+ return TRUE;
+ }
+ }
+ for (i = 0; i < remmina_available_plugin_table->len; i++) {
+ plugin = (RemminaPlugin*)g_ptr_array_index(remmina_available_plugin_table, i);
+ if ((g_strcmp0(plugin->name, available_plugin->name) == 0) && (g_strcmp0(plugin->version, available_plugin->version) == 0))
+ {
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+
+void* remmina_plugin_manager_get_available_plugins()
+{
+ TRACE_CALL(__func__);
+
+ gchar *formdata;
+ JsonGenerator *g;
+
+ JsonNode *n = remmina_plugin_manager_plugin_stats_get_all();
+
+ if (n == NULL)
+ {
+ formdata = "{}";
+ }
+ else
+ {
+ g = json_generator_new();
+ json_generator_set_root(g, n);
+ formdata = json_generator_to_data(g, NULL);
+ g_object_unref(g);
+ json_node_free(n);
+ }
+ remmina_curl_compose_message(formdata, "POST", LIST_URL, NULL);
+ return NULL;
+}
+
+void remmina_plugin_manager_on_response(GtkDialog * dialog, gint response_id, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ SignalData *data = remmina_plugin_signal_data;
+
+ GtkListStore *store = data->store;
+ GtkWidget *label = data->label;
+ GtkWidget *spinner = data->spinner;
+
+ GtkTreeIter iter;
+ gboolean found = TRUE;
+
+ if (response_id == ON_DOWNLOAD){
+ found = FALSE;
+ data->downloading = TRUE;
+ if (gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter)) {
+ GArray* name_array = g_array_new(FALSE, FALSE, sizeof(gchar*));
+ do {
+ gboolean value;
+ gchar *available, *name;
+ gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, 0, &value, -1);
+ gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, 1, &name, -1);
+ gtk_tree_model_get(GTK_TREE_MODEL(store), &iter, 4, &available, -1);
+
+ if(value){
+ if(g_strcmp0(available, "Installed") == 0){
+ REMMINA_DEBUG("%s is already installed", name);
+ }
+ else{
+ REMMINA_DEBUG("%s can be installed!", name);
+ found = TRUE;
+ gchar *c_name = g_strdup(name);
+
+ g_array_append_val(name_array, c_name);
+
+ gtk_widget_set_visible(spinner, TRUE);
+ if (gtk_widget_get_visible(label)) {
+ continue;
+ } else {
+ gtk_widget_show(label);
+ gtk_spinner_start(GTK_SPINNER(spinner));
+ }
+ }
+ }
+ g_free(available);
+ g_free(name);
+ } while (gtk_tree_model_iter_next(GTK_TREE_MODEL(store), &iter));
+ if (name_array->len > 0) {
+ remmina_plugin_manager_plugin_request(name_array);
+ g_array_free(name_array, TRUE); // free the array and element data
+
+ }
+ }
+ }
+ else{
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+ remmina_plugin_window = NULL;
+ }
+ if (found == FALSE){
+ remmina_plugin_manager_download_result_dialog(remmina_plugin_window, "No plugins selected for download\n");
+ }
+}
+
+void remmina_plugin_manager_show(GtkWindow *parent)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *dialog;
+ GtkWidget *scrolledwindow;
+ GtkWidget *tree, *available_tree;
+ GtkWidget *label = gtk_label_new("Downloading...");
+ GtkCellRenderer *renderer;
+ GtkTreeViewColumn *column;
+ GtkListStore *store, *available_store;
+ SignalData* data = NULL;
+
+ if (remmina_plugin_signal_data == NULL){
+ data = malloc(sizeof(SignalData));
+ }
+ else{
+ data = remmina_plugin_signal_data;
+
+ }
+
+ if (remmina_plugin_window == NULL){
+ dialog = gtk_dialog_new_with_buttons(_("Plugins"), parent, GTK_DIALOG_DESTROY_WITH_PARENT, _("_Download"), 1, _("Close"), 2, NULL);
+ GtkWidget *spinner = gtk_spinner_new();
+
+ gtk_window_set_default_size(GTK_WINDOW(dialog), 800, 600);
+
+ scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_show(scrolledwindow);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwindow), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), scrolledwindow, TRUE, TRUE, 0);
+
+ tree = gtk_tree_view_new();
+ available_tree = gtk_tree_view_new();
+ GtkWidget* tree_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add(GTK_CONTAINER(scrolledwindow), tree_box);
+
+ gtk_box_pack_end(GTK_BOX(tree_box), available_tree, TRUE, TRUE, 0);
+ GtkLabel *label_available = (GtkLabel*)gtk_label_new("Available");
+ GtkLabel *label_installed = (GtkLabel*)gtk_label_new("Installed");
+ gtk_widget_set_halign(GTK_WIDGET(label_available), GTK_ALIGN_START);
+ gtk_widget_set_halign(GTK_WIDGET(label_installed), GTK_ALIGN_START);
+ gtk_box_pack_end(GTK_BOX(tree_box), GTK_WIDGET(label_available), TRUE, TRUE, 1);
+ gtk_box_pack_end(GTK_BOX(tree_box), tree, TRUE, TRUE, 10);
+ gtk_box_pack_end(GTK_BOX(tree_box), GTK_WIDGET(label_installed), TRUE, TRUE, 1);
+
+ gtk_widget_show(tree);
+ gtk_widget_show(available_tree);
+ gtk_widget_show(tree_box);
+ gtk_widget_show(GTK_WIDGET(label_available));
+ gtk_widget_show(GTK_WIDGET(label_installed));
+
+ store = gtk_list_store_new(6, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+ available_store = gtk_list_store_new(6, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+
+ g_ptr_array_foreach(remmina_plugin_table, (GFunc)remmina_plugin_manager_show_for_each, store);
+ g_ptr_array_foreach(remmina_available_plugin_table, (GFunc)remmina_plugin_manager_show_for_each_available, available_store);
+ gtk_tree_view_set_model(GTK_TREE_VIEW(tree), GTK_TREE_MODEL(store));
+ gtk_tree_view_set_model(GTK_TREE_VIEW(available_tree), GTK_TREE_MODEL(available_store));
+
+ renderer = gtk_cell_renderer_toggle_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Install"), renderer, "active", 0, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, 0);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(available_tree), column);
+ g_signal_connect (renderer, "toggled", G_CALLBACK (remmina_plugin_manager_toggle_checkbox), available_store);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Name"), renderer, "text", 1, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, 0);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Name"), renderer, "text", 1, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, 0);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(available_tree), column);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Type"), renderer, "text", 2, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, 1);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Type"), renderer, "text", 2, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, 1);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(available_tree), column);
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Description"), renderer, "text", 3, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, 2);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Description"), renderer, "text", 3, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, 2);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(available_tree), column);
+
+
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Version"), renderer, "text", 5, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, 4);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column);
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes(_("Version"), renderer, "text", 5, NULL);
+ gtk_tree_view_column_set_resizable(column, TRUE);
+ gtk_tree_view_column_set_sort_column_id(column, 4);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(available_tree), column);
+
+ data->store = available_store;
+ data->label = label;
+ data->spinner = spinner;
+ data->downloading = FALSE;
+
+ g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(remmina_plugin_manager_on_response), data);
+
+ gtk_box_pack_end(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), label, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))), spinner, FALSE, FALSE, 0);
+
+ }
+ else {
+ return;
+ }
+ remmina_plugin_window = (GtkDialog*)dialog;
+ remmina_plugin_signal_data = data;
+
+ gtk_widget_show(GTK_WIDGET(dialog));
+}
+
+RemminaFilePlugin* remmina_plugin_manager_get_import_file_handler(const gchar *file)
+{
+ TRACE_CALL(__func__);
+ RemminaFilePlugin *plugin;
+ gsize i;
+
+ for (i = 0; i < remmina_plugin_table->len; i++) {
+ plugin = (RemminaFilePlugin*)g_ptr_array_index(remmina_plugin_table, i);
+
+ if (plugin->type != REMMINA_PLUGIN_TYPE_FILE)
+ continue;
+
+ if (plugin->import_test_func(plugin, file)) {
+ return plugin;
+ }
+ }
+ return NULL;
+}
+
+RemminaFilePlugin* remmina_plugin_manager_get_export_file_handler(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ RemminaFilePlugin *plugin;
+ guint i;
+
+ for (i = 0; i < remmina_plugin_table->len; i++) {
+ plugin = (RemminaFilePlugin*)g_ptr_array_index(remmina_plugin_table, i);
+ if (plugin->type != REMMINA_PLUGIN_TYPE_FILE)
+ continue;
+ if (plugin->export_test_func(plugin, remminafile)) {
+ return plugin;
+ }
+ }
+ return NULL;
+}
+
+RemminaSecretPlugin* remmina_plugin_manager_get_secret_plugin(void)
+{
+ TRACE_CALL(__func__);
+ return remmina_secret_plugin;
+}
+
+gboolean remmina_plugin_manager_query_feature_by_type(RemminaPluginType ptype, const gchar* name, RemminaProtocolFeatureType ftype)
+{
+ TRACE_CALL(__func__);
+ const RemminaProtocolFeature *feature;
+ RemminaProtocolPlugin* plugin;
+
+ plugin = (RemminaProtocolPlugin*)remmina_plugin_manager_get_plugin(ptype, name);
+
+ if (plugin == NULL) {
+ return FALSE;
+ }
+
+ for (feature = plugin->features; feature && feature->type; feature++) {
+ if (feature->type == ftype)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+gboolean remmina_plugin_manager_is_encrypted_setting(RemminaProtocolPlugin *pp, const char *setting)
+{
+ TRACE_CALL(__func__);
+ GHashTable *pht;
+
+ if (encrypted_settings_cache == NULL)
+ return FALSE;
+
+ if (!(pht = g_hash_table_lookup(encrypted_settings_cache, pp->name)))
+ return FALSE;
+
+ if (!g_hash_table_lookup(pht, setting))
+ return FALSE;
+
+ return TRUE;
+}
diff --git a/src/remmina_plugin_manager.h b/src/remmina_plugin_manager.h
new file mode 100644
index 0000000..5400f2a
--- /dev/null
+++ b/src/remmina_plugin_manager.h
@@ -0,0 +1,103 @@
+/*
+ * 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
+ * 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.
+ *
+ */
+
+#pragma once
+
+#include "remmina/plugin.h"
+#include "json-glib/json-glib.h"
+
+
+#define DOWNLOAD_URL "https://plugins.remmina.org/plugins/plugin_download"
+#define LIST_URL "https://plugins.remmina.org/plugins/get_list"
+
+#define ON_DOWNLOAD 1
+#define ON_CLOSE 2
+#define FAIL 0
+#define PARTIAL_SUCCESS 1
+#define SUCCESS 2
+#define MAX_PLUGIN_NAME_SIZE 100
+#define MAX_PLUGINS_ALLOWED_TO_DOWNLOAD 20
+
+G_BEGIN_DECLS
+
+typedef gboolean (*RemminaPluginFunc)(gchar *name, RemminaPlugin *plugin, gpointer data);
+
+void remmina_plugin_manager_init(void);
+JsonNode *remmina_plugin_manager_get_installed_plugins(void);
+RemminaPlugin *remmina_plugin_manager_get_plugin(RemminaPluginType type, const gchar *name);
+gboolean remmina_plugin_manager_query_feature_by_type(RemminaPluginType ptype, const gchar *name, RemminaProtocolFeatureType ftype);
+void remmina_plugin_manager_for_each_plugin(RemminaPluginType type, RemminaPluginFunc func, gpointer data);
+void remmina_plugin_manager_show(GtkWindow *parent);
+void remmina_plugin_manager_for_each_plugin_stdout(RemminaPluginType type, RemminaPluginFunc func, gpointer data);
+void remmina_plugin_manager_show_stdout();
+void* remmina_plugin_manager_get_available_plugins();
+gboolean remmina_plugin_manager_parse_plugin_list(gpointer user_data);
+gboolean remmina_plugin_manager_download_plugins(gpointer user_data);
+RemminaFilePlugin *remmina_plugin_manager_get_import_file_handler(const gchar *file);
+RemminaFilePlugin *remmina_plugin_manager_get_export_file_handler(RemminaFile *remminafile);
+RemminaSecretPlugin *remmina_plugin_manager_get_secret_plugin(void);
+const gchar *remmina_plugin_manager_get_canonical_setting_name(const RemminaProtocolSetting *setting);
+gboolean remmina_plugin_manager_is_encrypted_setting(RemminaProtocolPlugin *pp, const char *setting);
+gboolean remmina_gtksocket_available();
+gboolean remmina_plugin_manager_verify_duplicate_plugins(RemminaPlugin *test_plugin);
+void remmina_plugin_manager_toggle_checkbox(GtkCellRendererToggle *cell, gchar *path, GtkListStore *model);
+void remmina_plugin_manager_on_response(GtkDialog *dialog, gint response_id, gpointer user_data);
+void remmina_append_json_objects_from_response_str(JsonReader* response_str, GArray* data_array);
+guint remmina_plugin_manager_deserialize_plugin_response(GArray *name_array);
+gboolean remmina_attempt_to_write_plugin_data_to_disk(RemminaServerPluginResponse *plugin);
+GFile* remmina_create_plugin_file(const gchar* plugin_name, const gchar* plugin_version);
+JsonNode *remmina_plugin_manager_plugin_stats_get_all(void);
+extern RemminaPluginService remmina_plugin_manager_service;
+
+typedef gboolean (*RemminaPluginLoaderFunc)(RemminaPluginService*, const gchar* name);
+
+typedef struct {
+ char* filetype;
+ RemminaPluginLoaderFunc func;
+} RemminaPluginLoader;
+
+typedef struct {
+ GtkWidget *label;
+ GtkListStore *store;
+ GtkWidget *spinner;
+ gboolean downloading;
+} SignalData;
+
+gboolean remmina_plugin_manager_loader_supported(const char *filetype);
+void remmina_plugin_manager_add_loader(char *filetype, RemminaPluginLoaderFunc func);
+
+G_END_DECLS
diff --git a/src/remmina_plugin_native.c b/src/remmina_plugin_native.c
new file mode 100644
index 0000000..a5b9682
--- /dev/null
+++ b/src/remmina_plugin_native.c
@@ -0,0 +1,95 @@
+/*
+ * 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/gi18n.h>
+#include <gio/gio.h>
+#include <string.h>
+
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#elif defined(GDK_WINDOWING_WAYLAND)
+#include <gdk/gdkwayland.h>
+#endif
+
+#include "remmina_public.h"
+#include "remmina_file_manager.h"
+#include "remmina_pref.h"
+#include "remmina_protocol_widget.h"
+#include "remmina_log.h"
+#include "remmina_widget_pool.h"
+#include "rcw.h"
+#include "remmina_public.h"
+#include "remmina_plugin_native.h"
+#include "remmina_masterthread_exec.h"
+#include "remmina/remmina_trace_calls.h"
+
+gboolean remmina_plugin_native_load(RemminaPluginService* service, const char* name)
+{
+ TRACE_CALL(__func__);
+ GModule* module;
+ RemminaPluginEntryFunc entry;
+
+ //Python plugins cannot be lazy loaded, so hande their loading seperately
+ if (strstr(name, "remmina-plugin-python_wrapper") != NULL ){
+ module = g_module_open(name, 0);
+ }
+ else{
+ module = g_module_open(name, G_MODULE_BIND_LAZY | G_MODULE_BIND_LOCAL);
+ }
+
+ if (!module)
+ {
+ g_print("Failed to load plugin: %s.\n", name);
+ g_print("Error: %s\n", g_module_error());
+ return FALSE;
+ }
+
+ if (!g_module_symbol(module, "remmina_plugin_entry", (gpointer*)&entry))
+ {
+ g_print("Failed to locate plugin entry: %s.\n", name);
+ return FALSE;
+ }
+
+ if (!entry(service))
+ {
+ g_print("Plugin entry returned false: %s.\n", name);
+ return FALSE;
+ }
+
+ return TRUE;
+ /* We don’t close the module because we will need it throughout the process lifetime */
+}
diff --git a/src/remmina_plugin_native.h b/src/remmina_plugin_native.h
new file mode 100644
index 0000000..b36a763
--- /dev/null
+++ b/src/remmina_plugin_native.h
@@ -0,0 +1,45 @@
+/*
+ * 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 "remmina_plugin_manager.h"
+
+G_BEGIN_DECLS
+
+typedef gboolean (* RemminaPluginMain)(gchar* name);
+
+gboolean remmina_plugin_native_load(RemminaPluginService* service, const char* name);
+
+G_END_DECLS
diff --git a/src/remmina_pref.c b/src/remmina_pref.c
new file mode 100644
index 0000000..a839e66
--- /dev/null
+++ b/src/remmina_pref.c
@@ -0,0 +1,1252 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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
+ * 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.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/time.h>
+#include <sys/utsname.h>
+
+#include <glib/gstdio.h>
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtk.h>
+
+#include "remmina_sodium.h"
+
+#include "remmina_public.h"
+#include "remmina_string_array.h"
+#include "remmina_pref.h"
+#include "remmina_log.h"
+#include "remmina/remmina_trace_calls.h"
+
+const gchar *default_resolutions = "640x480,800x600,1024x768,1152x864,1280x960,1400x1050,1920x1080";
+const gchar *default_keystrokes = "Send hello world§hello world\\n";
+
+extern gboolean info_disable_stats;
+extern gboolean info_disable_news;
+extern gboolean info_disable_tip;
+
+gchar *remmina_keymap_file;
+static GHashTable *remmina_keymap_table = NULL;
+
+/* We could customize this further if there are more requirements */
+static const gchar *default_keymap_data = "# Please check gdk/gdkkeysyms.h for a full list of all key names or hex key values\n"
+ "\n"
+ "[Map Meta Keys]\n"
+ "Super_L = Meta_L\n"
+ "Super_R = Meta_R\n"
+ "Meta_L = Super_L\n"
+ "Meta_R = Super_R\n";
+
+static void remmina_pref_gen_secret(void)
+{
+ TRACE_CALL(__func__);
+ guchar s[32];
+ gint i;
+ GKeyFile *gkeyfile;
+ g_autofree gchar *content = NULL;
+ gsize length;
+
+ for (i = 0; i < 32; i++)
+ s[i] = (guchar)(randombytes_uniform(257));
+ remmina_pref.secret = g_base64_encode(s, 32);
+
+ gkeyfile = g_key_file_new();
+ g_key_file_load_from_file(gkeyfile, remmina_pref_file, G_KEY_FILE_NONE, NULL);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "secret", remmina_pref.secret);
+ content = g_key_file_to_data(gkeyfile, &length, NULL);
+ g_file_set_contents(remmina_pref_file, content, length, NULL);
+
+ g_key_file_free(gkeyfile);
+}
+
+static guint remmina_pref_get_keyval_from_str(const gchar *str)
+{
+ TRACE_CALL(__func__);
+ guint k;
+
+ if (!str)
+ return 0;
+
+ k = gdk_keyval_from_name(str);
+ if (!k)
+ if (sscanf(str, "%x", &k) < 1)
+ k = 0;
+ return k;
+}
+
+static void remmina_pref_init_keymap(void)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+ gchar **groups;
+ gchar **gptr;
+ gchar **keys;
+ gchar **kptr;
+ gsize nkeys;
+ g_autofree gchar *value = NULL;
+ guint *table;
+ guint *tableptr;
+ guint k1, k2;
+
+ if (remmina_keymap_table)
+ g_hash_table_destroy(remmina_keymap_table);
+ remmina_keymap_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+
+ gkeyfile = g_key_file_new();
+ if (!g_key_file_load_from_file(gkeyfile, remmina_keymap_file, G_KEY_FILE_NONE, NULL)) {
+ if (!g_key_file_load_from_data(gkeyfile, default_keymap_data, strlen(default_keymap_data), G_KEY_FILE_NONE,
+ NULL)) {
+ g_print("Failed to initialize keymap table\n");
+ g_key_file_free(gkeyfile);
+ return;
+ }
+ }
+
+ groups = g_key_file_get_groups(gkeyfile, NULL);
+ gptr = groups;
+ while (*gptr) {
+ keys = g_key_file_get_keys(gkeyfile, *gptr, &nkeys, NULL);
+ table = g_new0(guint, nkeys * 2 + 1);
+ g_hash_table_insert(remmina_keymap_table, g_strdup(*gptr), table);
+
+ kptr = keys;
+ tableptr = table;
+ while (*kptr) {
+ k1 = remmina_pref_get_keyval_from_str(*kptr);
+ if (k1) {
+ value = g_key_file_get_string(gkeyfile, *gptr, *kptr, NULL);
+ k2 = remmina_pref_get_keyval_from_str(value);
+ *tableptr++ = k1;
+ *tableptr++ = k2;
+ }
+ kptr++;
+ }
+ g_strfreev(keys);
+ gptr++;
+ }
+ g_strfreev(groups);
+ g_key_file_free(gkeyfile);
+}
+
+/** @todo remmina_pref_file_do_copy and remmina_file_manager_do_copy to remmina_files_copy */
+static gboolean remmina_pref_file_do_copy(const char *src_path, const char *dst_path)
+{
+ GFile *src = g_file_new_for_path(src_path), *dst = g_file_new_for_path(dst_path);
+ /* We don’t overwrite the target if it exists, because overwrite is not set */
+ const gboolean ok = g_file_copy(src, dst, G_FILE_COPY_NONE, NULL, NULL, NULL, NULL);
+
+ g_object_unref(dst);
+ g_object_unref(src);
+
+ return ok;
+}
+
+void remmina_pref_file_load_colors(GKeyFile *gkeyfile, RemminaColorPref *color_pref)
+{
+ const struct {
+ const char * name;
+ char ** setting;
+ char * fallback;
+ } colors[] = {
+ { "background", &color_pref->background, "#d5ccba" },
+ { "cursor", &color_pref->cursor, "#45373c" },
+ { "cursor_foreground", &color_pref->cursor_foreground, "#d5ccba" },
+ { "highlight", &color_pref->highlight, "#45373c" },
+ { "highlight_foreground", &color_pref->highlight_foreground, "#d5ccba" },
+ { "colorBD", &color_pref->colorBD, "#45373c" },
+ { "foreground", &color_pref->foreground, "#45373c" },
+ { "color0", &color_pref->color0, "#20111b" },
+ { "color1", &color_pref->color1, "#be100e" },
+ { "color2", &color_pref->color2, "#858162" },
+ { "color3", &color_pref->color3, "#eaa549" },
+ { "color4", &color_pref->color4, "#426a79" },
+ { "color5", &color_pref->color5, "#97522c" },
+ { "color6", &color_pref->color6, "#989a9c" },
+ { "color7", &color_pref->color7, "#968c83" },
+ { "color8", &color_pref->color8, "#5e5252" },
+ { "color9", &color_pref->color9, "#be100e" },
+ { "color10", &color_pref->color10, "#858162" },
+ { "color11", &color_pref->color11, "#eaa549" },
+ { "color12", &color_pref->color12, "#426a79" },
+ { "color13", &color_pref->color13, "#97522c" },
+ { "color14", &color_pref->color14, "#989a9c" },
+ { "color15", &color_pref->color15, "#d5ccba" },
+ };
+
+ int i;
+
+ for (i = 0; i < (sizeof(colors) / sizeof(colors[0])); i++) {
+ if (g_key_file_has_key(gkeyfile, "ssh_colors", colors[i].name, NULL))
+ *colors[i].setting = g_key_file_get_string(gkeyfile, "ssh_colors", colors[i].name,
+ NULL);
+ else
+ *colors[i].setting = colors[i].fallback;
+ }
+}
+
+extern gboolean disablenews;
+extern gboolean disablestats;
+
+void remmina_pref_init(void)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+ gchar *remmina_dir, *remmina_pref_path, *user_config_path;
+ const gchar *filename = "remmina.pref";
+ const gchar *colors_filename = "remmina.colors";
+ g_autofree gchar *remmina_colors_file = NULL;
+ GDir *dir;
+ const gchar *legacy = ".remmina";
+ int i;
+
+ remmina_dir = g_build_path("/", g_get_user_config_dir(), "remmina", NULL);
+ /* Create the XDG_CONFIG_HOME directory */
+ g_mkdir_with_parents(remmina_dir, 0750);
+
+ g_free(remmina_dir), remmina_dir = NULL;
+ /* Legacy ~/.remmina we copy the existing remmina.pref file inside
+ * XDG_CONFIG_HOME */
+ remmina_dir = g_build_path("/", g_get_home_dir(), legacy, NULL);
+ if (g_file_test(remmina_dir, G_FILE_TEST_IS_DIR)) {
+ dir = g_dir_open(remmina_dir, 0, NULL);
+ remmina_pref_path = g_build_path("/", remmina_dir, filename, NULL);
+ user_config_path = g_build_path("/", g_get_user_config_dir(),
+ "remmina", filename, NULL);
+ remmina_pref_file_do_copy(remmina_pref_path, user_config_path);
+ g_dir_close(dir);
+ g_free(remmina_pref_path);
+ g_free(user_config_path);
+ }
+
+ /* /usr/local/etc/remmina */
+ const gchar *const *dirs = g_get_system_config_dirs();
+
+ g_free(remmina_dir), remmina_dir = NULL;
+ for (i = 0; dirs[i] != NULL; ++i) {
+ remmina_dir = g_build_path("/", dirs[i], "remmina", NULL);
+ if (g_file_test(remmina_dir, G_FILE_TEST_IS_DIR)) {
+ dir = g_dir_open(remmina_dir, 0, NULL);
+ while ((filename = g_dir_read_name(dir)) != NULL) {
+ remmina_pref_path = g_build_path("/", remmina_dir, filename, NULL);
+ user_config_path = g_build_path("/", g_get_user_config_dir(),
+ "remmina", filename, NULL);
+ remmina_pref_file_do_copy(remmina_pref_path, user_config_path);
+ g_free(remmina_pref_path);
+ g_free(user_config_path);
+ }
+ g_free(remmina_dir), remmina_dir = NULL;
+ g_dir_close(dir);
+ }
+ }
+
+ /* The last case we use the home ~/.config/remmina */
+ if (remmina_dir != NULL)
+ g_free(remmina_dir), remmina_dir = NULL;
+ remmina_dir = g_build_path("/", g_get_user_config_dir(),
+ "remmina", NULL);
+
+ remmina_pref_file = g_strdup_printf("%s/remmina.pref", remmina_dir);
+ /* remmina.colors */
+ remmina_colors_file = g_strdup_printf("%s/%s", remmina_dir, colors_filename);
+
+ remmina_keymap_file = g_strdup_printf("%s/remmina.keymap", remmina_dir);
+
+ g_free(remmina_dir);
+
+ gkeyfile = g_key_file_new();
+
+ g_key_file_load_from_file(gkeyfile, remmina_pref_file, G_KEY_FILE_NONE, NULL);
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "save_view_mode", NULL))
+ remmina_pref.save_view_mode = g_key_file_get_boolean(gkeyfile, "remmina_pref", "save_view_mode", NULL);
+ else
+ remmina_pref.save_view_mode = TRUE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "confirm_close", NULL))
+ remmina_pref.confirm_close = g_key_file_get_boolean(gkeyfile, "remmina_pref", "confirm_close", NULL);
+ else
+ remmina_pref.confirm_close = TRUE;
+
+ if (extrahardening)
+ remmina_pref.confirm_close = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "use_master_password", NULL)) {
+ remmina_pref.use_primary_password = g_key_file_get_boolean(gkeyfile, "remmina_pref", "use_master_password", NULL);
+ } else if (g_key_file_has_key(gkeyfile, "remmina_pref", "use_primary_password", NULL))
+ remmina_pref.use_primary_password = g_key_file_get_boolean(gkeyfile, "remmina_pref", "use_primary_password", NULL);
+ else
+ remmina_pref.use_primary_password = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "unlock_timeout", NULL))
+ remmina_pref.unlock_timeout = g_key_file_get_integer(gkeyfile, "remmina_pref", "unlock_timeout", NULL);
+ else
+ remmina_pref.unlock_timeout = 300;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "unlock_password", NULL))
+ remmina_pref.unlock_password = g_key_file_get_string(gkeyfile, "remmina_pref", "unlock_password", NULL);
+ else
+ remmina_pref.unlock_password = g_strdup("");
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "lock_connect", NULL))
+ remmina_pref.lock_connect = g_key_file_get_boolean(gkeyfile, "remmina_pref", "lock_connect", NULL);
+ else
+ remmina_pref.lock_connect = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "lock_edit", NULL))
+ remmina_pref.lock_edit = g_key_file_get_boolean(gkeyfile, "remmina_pref", "lock_edit", NULL);
+ else
+ remmina_pref.lock_edit = FALSE;
+
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "lock_view_passwords", NULL))
+ remmina_pref.lock_view_passwords = g_key_file_get_boolean(gkeyfile, "remmina_pref", "lock_view_passwords", NULL);
+ else
+ remmina_pref.lock_view_passwords = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "enc_mode", NULL))
+ remmina_pref.enc_mode = g_key_file_get_integer(gkeyfile, "remmina_pref", "enc_mode", NULL);
+ else
+ remmina_pref.enc_mode = 1;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "audit", NULL))
+ remmina_pref.audit = g_key_file_get_boolean(gkeyfile, "remmina_pref", "audit", NULL);
+ else
+ remmina_pref.audit = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "trust_all", NULL))
+ remmina_pref.trust_all = g_key_file_get_boolean(gkeyfile, "remmina_pref", "trust_all", NULL);
+ else
+ remmina_pref.trust_all = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "fullscreen_on_auto", NULL))
+ remmina_pref.fullscreen_on_auto = g_key_file_get_boolean(gkeyfile, "remmina_pref", "fullscreen_on_auto", NULL);
+ else
+ remmina_pref.fullscreen_on_auto = TRUE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "floating_toolbar_placement", NULL))
+ remmina_pref.floating_toolbar_placement = g_key_file_get_integer(gkeyfile, "remmina_pref", "floating_toolbar_placement", NULL);
+ else
+ remmina_pref.floating_toolbar_placement = FLOATING_TOOLBAR_PLACEMENT_TOP;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "prevent_snap_welcome_message", NULL))
+ remmina_pref.prevent_snap_welcome_message = g_key_file_get_boolean(gkeyfile, "remmina_pref", "prevent_snap_welcome_message", NULL);
+ else
+ remmina_pref.prevent_snap_welcome_message = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "last_quickconnect_protocol", NULL))
+ remmina_pref.last_quickconnect_protocol = g_key_file_get_string(gkeyfile, "remmina_pref", "last_quickconnect_protocol", NULL);
+ else
+ remmina_pref.last_quickconnect_protocol = g_strdup("");
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "toolbar_placement", NULL))
+ remmina_pref.toolbar_placement = g_key_file_get_integer(gkeyfile, "remmina_pref", "toolbar_placement", NULL);
+ else
+ remmina_pref.toolbar_placement = TOOLBAR_PLACEMENT_LEFT;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "always_show_tab", NULL))
+ remmina_pref.always_show_tab = g_key_file_get_boolean(gkeyfile, "remmina_pref", "always_show_tab", NULL);
+ else
+ remmina_pref.always_show_tab = TRUE;
+
+ if (extrahardening)
+ remmina_pref.always_show_tab = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "always_show_notes", NULL))
+ remmina_pref.always_show_notes = g_key_file_get_boolean(gkeyfile, "remmina_pref", "always_show_notes", NULL);
+ else
+ remmina_pref.always_show_notes = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "hide_connection_toolbar", NULL))
+ remmina_pref.hide_connection_toolbar = g_key_file_get_boolean(gkeyfile, "remmina_pref",
+ "hide_connection_toolbar", NULL);
+ else
+ remmina_pref.hide_connection_toolbar = FALSE;
+
+ if (disabletoolbar)
+ remmina_pref.hide_connection_toolbar = TRUE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "hide_searchbar", NULL))
+ remmina_pref.hide_searchbar = g_key_file_get_boolean(gkeyfile, "remmina_pref",
+ "hide_searchbar", NULL);
+ else
+ remmina_pref.hide_searchbar = FALSE;
+
+ if (extrahardening)
+ remmina_pref.hide_searchbar = TRUE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "default_action", NULL))
+ remmina_pref.default_action = g_key_file_get_integer(gkeyfile, "remmina_pref", "default_action", NULL);
+ else
+ remmina_pref.default_action = REMMINA_ACTION_CONNECT;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "scale_quality", NULL))
+ remmina_pref.scale_quality = g_key_file_get_integer(gkeyfile, "remmina_pref", "scale_quality", NULL);
+ else
+ remmina_pref.scale_quality = GDK_INTERP_HYPER;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "hide_toolbar", NULL))
+ remmina_pref.hide_toolbar = g_key_file_get_boolean(gkeyfile, "remmina_pref", "hide_toolbar", NULL);
+ else
+ remmina_pref.hide_toolbar = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "small_toolbutton", NULL))
+ remmina_pref.small_toolbutton = g_key_file_get_boolean(gkeyfile, "remmina_pref", "small_toolbutton", NULL);
+ else
+ remmina_pref.small_toolbutton = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "view_file_mode", NULL))
+ remmina_pref.view_file_mode = g_key_file_get_integer(gkeyfile, "remmina_pref", "view_file_mode", NULL);
+ else
+ remmina_pref.view_file_mode = REMMINA_VIEW_FILE_LIST;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "resolutions", NULL))
+ remmina_pref.resolutions = g_key_file_get_string(gkeyfile, "remmina_pref", "resolutions", NULL);
+ else
+ remmina_pref.resolutions = g_strdup(default_resolutions);
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "keystrokes", NULL))
+ remmina_pref.keystrokes = g_key_file_get_string(gkeyfile, "remmina_pref", "keystrokes", NULL);
+ else
+ remmina_pref.keystrokes = g_strdup(default_keystrokes);
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "main_width", NULL))
+ remmina_pref.main_width = MAX(600, g_key_file_get_integer(gkeyfile, "remmina_pref", "main_width", NULL));
+ else
+ remmina_pref.main_width = 600;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "main_height", NULL))
+ remmina_pref.main_height = MAX(400, g_key_file_get_integer(gkeyfile, "remmina_pref", "main_height", NULL));
+ else
+ remmina_pref.main_height = 400;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "main_maximize", NULL))
+ remmina_pref.main_maximize = g_key_file_get_boolean(gkeyfile, "remmina_pref", "main_maximize", NULL);
+ else
+ remmina_pref.main_maximize = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "main_sort_column_id", NULL))
+ remmina_pref.main_sort_column_id = g_key_file_get_integer(gkeyfile, "remmina_pref", "main_sort_column_id",
+ NULL);
+ else
+ remmina_pref.main_sort_column_id = 1;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "main_sort_order", NULL))
+ remmina_pref.main_sort_order = g_key_file_get_integer(gkeyfile, "remmina_pref", "main_sort_order", NULL);
+ else
+ remmina_pref.main_sort_order = 0;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "expanded_group", NULL))
+ remmina_pref.expanded_group = g_key_file_get_string(gkeyfile, "remmina_pref", "expanded_group", NULL);
+ else
+ remmina_pref.expanded_group = g_strdup("");
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "toolbar_pin_down", NULL))
+ remmina_pref.toolbar_pin_down = g_key_file_get_boolean(gkeyfile, "remmina_pref", "toolbar_pin_down", NULL);
+ else
+ remmina_pref.toolbar_pin_down = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "ssh_loglevel", NULL))
+ remmina_pref.ssh_loglevel = g_key_file_get_integer(gkeyfile, "remmina_pref", "ssh_loglevel", NULL);
+ else
+ remmina_pref.ssh_loglevel = DEFAULT_SSH_LOGLEVEL;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "deny_screenshot_clipboard", NULL))
+ remmina_pref.deny_screenshot_clipboard = g_key_file_get_boolean(gkeyfile, "remmina_pref", "deny_screenshot_clipboard", NULL);
+ else
+ remmina_pref.deny_screenshot_clipboard = TRUE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "datadir_path", NULL))
+ remmina_pref.datadir_path = g_key_file_get_string(gkeyfile, "remmina_pref", "datadir_path", NULL);
+ else
+ remmina_pref.datadir_path = g_strdup("");
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "remmina_file_name", NULL))
+ remmina_pref.remmina_file_name = g_key_file_get_string(gkeyfile, "remmina_pref", "remmina_file_name", NULL);
+ else
+ remmina_pref.remmina_file_name = g_strdup("%G_%P_%N_%h");
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "screenshot_path", NULL)) {
+ remmina_pref.screenshot_path = g_key_file_get_string(gkeyfile, "remmina_pref", "screenshot_path", NULL);
+ } else {
+ remmina_pref.screenshot_path = g_get_user_special_dir(G_USER_DIRECTORY_PICTURES);
+ if (remmina_pref.screenshot_path == NULL)
+ remmina_pref.screenshot_path = g_get_home_dir();
+ }
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "screenshot_name", NULL))
+ remmina_pref.screenshot_name = g_key_file_get_string(gkeyfile, "remmina_pref", "screenshot_name", NULL);
+ else
+ remmina_pref.screenshot_name = g_strdup("remmina_%p_%h_%Y%m%d-%H%M%S");
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "ssh_parseconfig", NULL))
+ remmina_pref.ssh_parseconfig = g_key_file_get_boolean(gkeyfile, "remmina_pref", "ssh_parseconfig", NULL);
+ else
+ remmina_pref.ssh_parseconfig = DEFAULT_SSH_PARSECONFIG;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "sshtunnel_port", NULL))
+ remmina_pref.sshtunnel_port = g_key_file_get_integer(gkeyfile, "remmina_pref", "sshtunnel_port", NULL);
+ else
+ remmina_pref.sshtunnel_port = DEFAULT_SSHTUNNEL_PORT;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "ssh_tcp_keepidle", NULL))
+ remmina_pref.ssh_tcp_keepidle = g_key_file_get_integer(gkeyfile, "remmina_pref", "ssh_tcp_keepidle", NULL);
+ else
+ remmina_pref.ssh_tcp_keepidle = SSH_SOCKET_TCP_KEEPIDLE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "ssh_tcp_keepintvl", NULL))
+ remmina_pref.ssh_tcp_keepintvl = g_key_file_get_integer(gkeyfile, "remmina_pref", "ssh_tcp_keepintvl", NULL);
+ else
+ remmina_pref.ssh_tcp_keepintvl = SSH_SOCKET_TCP_KEEPINTVL;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "ssh_tcp_keepcnt", NULL))
+ remmina_pref.ssh_tcp_keepcnt = g_key_file_get_integer(gkeyfile, "remmina_pref", "ssh_tcp_keepcnt", NULL);
+ else
+ remmina_pref.ssh_tcp_keepcnt = SSH_SOCKET_TCP_KEEPCNT;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "ssh_tcp_usrtimeout", NULL))
+ remmina_pref.ssh_tcp_usrtimeout = g_key_file_get_integer(gkeyfile, "remmina_pref", "ssh_tcp_usrtimeout", NULL);
+ else
+ remmina_pref.ssh_tcp_usrtimeout = SSH_SOCKET_TCP_USER_TIMEOUT;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "applet_new_ontop", NULL))
+ remmina_pref.applet_new_ontop = g_key_file_get_boolean(gkeyfile, "remmina_pref", "applet_new_ontop", NULL);
+ else
+ remmina_pref.applet_new_ontop = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "applet_hide_count", NULL))
+ remmina_pref.applet_hide_count = g_key_file_get_boolean(gkeyfile, "remmina_pref", "applet_hide_count", NULL);
+ else
+ remmina_pref.applet_hide_count = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "applet_enable_avahi", NULL))
+ remmina_pref.applet_enable_avahi = g_key_file_get_boolean(gkeyfile, "remmina_pref", "applet_enable_avahi",
+ NULL);
+ else
+ remmina_pref.applet_enable_avahi = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "disable_tray_icon", NULL))
+ remmina_pref.disable_tray_icon = g_key_file_get_boolean(gkeyfile, "remmina_pref", "disable_tray_icon", NULL);
+ else
+ remmina_pref.disable_tray_icon = FALSE;
+
+ if (disabletrayicon)
+ remmina_pref.disable_tray_icon = TRUE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "dark_theme", NULL))
+ remmina_pref.dark_theme = g_key_file_get_boolean(gkeyfile, "remmina_pref", "dark_theme", NULL);
+ else
+ remmina_pref.dark_theme = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "list_refresh_workaround", NULL))
+ remmina_pref.list_refresh_workaround = g_key_file_get_boolean(gkeyfile, "remmina_pref", "list_refresh_workaround", NULL);
+ else
+ remmina_pref.list_refresh_workaround = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "grab_color_switch", NULL))
+ remmina_pref.grab_color_switch = g_key_file_get_boolean(gkeyfile, "remmina_pref", "grab_color_switch", NULL);
+ else
+ remmina_pref.grab_color_switch = FALSE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "grab_color", NULL))
+ remmina_pref.grab_color = g_key_file_get_string(gkeyfile, "remmina_pref", "grab_color", NULL);
+ else
+ remmina_pref.grab_color = "#00ff00";
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "recent_maximum", NULL))
+ remmina_pref.recent_maximum = g_key_file_get_integer(gkeyfile, "remmina_pref", "recent_maximum", NULL);
+ else
+ remmina_pref.recent_maximum = 10;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "default_mode", NULL))
+ remmina_pref.default_mode = g_key_file_get_integer(gkeyfile, "remmina_pref", "default_mode", NULL);
+ else
+ remmina_pref.default_mode = 0;
+
+ if (fullscreen)
+ remmina_pref.default_mode = VIEWPORT_FULLSCREEN_MODE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "tab_mode", NULL))
+ remmina_pref.tab_mode = g_key_file_get_integer(gkeyfile, "remmina_pref", "tab_mode", NULL);
+ else
+ remmina_pref.tab_mode = 0;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "fullscreen_toolbar_visibility", NULL))
+ remmina_pref.fullscreen_toolbar_visibility = g_key_file_get_integer(gkeyfile, "remmina_pref", "fullscreen_toolbar_visibility", NULL);
+ else
+ remmina_pref.fullscreen_toolbar_visibility = FLOATING_TOOLBAR_VISIBILITY_PEEKING;
+
+ if (disabletoolbar)
+ remmina_pref.fullscreen_toolbar_visibility = FLOATING_TOOLBAR_VISIBILITY_DISABLE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "auto_scroll_step", NULL))
+ remmina_pref.auto_scroll_step = g_key_file_get_integer(gkeyfile, "remmina_pref", "auto_scroll_step", NULL);
+ else
+ remmina_pref.auto_scroll_step = 10;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "hostkey", NULL))
+ remmina_pref.hostkey = g_key_file_get_integer(gkeyfile, "remmina_pref", "hostkey", NULL);
+ else
+ remmina_pref.hostkey = GDK_KEY_Control_R;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_fullscreen", NULL))
+ remmina_pref.shortcutkey_fullscreen = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_fullscreen",
+ NULL);
+ else
+ remmina_pref.shortcutkey_fullscreen = GDK_KEY_f;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_autofit", NULL))
+ remmina_pref.shortcutkey_autofit = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_autofit",
+ NULL);
+ else
+ remmina_pref.shortcutkey_autofit = GDK_KEY_1;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_nexttab", NULL))
+ remmina_pref.shortcutkey_nexttab = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_nexttab",
+ NULL);
+ else
+ remmina_pref.shortcutkey_nexttab = GDK_KEY_Right;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_prevtab", NULL))
+ remmina_pref.shortcutkey_prevtab = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_prevtab",
+ NULL);
+ else
+ remmina_pref.shortcutkey_prevtab = GDK_KEY_Left;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_scale", NULL))
+ remmina_pref.shortcutkey_scale = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_scale", NULL);
+ else
+ remmina_pref.shortcutkey_scale = GDK_KEY_s;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_clipboard", NULL))
+ remmina_pref.shortcutkey_clipboard = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_clipboard", NULL);
+ else
+ remmina_pref.shortcutkey_clipboard = GDK_KEY_b;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_viewonly", NULL))
+ remmina_pref.shortcutkey_viewonly = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_viewonly", NULL);
+ else
+ remmina_pref.shortcutkey_viewonly = GDK_KEY_m;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_multimon", NULL))
+ remmina_pref.shortcutkey_multimon = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_multimon", NULL);
+ else
+ remmina_pref.shortcutkey_multimon = GDK_KEY_Page_Up;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_grab", NULL))
+ remmina_pref.shortcutkey_grab = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_grab", NULL);
+ else
+ remmina_pref.shortcutkey_grab = GDK_KEY_Control_R;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_minimize", NULL))
+ remmina_pref.shortcutkey_minimize = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_minimize", NULL);
+ else
+ remmina_pref.shortcutkey_minimize = GDK_KEY_F9;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_screenshot", NULL))
+ remmina_pref.shortcutkey_screenshot = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_screenshot", NULL);
+ else
+ remmina_pref.shortcutkey_screenshot = GDK_KEY_F12;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_disconnect", NULL))
+ remmina_pref.shortcutkey_disconnect = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_disconnect",
+ NULL);
+ else
+ remmina_pref.shortcutkey_disconnect = GDK_KEY_F4;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "shortcutkey_toolbar", NULL))
+ remmina_pref.shortcutkey_toolbar = g_key_file_get_integer(gkeyfile, "remmina_pref", "shortcutkey_toolbar",
+ NULL);
+ else
+ remmina_pref.shortcutkey_toolbar = GDK_KEY_t;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "secret", NULL))
+ remmina_pref.secret = g_key_file_get_string(gkeyfile, "remmina_pref", "secret", NULL);
+ else
+ remmina_pref.secret = NULL;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "uid", NULL))
+ remmina_pref.uid = g_key_file_get_string(gkeyfile, "remmina_pref", "uid", NULL);
+ else
+ remmina_pref.uid = NULL;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "vte_font", NULL))
+ remmina_pref.vte_font = g_key_file_get_string(gkeyfile, "remmina_pref", "vte_font", NULL);
+ else
+ remmina_pref.vte_font = 0;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "vte_allow_bold_text", NULL))
+ remmina_pref.vte_allow_bold_text = g_key_file_get_boolean(gkeyfile, "remmina_pref", "vte_allow_bold_text",
+ NULL);
+ else
+ remmina_pref.vte_allow_bold_text = TRUE;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "vte_lines", NULL))
+ remmina_pref.vte_lines = g_key_file_get_integer(gkeyfile, "remmina_pref", "vte_lines", NULL);
+ else
+ remmina_pref.vte_lines = 512;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "vte_shortcutkey_copy", NULL))
+ remmina_pref.vte_shortcutkey_copy = g_key_file_get_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_copy",
+ NULL);
+ else
+ remmina_pref.vte_shortcutkey_copy = GDK_KEY_c;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "vte_shortcutkey_paste", NULL))
+ remmina_pref.vte_shortcutkey_paste = g_key_file_get_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_paste",
+ NULL);
+ else
+ remmina_pref.vte_shortcutkey_paste = GDK_KEY_v;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "vte_shortcutkey_select_all", NULL))
+ remmina_pref.vte_shortcutkey_select_all = g_key_file_get_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_select_all",
+ NULL);
+ else
+ remmina_pref.vte_shortcutkey_select_all = GDK_KEY_a;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "vte_shortcutkey_increase_font", NULL))
+ remmina_pref.vte_shortcutkey_increase_font = g_key_file_get_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_increase_font",
+ NULL);
+ else
+ remmina_pref.vte_shortcutkey_increase_font = GDK_KEY_Page_Up;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "vte_shortcutkey_decrease_font", NULL))
+ remmina_pref.vte_shortcutkey_decrease_font = g_key_file_get_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_decrease_font",
+ NULL);
+ else
+ remmina_pref.vte_shortcutkey_decrease_font = GDK_KEY_Page_Down;
+
+ if (g_key_file_has_key(gkeyfile, "remmina_pref", "vte_shortcutkey_search_text", NULL))
+ remmina_pref.vte_shortcutkey_search_text = g_key_file_get_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_search_text",
+ NULL);
+ else
+ remmina_pref.vte_shortcutkey_search_text = GDK_KEY_g;
+
+ remmina_pref_file_load_colors(gkeyfile, &remmina_pref.color_pref);
+
+ if (g_key_file_has_key(gkeyfile, "remmina_info", "periodic_news_last_checksum", NULL)) {
+ remmina_pref.periodic_news_last_checksum = g_key_file_get_string(gkeyfile, "remmina_info", "periodic_news_last_checksum", NULL);
+ }
+ else {
+ remmina_pref.periodic_news_last_checksum = NULL;
+ }
+
+ if (disablenews) {
+ info_disable_news = 1;
+ }
+ else if (g_key_file_has_key(gkeyfile, "remmina_info", "periodic_news_permitted", NULL)) {
+ remmina_pref.disable_news = !g_key_file_get_boolean(gkeyfile, "remmina_info", "periodic_news_permitted", NULL);
+ info_disable_news = remmina_pref.disable_news;
+ }
+ else {
+ remmina_pref.disable_news = TRUE;
+ info_disable_news = remmina_pref.disable_news;
+ }
+
+ if (disablestats) {
+ info_disable_stats = 1;
+ }
+ else if (g_key_file_has_key(gkeyfile, "remmina_info", "periodic_usage_stats_permitted", NULL)) {
+ remmina_pref.disable_stats = !g_key_file_get_boolean(gkeyfile, "remmina_info", "periodic_usage_stats_permitted", NULL);
+ info_disable_stats = remmina_pref.disable_stats;
+ }
+ else {
+ remmina_pref.disable_stats = TRUE;
+ info_disable_stats = remmina_pref.disable_stats;
+ }
+
+ if (g_key_file_has_key(gkeyfile, "remmina_info", "disable_tip", NULL)) {
+ remmina_pref.disable_tip = g_key_file_get_boolean(gkeyfile, "remmina_info", "disable_tip", NULL);
+ info_disable_tip = remmina_pref.disable_tip;
+ }
+ else {
+ remmina_pref.disable_tip = TRUE;
+ info_disable_tip = remmina_pref.disable_tip;
+ }
+
+ #ifdef DISABLE_NEWS
+ info_disable_news = 1;
+ remmina_pref.disable_news = TRUE;
+ #endif
+
+ #ifdef DISABLE_STATS
+ info_disable_stats = 1;
+ remmina_pref.disable_stats = TRUE;
+ #endif
+
+ #ifdef DISABLE_TIP
+ info_disable_tip = 1;
+ remmina_pref.disable_tip = TRUE;
+ #endif
+
+
+ if (g_key_file_has_key(gkeyfile, "remmina_info", "info_uid_prefix", NULL)) {
+ remmina_pref.info_uid_prefix = g_key_file_get_string(gkeyfile, "remmina_info", "info_uid_prefix", NULL);
+ }
+ else {
+ remmina_pref.info_uid_prefix = NULL;
+ }
+
+
+ /* If we have a color scheme file, we switch to it, GIO will merge it in the
+ * remmina.pref file */
+ if (g_file_test(remmina_colors_file, G_FILE_TEST_IS_REGULAR)) {
+ g_key_file_load_from_file(gkeyfile, remmina_colors_file, G_KEY_FILE_NONE, NULL);
+ g_remove(remmina_colors_file);
+ }
+
+ /* Default settings */
+ if (!g_key_file_has_key(gkeyfile, "remmina", "name", NULL)) {
+ g_key_file_set_string(gkeyfile, "remmina", "name", "");
+ g_key_file_set_integer(gkeyfile, "remmina", "ignore-tls-errors", 1);
+ g_key_file_set_integer(gkeyfile, "remmina", "enable-plugins", 1);
+ remmina_pref_save();
+ }
+
+ g_key_file_free(gkeyfile);
+
+ if (remmina_pref.secret == NULL)
+ remmina_pref_gen_secret();
+
+ remmina_pref_init_keymap();
+}
+
+gboolean remmina_pref_is_rw(void)
+{
+ TRACE_CALL(__func__);
+ if (access(remmina_pref_file, W_OK) == 0)
+ return TRUE;
+ else
+ return FALSE;
+ return FALSE;
+}
+
+gboolean remmina_pref_save(void)
+{
+ TRACE_CALL(__func__);
+
+ if (remmina_pref_is_rw() == FALSE) {
+ g_debug("remmina.pref is not writable, returning");
+ return FALSE;
+ }
+ GKeyFile *gkeyfile;
+ GError *error = NULL;
+ g_autofree gchar *content = NULL;
+ gsize length;
+
+ gkeyfile = g_key_file_new();
+
+ g_key_file_load_from_file(gkeyfile, remmina_pref_file, G_KEY_FILE_NONE, NULL);
+
+ g_key_file_set_string(gkeyfile, "remmina_pref", "datadir_path", remmina_pref.datadir_path);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "remmina_file_name", remmina_pref.remmina_file_name);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "screenshot_path", remmina_pref.screenshot_path);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "screenshot_name", remmina_pref.screenshot_name);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "deny_screenshot_clipboard", remmina_pref.deny_screenshot_clipboard);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "save_view_mode", remmina_pref.save_view_mode);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "confirm_close", remmina_pref.confirm_close);
+ if (g_key_file_remove_key(gkeyfile, "remmina_pref", "use_master_password", NULL))
+ REMMINA_DEBUG("use_master_password removed…");
+ else
+ REMMINA_INFO("use_master_password already migrated");
+#if SODIUM_VERSION_INT >= 90200
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "use_primary_password", remmina_pref.use_primary_password);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "unlock_timeout", remmina_pref.unlock_timeout);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "unlock_password", remmina_pref.unlock_password);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "lock_connect", remmina_pref.lock_connect);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "lock_edit", remmina_pref.lock_edit);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "lock_view_passwords", remmina_pref.lock_view_passwords);
+#else
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "use_primary_password", FALSE);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "unlock_timeout", 0);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "unlock_password", g_strdup(""));
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "lock_connect", FALSE);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "lock_edit", FALSE);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "lock_view_passwords", FALSE);
+#endif
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "enc_mode", remmina_pref.enc_mode);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "audit", remmina_pref.audit);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "trust_all", remmina_pref.trust_all);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "floating_toolbar_placement", remmina_pref.floating_toolbar_placement);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "toolbar_placement", remmina_pref.toolbar_placement);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "prevent_snap_welcome_message", remmina_pref.prevent_snap_welcome_message);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "last_quickconnect_protocol", remmina_pref.last_quickconnect_protocol);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "fullscreen_on_auto", remmina_pref.fullscreen_on_auto);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "always_show_tab", remmina_pref.always_show_tab);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "always_show_notes", remmina_pref.always_show_notes);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "hide_connection_toolbar", remmina_pref.hide_connection_toolbar);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "hide_searchbar", remmina_pref.hide_searchbar);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "default_action", remmina_pref.default_action);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "scale_quality", remmina_pref.scale_quality);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "ssh_loglevel", remmina_pref.ssh_loglevel);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "ssh_parseconfig", remmina_pref.ssh_parseconfig);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "hide_toolbar", remmina_pref.hide_toolbar);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "small_toolbutton", remmina_pref.small_toolbutton);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "view_file_mode", remmina_pref.view_file_mode);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "resolutions", remmina_pref.resolutions);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "keystrokes", remmina_pref.keystrokes);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "main_width", remmina_pref.main_width);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "main_height", remmina_pref.main_height);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "main_maximize", remmina_pref.main_maximize);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "main_sort_column_id", remmina_pref.main_sort_column_id);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "main_sort_order", remmina_pref.main_sort_order);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "expanded_group", remmina_pref.expanded_group);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "toolbar_pin_down", remmina_pref.toolbar_pin_down);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "sshtunnel_port", remmina_pref.sshtunnel_port);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "ssh_tcp_keepidle", remmina_pref.ssh_tcp_keepidle);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "ssh_tcp_keepintvl", remmina_pref.ssh_tcp_keepintvl);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "ssh_tcp_keepcnt", remmina_pref.ssh_tcp_keepcnt);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "ssh_tcp_usrtimeout", remmina_pref.ssh_tcp_usrtimeout);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "applet_new_ontop", remmina_pref.applet_new_ontop);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "applet_hide_count", remmina_pref.applet_hide_count);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "applet_enable_avahi", remmina_pref.applet_enable_avahi);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "disable_tray_icon", remmina_pref.disable_tray_icon);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "dark_theme", remmina_pref.dark_theme);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "recent_maximum", remmina_pref.recent_maximum);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "default_mode", remmina_pref.default_mode);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "tab_mode", remmina_pref.tab_mode);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "fullscreen_toolbar_visibility", remmina_pref.fullscreen_toolbar_visibility);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "auto_scroll_step", remmina_pref.auto_scroll_step);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "hostkey", remmina_pref.hostkey);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_fullscreen", remmina_pref.shortcutkey_fullscreen);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_autofit", remmina_pref.shortcutkey_autofit);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_nexttab", remmina_pref.shortcutkey_nexttab);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_prevtab", remmina_pref.shortcutkey_prevtab);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_scale", remmina_pref.shortcutkey_scale);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_clipboard", remmina_pref.shortcutkey_clipboard);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_grab", remmina_pref.shortcutkey_grab);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_multimon", remmina_pref.shortcutkey_multimon);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_viewonly", remmina_pref.shortcutkey_viewonly);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_screenshot", remmina_pref.shortcutkey_screenshot);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_minimize", remmina_pref.shortcutkey_minimize);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_disconnect", remmina_pref.shortcutkey_disconnect);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "shortcutkey_toolbar", remmina_pref.shortcutkey_toolbar);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_copy", remmina_pref.vte_shortcutkey_copy);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_paste", remmina_pref.vte_shortcutkey_paste);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_select_all", remmina_pref.vte_shortcutkey_select_all);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_increase_font", remmina_pref.vte_shortcutkey_increase_font);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_decrease_font", remmina_pref.vte_shortcutkey_decrease_font);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "vte_shortcutkey_search_text", remmina_pref.vte_shortcutkey_search_text);
+ g_key_file_set_string(gkeyfile, "remmina_pref", "vte_font", remmina_pref.vte_font ? remmina_pref.vte_font : "");
+ g_key_file_set_string(gkeyfile, "remmina_pref", "grab_color", remmina_pref.grab_color ? remmina_pref.grab_color : "");
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "grab_color_switch", remmina_pref.grab_color_switch);
+ g_key_file_set_boolean(gkeyfile, "remmina_pref", "vte_allow_bold_text", remmina_pref.vte_allow_bold_text);
+ g_key_file_set_integer(gkeyfile, "remmina_pref", "vte_lines", remmina_pref.vte_lines);
+ g_key_file_set_string(gkeyfile, "ssh_colors", "background", remmina_pref.color_pref.background ? remmina_pref.color_pref.background : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "cursor", remmina_pref.color_pref.cursor ? remmina_pref.color_pref.cursor : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "cursor_foreground", remmina_pref.color_pref.cursor_foreground ? remmina_pref.color_pref.cursor_foreground : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "highlight", remmina_pref.color_pref.highlight ? remmina_pref.color_pref.highlight : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "highlight_foreground", remmina_pref.color_pref.highlight_foreground ? remmina_pref.color_pref.highlight_foreground : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "colorBD", remmina_pref.color_pref.colorBD ? remmina_pref.color_pref.colorBD : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "foreground", remmina_pref.color_pref.foreground ? remmina_pref.color_pref.foreground : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color0", remmina_pref.color_pref.color0 ? remmina_pref.color_pref.color0 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color1", remmina_pref.color_pref.color1 ? remmina_pref.color_pref.color1 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color2", remmina_pref.color_pref.color2 ? remmina_pref.color_pref.color2 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color3", remmina_pref.color_pref.color3 ? remmina_pref.color_pref.color3 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color4", remmina_pref.color_pref.color4 ? remmina_pref.color_pref.color4 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color5", remmina_pref.color_pref.color5 ? remmina_pref.color_pref.color5 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color6", remmina_pref.color_pref.color6 ? remmina_pref.color_pref.color6 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color7", remmina_pref.color_pref.color7 ? remmina_pref.color_pref.color7 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color8", remmina_pref.color_pref.color8 ? remmina_pref.color_pref.color8 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color9", remmina_pref.color_pref.color9 ? remmina_pref.color_pref.color9 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color10", remmina_pref.color_pref.color10 ? remmina_pref.color_pref.color10 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color11", remmina_pref.color_pref.color11 ? remmina_pref.color_pref.color11 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color12", remmina_pref.color_pref.color12 ? remmina_pref.color_pref.color12 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color13", remmina_pref.color_pref.color13 ? remmina_pref.color_pref.color13 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color14", remmina_pref.color_pref.color14 ? remmina_pref.color_pref.color14 : "");
+ g_key_file_set_string(gkeyfile, "ssh_colors", "color15", remmina_pref.color_pref.color15 ? remmina_pref.color_pref.color15 : "");
+ g_key_file_set_boolean(gkeyfile, "remmina_info", "periodic_news_permitted", !remmina_pref.disable_news);
+ g_key_file_set_string(gkeyfile, "remmina_info", "periodic_news_last_checksum", remmina_pref.periodic_news_last_checksum ? remmina_pref.periodic_news_last_checksum: "");
+ g_key_file_set_boolean(gkeyfile, "remmina_info", "periodic_usage_stats_permitted", !remmina_pref.disable_stats);
+ g_key_file_set_string(gkeyfile, "remmina_info", "info_uid_prefix", remmina_pref.info_uid_prefix ? remmina_pref.info_uid_prefix : "");
+ g_key_file_set_boolean(gkeyfile, "remmina_info", "disable_tip", remmina_pref.disable_tip);
+
+ /* Default settings */
+ g_key_file_set_string(gkeyfile, "remmina", "name", "");
+ g_key_file_set_integer(gkeyfile, "remmina", "ignore-tls-errors", 1);
+
+ content = g_key_file_to_data(gkeyfile, &length, NULL);
+ g_file_set_contents(remmina_pref_file, content, length, &error);
+
+ if (error != NULL) {
+ g_warning("remmina_pref_save error: %s", error->message);
+ g_clear_error(&error);
+ g_key_file_free(gkeyfile);
+ return FALSE;
+ }
+ g_key_file_free(gkeyfile);
+ return TRUE;
+}
+
+void remmina_pref_add_recent(const gchar *protocol, const gchar *server)
+{
+ TRACE_CALL(__func__);
+ RemminaStringArray *array;
+ GKeyFile *gkeyfile;
+ gchar key[20];
+ g_autofree gchar *val = NULL;
+ g_autofree gchar *content = NULL;
+ gsize length;
+
+ if (remmina_pref.recent_maximum <= 0 || server == NULL || server[0] == 0)
+ return;
+
+ /* Load original value into memory */
+ gkeyfile = g_key_file_new();
+
+ g_key_file_load_from_file(gkeyfile, remmina_pref_file, G_KEY_FILE_NONE, NULL);
+
+ g_snprintf(key, sizeof(key), "recent_%s", protocol);
+ array = remmina_string_array_new_from_allocated_string(g_key_file_get_string(gkeyfile, "remmina_pref", key, NULL));
+
+ /* Add the new value */
+ remmina_string_array_remove(array, server);
+ while (array->len >= remmina_pref.recent_maximum)
+ remmina_string_array_remove_index(array, 0);
+ remmina_string_array_add(array, server);
+
+ /* Save */
+ val = remmina_string_array_to_string(array);
+ g_key_file_set_string(gkeyfile, "remmina_pref", key, val);
+
+ content = g_key_file_to_data(gkeyfile, &length, NULL);
+ g_file_set_contents(remmina_pref_file, content, length, NULL);
+
+ g_key_file_free(gkeyfile);
+}
+
+gchar *
+remmina_pref_get_recent(const gchar *protocol)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+ gchar key[20];
+ gchar *val = NULL;
+
+ gkeyfile = g_key_file_new();
+
+ g_key_file_load_from_file(gkeyfile, remmina_pref_file, G_KEY_FILE_NONE, NULL);
+
+ g_snprintf(key, sizeof(key), "recent_%s", protocol);
+ val = g_key_file_get_string(gkeyfile, "remmina_pref", key, NULL);
+
+ g_key_file_free(gkeyfile);
+
+ return val;
+}
+
+void remmina_pref_clear_recent(void)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+ gchar **keys;
+ gint i;
+ g_autofree gchar *content = NULL;
+ gsize length;
+
+ gkeyfile = g_key_file_new();
+
+ g_key_file_load_from_file(gkeyfile, remmina_pref_file, G_KEY_FILE_NONE, NULL);
+ keys = g_key_file_get_keys(gkeyfile, "remmina_pref", NULL, NULL);
+ if (keys) {
+ for (i = 0; keys[i]; i++)
+ if (strncmp(keys[i], "recent_", 7) == 0)
+ g_key_file_set_string(gkeyfile, "remmina_pref", keys[i], "");
+ g_strfreev(keys);
+ }
+
+ content = g_key_file_to_data(gkeyfile, &length, NULL);
+ g_file_set_contents(remmina_pref_file, content, length, NULL);
+
+ g_key_file_free(gkeyfile);
+}
+
+guint *remmina_pref_keymap_get_table(const gchar *keymap)
+{
+ TRACE_CALL(__func__);
+
+ if (!keymap || keymap[0] == '\0')
+ return NULL;
+
+ return (guint *)g_hash_table_lookup(remmina_keymap_table, keymap);
+}
+
+guint remmina_pref_keymap_get_keyval(const gchar *keymap, guint keyval)
+{
+ TRACE_CALL(__func__);
+ guint *table;
+ gint i;
+
+ if (!keymap || keymap[0] == '\0')
+ return keyval;
+
+ table = (guint *)g_hash_table_lookup(remmina_keymap_table, keymap);
+ if (!table)
+ return keyval;
+ for (i = 0; table[i] > 0; i += 2)
+ if (table[i] == keyval)
+ return table[i + 1];
+ return keyval;
+}
+
+gchar **
+remmina_pref_keymap_groups(void)
+{
+ TRACE_CALL(__func__);
+ GList *list;
+ guint len;
+ gchar **keys;
+ guint i;
+
+ list = g_hash_table_get_keys(remmina_keymap_table);
+ len = g_list_length(list);
+
+ keys = g_new0(gchar *, (len + 1) * 2 + 1);
+ keys[0] = g_strdup("");
+ keys[1] = g_strdup("");
+ for (i = 0; i < len; i++) {
+ keys[(i + 1) * 2] = g_strdup((gchar *)g_list_nth_data(list, i));
+ keys[(i + 1) * 2 + 1] = g_strdup((gchar *)g_list_nth_data(list, i));
+ }
+ g_list_free(list);
+
+ return keys;
+}
+
+gint remmina_pref_get_scale_quality(void)
+{
+ TRACE_CALL(__func__);
+ /* Paranoid programming */
+ if (remmina_pref.scale_quality < 0)
+ remmina_pref.scale_quality = 0;
+ return remmina_pref.scale_quality;
+}
+
+gint remmina_pref_get_ssh_loglevel(void)
+{
+ TRACE_CALL(__func__);
+ return remmina_pref.ssh_loglevel;
+}
+
+gboolean remmina_pref_get_ssh_parseconfig(void)
+{
+ TRACE_CALL(__func__);
+ return remmina_pref.ssh_parseconfig;
+}
+
+gint remmina_pref_get_sshtunnel_port(void)
+{
+ TRACE_CALL(__func__);
+ return remmina_pref.sshtunnel_port;
+}
+
+gint remmina_pref_get_ssh_tcp_keepidle(void)
+{
+ TRACE_CALL(__func__);
+ return remmina_pref.ssh_tcp_keepidle;
+}
+
+gint remmina_pref_get_ssh_tcp_keepintvl(void)
+{
+ TRACE_CALL(__func__);
+ return remmina_pref.ssh_tcp_keepintvl;
+}
+
+gint remmina_pref_get_ssh_tcp_keepcnt(void)
+{
+ TRACE_CALL(__func__);
+ return remmina_pref.ssh_tcp_keepcnt;
+}
+
+gint remmina_pref_get_ssh_tcp_usrtimeout(void)
+{
+ TRACE_CALL(__func__);
+ return remmina_pref.ssh_tcp_usrtimeout;
+}
+
+void remmina_pref_set_value(const gchar *key, const gchar *value)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+ gsize length;
+
+ gkeyfile = g_key_file_new();
+ if (g_key_file_load_from_file(gkeyfile, remmina_pref_file, G_KEY_FILE_NONE, NULL)) {
+ g_key_file_set_string(gkeyfile, "remmina_pref", key, value);
+ gchar *content = g_key_file_to_data(gkeyfile, &length, NULL);
+ if (g_file_set_contents(remmina_pref_file, content, length, NULL)) {
+ g_free(content);
+ } else {
+ REMMINA_WARNING("Cannot save Remmina preferences");
+ REMMINA_WARNING("Key was \"%s\", and value \"%s\"", key, value);
+ }
+ g_key_file_free(gkeyfile);
+ } else {
+ REMMINA_WARNING("Cannot load Remmina preferences file");
+ }
+}
+
+gchar *remmina_pref_get_value(const gchar *key)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+ gchar *value = NULL;
+
+ gkeyfile = g_key_file_new();
+ g_key_file_load_from_file(gkeyfile, remmina_pref_file, G_KEY_FILE_NONE, NULL);
+ value = g_key_file_get_string(gkeyfile, "remmina_pref", key, NULL);
+ g_key_file_free(gkeyfile);
+
+ return value;
+}
+
+gboolean remmina_pref_get_boolean(const gchar *key)
+{
+ TRACE_CALL(__func__);
+ GKeyFile *gkeyfile;
+ gboolean value;
+
+ gkeyfile = g_key_file_new();
+ g_key_file_load_from_file(gkeyfile, remmina_pref_file, G_KEY_FILE_NONE, NULL);
+ value = g_key_file_get_boolean(gkeyfile, "remmina_pref", key, NULL);
+ g_key_file_free(gkeyfile);
+
+ return value;
+}
diff --git a/src/remmina_pref.h b/src/remmina_pref.h
new file mode 100644
index 0000000..a0be42f
--- /dev/null
+++ b/src/remmina_pref.h
@@ -0,0 +1,295 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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
+ * 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.
+ *
+ */
+
+#pragma once
+#include <gtk/gtk.h>
+#include "remmina_sodium.h"
+
+#ifdef HAVE_LIBGCRYPT
+#include <gcrypt.h>
+#endif
+
+
+/*
+ * Remmina Preference Loader
+ */
+
+G_BEGIN_DECLS
+
+enum {
+ REMMINA_VIEW_FILE_LIST,
+ REMMINA_VIEW_FILE_TREE
+};
+
+enum {
+ REMMINA_ACTION_CONNECT = 0,
+ REMMINA_ACTION_EDIT = 1
+};
+
+enum {
+ UNDEFINED_MODE = 0,
+ SCROLLED_WINDOW_MODE = 1,
+ FULLSCREEN_MODE = 2,
+ SCROLLED_FULLSCREEN_MODE = 3,
+ VIEWPORT_FULLSCREEN_MODE = 4
+};
+
+enum {
+ FLOATING_TOOLBAR_PLACEMENT_TOP = 0,
+ FLOATING_TOOLBAR_PLACEMENT_BOTTOM = 1
+};
+
+enum {
+ TOOLBAR_PLACEMENT_TOP = 0,
+ TOOLBAR_PLACEMENT_RIGHT = 1,
+ TOOLBAR_PLACEMENT_BOTTOM = 2,
+ TOOLBAR_PLACEMENT_LEFT = 3
+};
+
+enum {
+ REMMINA_TAB_BY_GROUP = 0,
+ REMMINA_TAB_BY_PROTOCOL = 1,
+ REMMINA_TAB_ALL = 2,
+ REMMINA_TAB_NONE = 3
+};
+
+/* Remember to add the id 0, 4 and 5 in the remmina pref editor */
+enum {
+ RM_ENC_MODE_SECRET = 0, /* Using libsecret */
+ RM_ENC_MODE_SODIUM_INTERACTIVE = 1, /* Using libsodium */
+ RM_ENC_MODE_SODIUM_MODERATE = 2, /* Using libsodium */
+ RM_ENC_MODE_SODIUM_SENSITIVE = 3, /* Using libsodium */
+ RM_ENC_MODE_GCRYPT = 4, /* Using GCrypt */
+ RM_ENC_MODE_NONE = 5 /* No encryption */
+};
+
+enum {
+ FLOATING_TOOLBAR_VISIBILITY_PEEKING = 0,
+ FLOATING_TOOLBAR_VISIBILITY_INVISIBLE = 1, //"Invisible" corresponds to the "Hidden" option in the drop-down
+ FLOATING_TOOLBAR_VISIBILITY_DISABLE = 2
+};
+
+typedef struct _RemminaColorPref {
+ /* Color palette for VTE terminal */
+ gchar * background;
+ gchar * cursor;
+ gchar * cursor_foreground;
+ gchar * highlight;
+ gchar * highlight_foreground;
+ gchar * colorBD;
+ gchar * foreground;
+ gchar * color0;
+ gchar * color1;
+ gchar * color2;
+ gchar * color3;
+ gchar * color4;
+ gchar * color5;
+ gchar * color6;
+ gchar * color7;
+ gchar * color8;
+ gchar * color9;
+ gchar * color10;
+ gchar * color11;
+ gchar * color12;
+ gchar * color13;
+ gchar * color14;
+ gchar * color15;
+} RemminaColorPref;
+
+typedef struct _RemminaPref {
+ /* In RemminaPrefDialog options tab */
+ const gchar * datadir_path;
+ const gchar * remmina_file_name;
+ const gchar * screenshot_path;
+ gboolean deny_screenshot_clipboard;
+ const gchar * screenshot_name;
+ gboolean save_view_mode;
+ gint default_action;
+ gint scale_quality;
+ gint auto_scroll_step;
+ gint recent_maximum;
+ gchar * resolutions;
+ gchar * keystrokes;
+ gboolean confirm_close;
+ /* In RemminaPrefDialog appearance tab */
+ gboolean dark_theme;
+ gboolean list_refresh_workaround;
+ gboolean fullscreen_on_auto;
+ gboolean always_show_tab;
+ gboolean always_show_notes;
+ gboolean hide_connection_toolbar;
+ gboolean hide_searchbar;
+ gint default_mode;
+ gint tab_mode;
+ gint fullscreen_toolbar_visibility;
+ const gchar * grab_color;
+ gboolean grab_color_switch;
+ /* In RemminaPrefDialog applet tab */
+ gboolean applet_new_ontop;
+ gboolean applet_hide_count;
+ gboolean disable_tray_icon;
+ /* In RemminaPrefDialog SSH Option tab */
+ gint ssh_loglevel;
+ gboolean ssh_parseconfig;
+ gint sshtunnel_port;
+ gint ssh_tcp_keepidle;
+ gint ssh_tcp_keepintvl;
+ gint ssh_tcp_keepcnt;
+ gint ssh_tcp_usrtimeout;
+ /* In RemminaPrefDialog keyboard tab */
+ guint hostkey;
+ guint shortcutkey_fullscreen;
+ guint shortcutkey_autofit;
+ guint shortcutkey_prevtab;
+ guint shortcutkey_clipboard;
+ guint shortcutkey_nexttab;
+ guint shortcutkey_dynres;
+ guint shortcutkey_scale;
+ guint shortcutkey_multimon;
+ guint shortcutkey_grab;
+ guint shortcutkey_viewonly;
+ guint shortcutkey_screenshot;
+ guint shortcutkey_minimize;
+ guint shortcutkey_disconnect;
+ guint shortcutkey_toolbar;
+ /* In RemminaPrefDialog security tab */
+ gboolean use_primary_password;
+ const gchar * unlock_password;
+ const gchar * unlock_repassword;
+ gint unlock_timeout;
+ gboolean lock_connect;
+ gboolean lock_edit;
+ gboolean lock_view_passwords;
+ gint enc_mode;
+ gboolean audit;
+ gboolean trust_all;
+ /* In RemminaPrefDialog terminal tab */
+ gchar * vte_font;
+ gboolean vte_allow_bold_text;
+ gboolean vte_system_colors;
+ gint vte_lines;
+ guint vte_shortcutkey_copy;
+ guint vte_shortcutkey_paste;
+ guint vte_shortcutkey_select_all;
+ guint vte_shortcutkey_increase_font;
+ guint vte_shortcutkey_decrease_font;
+ guint vte_shortcutkey_search_text;
+ /* In View menu */
+ gboolean hide_toolbar;
+ gboolean small_toolbutton;
+ gint view_file_mode;
+ /* In tray icon */
+ gboolean applet_enable_avahi;
+ /* Auto */
+ gint main_width;
+ gint main_height;
+ gboolean main_maximize;
+ gint main_sort_column_id;
+ gint main_sort_order;
+ gchar * expanded_group;
+ gboolean toolbar_pin_down;
+ gint floating_toolbar_placement;
+ gint toolbar_placement;
+ gboolean prevent_snap_welcome_message;
+ gchar * last_quickconnect_protocol;
+
+ /* Crypto */
+ gchar * secret;
+
+ /* UID */
+ gchar * uid;
+
+ RemminaColorPref color_pref;
+
+ /* Usage stats */
+ gboolean disable_stats;
+ gchar * info_uid_prefix;
+
+ /* Remmina news */
+ gboolean disable_news;
+ gchar * periodic_news_last_checksum;
+
+ gboolean disable_tip;
+
+} RemminaPref;
+
+#define DEFAULT_SSH_PARSECONFIG TRUE
+#define DEFAULT_SSHTUNNEL_PORT 4732
+#define DEFAULT_SSH_PORT 22
+#define DEFAULT_SSH_LOGLEVEL 1
+#define SSH_SOCKET_TCP_KEEPIDLE 20
+#define SSH_SOCKET_TCP_KEEPINTVL 10
+#define SSH_SOCKET_TCP_KEEPCNT 3
+#define SSH_SOCKET_TCP_USER_TIMEOUT 60000 // 60 seconds
+
+extern const gchar *default_resolutions;
+extern gchar *remmina_pref_file;
+extern gchar *remmina_colors_file;
+extern RemminaPref remmina_pref;
+extern gboolean disabletoolbar;
+extern gboolean fullscreen;
+extern gboolean extrahardening;
+extern gboolean disabletrayicon;
+
+void remmina_pref_init(void);
+gboolean remmina_pref_is_rw(void);
+gboolean remmina_pref_save(void);
+
+void remmina_pref_add_recent(const gchar *protocol, const gchar *server);
+gchar *remmina_pref_get_recent(const gchar *protocol);
+void remmina_pref_clear_recent(void);
+
+guint *remmina_pref_keymap_get_table(const gchar *keymap);
+guint remmina_pref_keymap_get_keyval(const gchar *keymap, guint keyval);
+gchar **remmina_pref_keymap_groups(void);
+
+gint remmina_pref_get_scale_quality(void);
+gint remmina_pref_get_ssh_loglevel(void);
+gboolean remmina_pref_get_ssh_parseconfig(void);
+gint remmina_pref_get_sshtunnel_port(void);
+void remmina_pref_file_load_colors(GKeyFile *gkeyfile, RemminaColorPref *color_pref);
+gint remmina_pref_get_ssh_tcp_keepidle(void);
+gint remmina_pref_get_ssh_tcp_keepintvl(void);
+gint remmina_pref_get_ssh_tcp_keepcnt(void);
+gint remmina_pref_get_ssh_tcp_usrtimeout(void);
+
+void remmina_pref_set_value(const gchar *key, const gchar *value);
+gchar *remmina_pref_get_value(const gchar *key);
+gboolean remmina_pref_get_boolean(const gchar *key);
+
+G_END_DECLS
diff --git a/src/remmina_pref_dialog.c b/src/remmina_pref_dialog.c
new file mode 100644
index 0000000..ab85f0d
--- /dev/null
+++ b/src/remmina_pref_dialog.c
@@ -0,0 +1,883 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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
+ * 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.
+ *
+ */
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include <stdlib.h>
+#include "config.h"
+#if defined (HAVE_LIBSSH) && defined (HAVE_LIBVTE)
+#include <vte/vte.h>
+#endif
+#include "remmina_log.h"
+#include "remmina_file_manager.h"
+#include "remmina_sodium.h"
+#include "remmina_passwd.h"
+#include "remmina_public.h"
+#include "remmina_main.h"
+#include "remmina_string_list.h"
+#include "remmina_widget_pool.h"
+#include "remmina_key_chooser.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_icon.h"
+#include "remmina_pref.h"
+#include "remmina_pref_dialog.h"
+#include "remmina/remmina_trace_calls.h"
+
+static RemminaPrefDialog *remmina_pref_dialog;
+
+#define GET_OBJECT(object_name) gtk_builder_get_object(remmina_pref_dialog->builder, object_name)
+
+static GActionEntry pref_actions[] = {
+ { "close", remmina_pref_dialog_on_action_close, NULL, NULL, NULL },
+};
+
+
+/* Show a key chooser dialog */
+void remmina_pref_dialog_on_key_chooser(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaKeyChooserArguments *arguments;
+
+ g_return_if_fail(GTK_IS_BUTTON(widget));
+
+ arguments = remmina_key_chooser_new(GTK_WINDOW(remmina_pref_dialog->dialog), FALSE);
+ if (arguments->response != GTK_RESPONSE_CANCEL && arguments->response != GTK_RESPONSE_DELETE_EVENT) {
+ gchar *val = remmina_key_chooser_get_value(arguments->keyval, arguments->state);
+ gtk_button_set_label(GTK_BUTTON(widget), val);
+ g_free(val);
+ }
+ g_free(arguments);
+}
+
+/* Show the available resolutions list dialog */
+void remmina_pref_on_button_resolutions_clicked(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkDialog *dialog = remmina_string_list_new(FALSE, NULL);
+
+ remmina_string_list_set_validation_func(remmina_public_resolution_validation_func);
+ remmina_string_list_set_text(remmina_pref.resolutions, TRUE);
+ remmina_string_list_set_titles(_("Resolutions"), _("Configure the available resolutions"));
+ gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(remmina_pref_dialog->dialog));
+ gtk_dialog_run(dialog);
+ g_free(remmina_pref.resolutions);
+ remmina_pref.resolutions = remmina_string_list_get_text();
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+}
+
+/* Re-initialize the remmina_pref_init to reload the color scheme when a color scheme
+ * file is selected*/
+void remmina_pref_on_color_scheme_selected(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gchar *sourcepath;
+ gchar *remmina_dir;
+ gchar *destpath;
+ GFile *source;
+ GFile *destination;
+
+ sourcepath = gtk_file_chooser_get_filename(remmina_pref_dialog->button_term_cs);
+ source = g_file_new_for_path(sourcepath);
+
+ remmina_dir = g_build_path("/", g_get_user_config_dir(), "remmina", NULL);
+ /* /home/foo/.config/remmina */
+ destpath = g_strdup_printf("%s/remmina.colors", remmina_dir);
+ destination = g_file_new_for_path(destpath);
+
+ if (g_file_test(sourcepath, G_FILE_TEST_IS_REGULAR)) {
+ g_file_copy(source,
+ destination,
+ G_FILE_COPY_OVERWRITE,
+ NULL,
+ NULL,
+ NULL,
+ NULL);
+ /* Here we should reinitialize the widget */
+ gtk_file_chooser_set_file(remmina_pref_dialog->button_term_cs, source, NULL);
+ }
+ g_free(sourcepath);
+ g_free(remmina_dir);
+ g_free(destpath);
+ g_object_unref(source);
+}
+
+void remmina_pref_dialog_clear_recent(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkDialog *dialog;
+
+ remmina_pref_clear_recent();
+ dialog = GTK_DIALOG(gtk_message_dialog_new(GTK_WINDOW(remmina_pref_dialog->dialog),
+ GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
+ _("Recent lists cleared.")));
+ gtk_dialog_run(dialog);
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+}
+
+/* Configure custom keystrokes to send to the plugins */
+void remmina_pref_on_button_keystrokes_clicked(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkDialog *dialog = remmina_string_list_new(TRUE, STRING_DELIMITOR2);
+
+ remmina_string_list_set_text(remmina_pref.keystrokes, TRUE);
+ remmina_string_list_set_titles(_("Keystrokes"), _("Configure the keystrokes"));
+ gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(remmina_pref_dialog->dialog));
+ gtk_dialog_run(dialog);
+ g_free(remmina_pref.keystrokes);
+ remmina_pref.keystrokes = remmina_string_list_get_text();
+ gtk_widget_destroy(GTK_WIDGET(dialog));
+}
+
+void remmina_prefdiag_on_grab_color_activated(GtkSwitch *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ //REMMINA_DEBUG ("entry_grab_color %d", gtk_switch_get_active(widget));
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->entry_grab_color), gtk_switch_get_active(widget));
+}
+
+/* connect to notify::active or toggled (in this case ::toggled */
+void remmina_prefdiag_on_use_password_activated(GtkSwitch *sw, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ //REMMINA_DEBUG ("Use Primary Password %d", gtk_switch_get_active(sw));
+ if (gtk_switch_get_active(sw)) {
+ //REMMINA_DEBUG ("use_password activated");
+ gchar *unlock_password = NULL;
+ unlock_password = g_strdup(remmina_pref_get_value("unlock_password"));
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->switch_security_lock_connect), TRUE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->switch_security_lock_edit), TRUE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->switch_security_lock_view_passwords), TRUE);
+ //REMMINA_DEBUG ("Password from preferences is: %s", unlock_password);
+ if (unlock_password == NULL || unlock_password[0] == '\0') {
+ if (remmina_passwd(GTK_WINDOW(remmina_pref_dialog->dialog), &unlock_password)) {
+ //REMMINA_DEBUG ("Password is: %s", unlock_password);
+ remmina_pref_set_value("unlock_password", g_strdup(unlock_password));
+ remmina_pref.unlock_password = g_strdup(unlock_password);
+ } else {
+ remmina_pref.unlock_password = "";
+ remmina_pref_set_value("unlock_password", "");
+ }
+ }
+ g_free(unlock_password), unlock_password = NULL;
+ } else {
+ //REMMINA_DEBUG ("use_password deactivated");
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->switch_security_lock_connect), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->switch_security_lock_edit), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->switch_security_lock_view_passwords), FALSE);
+ remmina_pref.unlock_password = "";
+ remmina_pref_set_value("unlock_password", "");
+ }
+}
+
+void remmina_pref_dialog_on_action_close(GSimpleAction *action, GVariant *param, gpointer data)
+{
+ TRACE_CALL(__func__);
+ gtk_widget_destroy(GTK_WIDGET(remmina_pref_dialog->dialog));
+ /* Reload to use new preferences */
+ remmina_main_reload_preferences();
+}
+
+void remmina_pref_dialog_on_close_clicked(GtkWidget *widget, RemminaPrefDialog *dialog)
+{
+ TRACE_CALL(__func__);
+ gtk_widget_destroy(GTK_WIDGET(remmina_pref_dialog->dialog));
+}
+
+void remmina_pref_on_dialog_destroy(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gboolean b;
+ GdkRGBA color;
+ gboolean rebuild_remmina_icon = FALSE;
+
+ remmina_pref.datadir_path = gtk_file_chooser_get_filename(remmina_pref_dialog->filechooserbutton_options_datadir_path);
+ if (remmina_pref.datadir_path == NULL)
+ remmina_pref.datadir_path = g_strdup("");
+ remmina_pref.remmina_file_name = gtk_entry_get_text(remmina_pref_dialog->entry_options_file_name);
+ remmina_pref.screenshot_path = gtk_file_chooser_get_filename(remmina_pref_dialog->filechooserbutton_options_screenshots_path);
+ remmina_pref.screenshot_name = gtk_entry_get_text(remmina_pref_dialog->entry_options_screenshot_name);
+ remmina_pref.deny_screenshot_clipboard = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_options_deny_screenshot_clipboard));
+ remmina_pref.save_view_mode = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_options_remember_last_view_mode));
+ remmina_pref.confirm_close = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_options_confirm_close));
+ remmina_pref.use_primary_password = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_security_use_primary_password));
+ remmina_pref.lock_connect = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_security_lock_connect));
+ remmina_pref.lock_edit = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_security_lock_edit));
+ remmina_pref.lock_view_passwords = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_security_lock_view_passwords));
+ remmina_pref.enc_mode = gtk_combo_box_get_active(remmina_pref_dialog->comboboxtext_security_enc_method);
+ remmina_pref.audit = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_security_audit));
+ remmina_pref.trust_all = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_security_trust_all));
+ remmina_pref.screenshot_path = gtk_file_chooser_get_filename(remmina_pref_dialog->filechooserbutton_options_screenshots_path);
+ remmina_pref.fullscreen_on_auto = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_appearance_fullscreen_on_auto));
+ remmina_pref.always_show_tab = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_appearance_show_tabs));
+ remmina_pref.always_show_notes = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_appearance_show_notes));
+ remmina_pref.hide_connection_toolbar = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_appearance_hide_toolbar));
+ remmina_pref.hide_searchbar = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_appearance_hide_searchbar));
+ remmina_pref.disable_news = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_disable_news));
+ remmina_pref.disable_stats = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_disable_stats));
+ remmina_pref.disable_tip = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_disable_tip));
+ remmina_pref.default_action = gtk_combo_box_get_active(remmina_pref_dialog->comboboxtext_options_double_click);
+ remmina_pref.default_mode = gtk_combo_box_get_active(remmina_pref_dialog->comboboxtext_appearance_view_mode);
+ remmina_pref.tab_mode = gtk_combo_box_get_active(remmina_pref_dialog->comboboxtext_appearance_tab_interface);
+ remmina_pref.fullscreen_toolbar_visibility = gtk_combo_box_get_active(remmina_pref_dialog->comboboxtext_appearance_fullscreen_toolbar_visibility);
+ remmina_pref.scale_quality = gtk_combo_box_get_active(remmina_pref_dialog->comboboxtext_options_scale_quality);
+ remmina_pref.ssh_loglevel = gtk_combo_box_get_active(remmina_pref_dialog->comboboxtext_options_ssh_loglevel);
+ remmina_pref.sshtunnel_port = atoi(gtk_entry_get_text(remmina_pref_dialog->entry_options_ssh_port));
+ if (remmina_pref.sshtunnel_port <= 0)
+ remmina_pref.sshtunnel_port = DEFAULT_SSHTUNNEL_PORT;
+ remmina_pref.ssh_tcp_keepidle = atoi(gtk_entry_get_text(remmina_pref_dialog->entry_options_ssh_tcp_keepidle));
+ if (remmina_pref.ssh_tcp_keepidle <= 0)
+ remmina_pref.ssh_tcp_keepidle = SSH_SOCKET_TCP_KEEPIDLE;
+ remmina_pref.ssh_tcp_keepintvl = atoi(gtk_entry_get_text(remmina_pref_dialog->entry_options_ssh_tcp_keepintvl));
+ if (remmina_pref.ssh_tcp_keepintvl <= 0)
+ remmina_pref.ssh_tcp_keepintvl = SSH_SOCKET_TCP_KEEPINTVL;
+ remmina_pref.ssh_tcp_keepcnt = atoi(gtk_entry_get_text(remmina_pref_dialog->entry_options_ssh_tcp_keepcnt));
+ if (remmina_pref.ssh_tcp_keepcnt <= 0)
+ remmina_pref.ssh_tcp_keepcnt = SSH_SOCKET_TCP_KEEPCNT;
+ remmina_pref.ssh_tcp_usrtimeout = atoi(gtk_entry_get_text(remmina_pref_dialog->entry_options_ssh_tcp_usrtimeout));
+ if (remmina_pref.ssh_tcp_usrtimeout <= 0)
+ remmina_pref.ssh_tcp_usrtimeout = SSH_SOCKET_TCP_USER_TIMEOUT;
+ remmina_pref.ssh_parseconfig = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_options_ssh_parseconfig));
+#if SODIUM_VERSION_INT >= 90200
+ remmina_pref.unlock_timeout = atoi(gtk_entry_get_text(remmina_pref_dialog->unlock_timeout));
+ if (remmina_pref.unlock_timeout < 0)
+ remmina_pref.unlock_timeout = 0;
+#endif
+
+ remmina_pref.auto_scroll_step = atoi(gtk_entry_get_text(remmina_pref_dialog->entry_options_scroll));
+ if (remmina_pref.auto_scroll_step < 10)
+ remmina_pref.auto_scroll_step = 10;
+ else if (remmina_pref.auto_scroll_step > 500)
+ remmina_pref.auto_scroll_step = 500;
+
+ remmina_pref.recent_maximum = atoi(gtk_entry_get_text(remmina_pref_dialog->entry_options_recent_items));
+ if (remmina_pref.recent_maximum < 0)
+ remmina_pref.recent_maximum = 0;
+
+ remmina_pref.applet_new_ontop = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_applet_new_connection_on_top));
+ remmina_pref.applet_hide_count = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_applet_hide_totals));
+ b = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_dark_theme));
+ if (remmina_pref.dark_theme != b) {
+ remmina_pref.dark_theme = b;
+ rebuild_remmina_icon = TRUE;
+ }
+
+ remmina_pref.grab_color_switch = gtk_switch_get_active(remmina_pref_dialog->switch_appearance_grab_color);
+ remmina_pref.grab_color = gtk_entry_get_text(remmina_pref_dialog->entry_grab_color);
+
+ b = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_applet_disable_tray));
+ if (remmina_pref.disable_tray_icon != b) {
+ remmina_pref.disable_tray_icon = b;
+ rebuild_remmina_icon = TRUE;
+ }
+ if (b)
+ b = FALSE;
+ else
+ b = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_applet_start_in_tray));
+ remmina_icon_set_autostart(b);
+
+ if (rebuild_remmina_icon) {
+ remmina_icon_init();
+ remmina_icon_populate_menu();
+ }
+
+ remmina_pref.hostkey = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_host_key));
+ remmina_pref.shortcutkey_fullscreen = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_fullscreen));
+ remmina_pref.shortcutkey_autofit = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_auto_fit));
+ remmina_pref.shortcutkey_prevtab = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_switch_tab_left));
+ remmina_pref.shortcutkey_nexttab = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_switch_tab_right));
+ remmina_pref.shortcutkey_scale = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_scaled));
+ remmina_pref.shortcutkey_clipboard = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_clipboard));
+ remmina_pref.shortcutkey_multimon = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_multimon));
+ remmina_pref.shortcutkey_grab = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_grab_keyboard));
+ remmina_pref.shortcutkey_screenshot = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_screenshot));
+ remmina_pref.shortcutkey_viewonly = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_viewonly));
+ remmina_pref.shortcutkey_minimize = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_minimize));
+ remmina_pref.shortcutkey_disconnect = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_disconnect));
+ remmina_pref.shortcutkey_toolbar = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_toolbar));
+
+ g_free(remmina_pref.vte_font);
+ if (gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_terminal_font_system)))
+ remmina_pref.vte_font = NULL;
+ else
+ remmina_pref.vte_font = g_strdup(gtk_font_chooser_get_font(GTK_FONT_CHOOSER(remmina_pref_dialog->fontbutton_terminal_font)));
+ remmina_pref.vte_allow_bold_text = gtk_switch_get_active(GTK_SWITCH(remmina_pref_dialog->switch_terminal_bold));
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_foreground), &color);
+ remmina_pref.color_pref.foreground = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_background), &color);
+ remmina_pref.color_pref.background = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_cursor), &color);
+ remmina_pref.color_pref.cursor = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_cursor_foreground), &color);
+ remmina_pref.color_pref.cursor_foreground = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_highlight), &color);
+ remmina_pref.color_pref.highlight = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_highlight_foreground), &color);
+ remmina_pref.color_pref.highlight_foreground = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_colorBD), &color);
+ remmina_pref.color_pref.colorBD = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color0), &color);
+ remmina_pref.color_pref.color0 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color1), &color);
+ remmina_pref.color_pref.color1 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color2), &color);
+ remmina_pref.color_pref.color2 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color3), &color);
+ remmina_pref.color_pref.color3 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color4), &color);
+ remmina_pref.color_pref.color4 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color5), &color);
+ remmina_pref.color_pref.color5 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color6), &color);
+ remmina_pref.color_pref.color6 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color7), &color);
+ remmina_pref.color_pref.color7 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color8), &color);
+ remmina_pref.color_pref.color8 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color9), &color);
+ remmina_pref.color_pref.color9 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color10), &color);
+ remmina_pref.color_pref.color10 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color11), &color);
+ remmina_pref.color_pref.color11 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color12), &color);
+ remmina_pref.color_pref.color12 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color13), &color);
+ remmina_pref.color_pref.color13 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color14), &color);
+ remmina_pref.color_pref.color14 = gdk_rgba_to_string(&color);
+ gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color15), &color);
+ remmina_pref.color_pref.color15 = gdk_rgba_to_string(&color);
+ remmina_pref.vte_lines = atoi(gtk_entry_get_text(remmina_pref_dialog->entry_scrollback_lines));
+ remmina_pref.vte_shortcutkey_copy = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_copy));
+ remmina_pref.vte_shortcutkey_paste = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_paste));
+ remmina_pref.vte_shortcutkey_select_all = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_select_all));
+ remmina_pref.vte_shortcutkey_increase_font = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_increase_font));
+ remmina_pref.vte_shortcutkey_decrease_font = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_decrease_font));
+ remmina_pref.vte_shortcutkey_search_text = remmina_key_chooser_get_keyval(gtk_button_get_label(remmina_pref_dialog->button_keyboard_search_text));
+
+ remmina_pref_save();
+ remmina_pref_init();
+
+ remmina_pref_dialog->dialog = NULL;
+}
+
+static gboolean remmina_pref_dialog_add_pref_plugin(gchar *name, RemminaPlugin *plugin, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ RemminaPrefPlugin *pref_plugin;
+ GtkWidget *vbox;
+ GtkWidget *widget;
+
+ pref_plugin = (RemminaPrefPlugin *)plugin;
+
+ widget = gtk_label_new(pref_plugin->pref_label);
+ gtk_widget_set_halign(widget, GTK_ALIGN_START);
+ gtk_widget_show(widget);
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_show(vbox);
+ gtk_notebook_append_page(GTK_NOTEBOOK(remmina_pref_dialog->notebook_preferences), vbox, widget);
+
+ widget = pref_plugin->get_pref_body(pref_plugin);
+ gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, FALSE, 0);
+
+ return FALSE;
+}
+
+void remmina_pref_dialog_vte_font_on_toggled(GtkSwitch *widget, RemminaPrefDialog *dialog)
+{
+ TRACE_CALL(__func__);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->fontbutton_terminal_font), !gtk_switch_get_active(widget));
+}
+
+void remmina_pref_dialog_disable_tray_icon_on_toggled(GtkWidget *widget, RemminaPrefDialog *dialog)
+{
+ TRACE_CALL(__func__);
+ gboolean b;
+
+ b = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
+
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->checkbutton_applet_start_in_tray), b);
+}
+
+/* Helper function for remmina_pref_dialog_init() */
+static void remmina_pref_dialog_set_button_label(GtkButton *button, guint keyval)
+{
+ gchar *val;
+
+ val = remmina_key_chooser_get_value(keyval, 0);
+ gtk_button_set_label(button, val);
+ g_free(val);
+}
+
+/* Remmina preferences initialization */
+static void remmina_pref_dialog_init(void)
+{
+ TRACE_CALL(__func__);
+ gchar buf[100];
+ GdkRGBA color;
+
+#if !defined (HAVE_LIBSSH) || !defined (HAVE_LIBVTE)
+ GtkWidget *align;
+#endif
+
+#if !defined (HAVE_LIBVTE)
+ align = GTK_WIDGET(GET_OBJECT("alignment_terminal"));
+ gtk_widget_set_sensitive(align, FALSE);
+#endif
+
+#if !defined (HAVE_LIBSSH)
+ align = GTK_WIDGET(GET_OBJECT("alignment_ssh"));
+ gtk_widget_set_sensitive(align, FALSE);
+#endif
+
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_options_remember_last_view_mode), remmina_pref.save_view_mode);
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_options_confirm_close), remmina_pref.confirm_close);
+#if SODIUM_VERSION_INT >= 90200
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_security_use_primary_password), remmina_pref.use_primary_password);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->switch_security_use_primary_password), TRUE);
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_security_lock_connect), remmina_pref.lock_connect);
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_security_lock_edit), remmina_pref.lock_edit);
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_security_lock_view_passwords), remmina_pref.lock_view_passwords);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->unlock_timeout), TRUE);
+#else
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_security_use_primary_password), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->switch_security_use_primary_password), FALSE);
+ // TRANSLATORS: Do not translate libsodium, is the name of a library
+ gtk_widget_set_tooltip_text(GTK_WIDGET(remmina_pref_dialog->switch_security_use_primary_password), _("libsodium >= 1.9.0 is required to use Primary Password"));
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_security_lock_connect), FALSE);
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_security_lock_edit), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->unlock_timeout), FALSE);
+#endif
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_security_audit), remmina_pref.audit);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->switch_security_audit), TRUE);
+ if (remmina_pref.remmina_file_name != NULL)
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_file_name, remmina_pref.remmina_file_name);
+ else
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_file_name, "%G_%P_%N_%h.remmina");
+
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_security_trust_all), remmina_pref.trust_all);
+
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_options_deny_screenshot_clipboard), remmina_pref.deny_screenshot_clipboard);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_appearance_fullscreen_on_auto), remmina_pref.fullscreen_on_auto);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_appearance_show_tabs), remmina_pref.always_show_tab);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_appearance_show_notes), remmina_pref.always_show_notes);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_appearance_hide_toolbar), remmina_pref.hide_connection_toolbar);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_appearance_hide_searchbar), remmina_pref.hide_searchbar);
+
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_disable_news), remmina_pref.disable_news);
+#ifdef DISABLE_NEWS
+ gtk_widget_hide(GTK_WIDGET(remmina_pref_dialog->switch_disable_news));
+ gtk_widget_hide(GTK_WIDGET(remmina_pref_dialog->label_disable_news));
+#endif
+
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_disable_stats), remmina_pref.disable_stats);
+#ifdef DISABLE_STATS
+ gtk_widget_hide(GTK_WIDGET(remmina_pref_dialog->switch_disable_stats));
+ gtk_widget_hide(GTK_WIDGET(remmina_pref_dialog->label_disable_stats));
+#endif
+
+#ifdef DISABLE_TIP
+ gtk_widget_hide(GTK_WIDGET(remmina_pref_dialog->switch_disable_tip));
+ gtk_widget_hide(GTK_WIDGET(remmina_pref_dialog->label_disable_tip));
+#endif
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_disable_tip), remmina_pref.disable_tip);
+
+ g_snprintf(buf, sizeof(buf), "%i", remmina_pref.sshtunnel_port);
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_ssh_port, buf);
+ g_snprintf(buf, sizeof(buf), "%i", remmina_pref.ssh_tcp_keepidle);
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_ssh_tcp_keepidle, buf);
+ g_snprintf(buf, sizeof(buf), "%i", remmina_pref.ssh_tcp_keepintvl);
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_ssh_tcp_keepintvl, buf);
+ g_snprintf(buf, sizeof(buf), "%i", remmina_pref.ssh_tcp_keepcnt);
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_ssh_tcp_keepcnt, buf);
+ g_snprintf(buf, sizeof(buf), "%i", remmina_pref.ssh_tcp_usrtimeout);
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_ssh_tcp_usrtimeout, buf);
+ g_snprintf(buf, sizeof(buf), "%i", remmina_pref.auto_scroll_step);
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_scroll, buf);
+ g_snprintf(buf, sizeof(buf), "%i", remmina_pref.recent_maximum);
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_recent_items, buf);
+
+#ifdef HAVE_LIBAPPINDICATOR
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_applet_new_connection_on_top), remmina_pref.applet_new_ontop);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_applet_hide_totals), remmina_pref.applet_hide_count);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_applet_disable_tray), remmina_pref.disable_tray_icon);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_dark_theme), remmina_pref.dark_theme);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_applet_start_in_tray), remmina_icon_is_autostart());
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->checkbutton_applet_start_in_tray), !remmina_pref.disable_tray_icon);
+#else
+ remmina_pref.disable_tray_icon = TRUE;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_dark_theme), remmina_pref.dark_theme);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->checkbutton_applet_new_connection_on_top), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->checkbutton_applet_hide_totals), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->checkbutton_applet_disable_tray), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->checkbutton_applet_start_in_tray), FALSE);
+#endif
+
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_host_key, remmina_pref.hostkey);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_fullscreen, remmina_pref.shortcutkey_fullscreen);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_auto_fit, remmina_pref.shortcutkey_autofit);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_switch_tab_left, remmina_pref.shortcutkey_prevtab);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_switch_tab_right, remmina_pref.shortcutkey_nexttab);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_scaled, remmina_pref.shortcutkey_scale);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_clipboard, remmina_pref.shortcutkey_clipboard);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_multimon, remmina_pref.shortcutkey_multimon);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_grab_keyboard, remmina_pref.shortcutkey_grab);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_screenshot, remmina_pref.shortcutkey_screenshot);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_viewonly, remmina_pref.shortcutkey_viewonly);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_minimize, remmina_pref.shortcutkey_minimize);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_disconnect, remmina_pref.shortcutkey_disconnect);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_toolbar, remmina_pref.shortcutkey_toolbar);
+
+ if (!(remmina_pref.vte_font && remmina_pref.vte_font[0]))
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_terminal_font_system), TRUE);
+ if (remmina_pref.vte_font && remmina_pref.vte_font[0]) {
+ gtk_font_chooser_set_font(GTK_FONT_CHOOSER(remmina_pref_dialog->fontbutton_terminal_font), remmina_pref.vte_font);
+ } else {
+ gtk_font_chooser_set_font(GTK_FONT_CHOOSER(remmina_pref_dialog->fontbutton_terminal_font), "Monospace 12");
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->fontbutton_terminal_font), FALSE);
+ }
+ gtk_switch_set_active(GTK_SWITCH(remmina_pref_dialog->switch_terminal_bold), remmina_pref.vte_allow_bold_text);
+
+ /* Foreground color option */
+ gdk_rgba_parse(&color, remmina_pref.color_pref.foreground);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_foreground), &color);
+ /* Background color option */
+ gdk_rgba_parse(&color, remmina_pref.color_pref.background);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_background), &color);
+ /* Cursor color option */
+ gdk_rgba_parse(&color, remmina_pref.color_pref.cursor);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_cursor), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.cursor_foreground);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_cursor_foreground), &color);
+ /* Highlight color option */
+ gdk_rgba_parse(&color, remmina_pref.color_pref.highlight);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_highlight), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.highlight_foreground);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_highlight_foreground), &color);
+ /* Bold color option */
+ gdk_rgba_parse(&color, remmina_pref.color_pref.colorBD);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_colorBD), &color);
+ /* 16 colors */
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color0);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color0), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color1);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color1), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color2);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color2), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color3);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color3), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color4);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color4), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color5);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color5), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color6);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color6), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color7);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color7), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color8);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color8), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color9);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color9), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color10);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color10), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color11);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color11), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color12);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color12), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color13);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color13), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color14);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color14), &color);
+ gdk_rgba_parse(&color, remmina_pref.color_pref.color15);
+ gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(remmina_pref_dialog->colorbutton_color15), &color);
+#if defined (HAVE_LIBSSH) && defined (HAVE_LIBVTE)
+#if !VTE_CHECK_VERSION(0, 38, 0)
+ /* Disable color scheme buttons if old version of VTE */
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_cursor), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_cursor_foreground), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_highlight), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_highlight_foreground), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_colorBD), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color0), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color1), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color2), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color3), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color4), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color5), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color6), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color7), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color8), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color9), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color10), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color11), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color12), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color13), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color14), FALSE);
+ gtk_widget_set_sensitive(GTK_WIDGET(remmina_pref_dialog->colorbutton_color15), FALSE);
+#endif
+#endif
+
+ g_snprintf(buf, sizeof(buf), "%i", remmina_pref.vte_lines);
+ gtk_entry_set_text(remmina_pref_dialog->entry_scrollback_lines, buf);
+
+#if SODIUM_VERSION_INT >= 90200
+ g_snprintf(buf, sizeof(buf), "%i", remmina_pref.unlock_timeout);
+ gtk_entry_set_text(remmina_pref_dialog->unlock_timeout, buf);
+#endif
+
+ gtk_combo_box_set_active(remmina_pref_dialog->comboboxtext_options_double_click, remmina_pref.default_action);
+ gtk_combo_box_set_active(remmina_pref_dialog->comboboxtext_appearance_view_mode, remmina_pref.default_mode);
+ gtk_combo_box_set_active(remmina_pref_dialog->comboboxtext_appearance_tab_interface, remmina_pref.tab_mode);
+ gtk_combo_box_set_active(remmina_pref_dialog->comboboxtext_security_enc_method, remmina_pref.enc_mode);
+ gtk_combo_box_set_active(remmina_pref_dialog->comboboxtext_appearance_fullscreen_toolbar_visibility, remmina_pref.fullscreen_toolbar_visibility);
+ gtk_combo_box_set_active(remmina_pref_dialog->comboboxtext_options_scale_quality, remmina_pref.scale_quality);
+ gtk_combo_box_set_active(remmina_pref_dialog->comboboxtext_options_ssh_loglevel, remmina_pref.ssh_loglevel);
+ if (remmina_pref.datadir_path != NULL && strlen(remmina_pref.datadir_path) > 0)
+ gtk_file_chooser_set_filename(remmina_pref_dialog->filechooserbutton_options_datadir_path, remmina_pref.datadir_path);
+ else
+ gtk_file_chooser_set_filename(remmina_pref_dialog->filechooserbutton_options_datadir_path, remmina_file_get_datadir());
+ if (remmina_pref.remmina_file_name != NULL)
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_file_name, remmina_pref.remmina_file_name);
+ else
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_file_name, "%G_%P_%N_%h.remmina");
+
+ if (remmina_pref.screenshot_path != NULL)
+ gtk_file_chooser_set_filename(remmina_pref_dialog->filechooserbutton_options_screenshots_path, remmina_pref.screenshot_path);
+ else
+ gtk_file_chooser_set_filename(remmina_pref_dialog->filechooserbutton_options_screenshots_path, g_get_home_dir());
+ if (remmina_pref.screenshot_name != NULL)
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_screenshot_name, remmina_pref.screenshot_name);
+ else
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_screenshot_name, "remmina_%p_%h_%Y%m%d-%H%M%S");
+
+ gtk_switch_set_active(remmina_pref_dialog->switch_appearance_grab_color, remmina_pref.grab_color_switch);
+ if (remmina_pref.grab_color != NULL)
+ gtk_entry_set_text(remmina_pref_dialog->entry_grab_color, remmina_pref.grab_color);
+ else
+ gtk_entry_set_text(remmina_pref_dialog->entry_options_file_name, "#00FF00");
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(remmina_pref_dialog->checkbutton_options_ssh_parseconfig), remmina_pref.ssh_parseconfig);
+
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_copy, remmina_pref.vte_shortcutkey_copy);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_paste, remmina_pref.vte_shortcutkey_paste);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_select_all, remmina_pref.vte_shortcutkey_select_all);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_increase_font, remmina_pref.vte_shortcutkey_increase_font);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_decrease_font, remmina_pref.vte_shortcutkey_decrease_font);
+ remmina_pref_dialog_set_button_label(remmina_pref_dialog->button_keyboard_search_text, remmina_pref.vte_shortcutkey_search_text);
+
+ remmina_plugin_manager_for_each_plugin(REMMINA_PLUGIN_TYPE_PREF, remmina_pref_dialog_add_pref_plugin, remmina_pref_dialog->dialog);
+
+ g_signal_connect(G_OBJECT(remmina_pref_dialog->dialog), "destroy", G_CALLBACK(remmina_pref_on_dialog_destroy), NULL);
+
+ g_object_set_data(G_OBJECT(remmina_pref_dialog->dialog), "tag", "remmina-pref-dialog");
+ remmina_widget_pool_register(GTK_WIDGET(remmina_pref_dialog->dialog));
+}
+
+/* RemminaPrefDialog instance */
+GtkWidget *remmina_pref_dialog_new(gint default_tab, GtkWindow *parent)
+{
+ TRACE_CALL(__func__);
+ GSimpleActionGroup *actions;
+ GtkAccelGroup *accel_group = NULL;
+
+ remmina_pref_dialog = g_new0(RemminaPrefDialog, 1);
+ remmina_pref_dialog->priv = g_new0(RemminaPrefDialogPriv, 1);
+
+ remmina_pref_dialog->builder = remmina_public_gtk_builder_new_from_resource("/org/remmina/Remmina/src/../data/ui/remmina_preferences.glade");
+ remmina_pref_dialog->dialog = GTK_WIDGET(gtk_builder_get_object(remmina_pref_dialog->builder, "RemminaPrefDialog"));
+ if (parent)
+ gtk_window_set_transient_for(GTK_WINDOW(remmina_pref_dialog->dialog), parent);
+
+ remmina_pref_dialog->notebook_preferences = GTK_NOTEBOOK(GET_OBJECT("notebook_preferences"));
+
+ remmina_pref_dialog->filechooserbutton_options_datadir_path = GTK_FILE_CHOOSER(GET_OBJECT("filechooserbutton_options_datadir_path"));
+ remmina_pref_dialog->entry_options_file_name = GTK_ENTRY(GET_OBJECT("entry_options_file_name"));
+ remmina_pref_dialog->filechooserbutton_options_screenshots_path = GTK_FILE_CHOOSER(GET_OBJECT("filechooserbutton_options_screenshots_path"));
+ remmina_pref_dialog->entry_options_screenshot_name = GTK_ENTRY(GET_OBJECT("entry_options_screenshot_name"));
+ remmina_pref_dialog->switch_options_deny_screenshot_clipboard = GTK_SWITCH(GET_OBJECT("switch_options_deny_screenshot_clipboard"));
+ remmina_pref_dialog->switch_options_remember_last_view_mode = GTK_SWITCH(GET_OBJECT("switch_options_remember_last_view_mode"));
+ remmina_pref_dialog->switch_options_confirm_close = GTK_SWITCH(GET_OBJECT("switch_options_confirm_close"));
+ remmina_pref_dialog->switch_security_use_primary_password = GTK_SWITCH(GET_OBJECT("switch_security_use_primary_password"));
+ remmina_pref_dialog->unlock_timeout = GTK_ENTRY(GET_OBJECT("unlock_timeout"));
+ remmina_pref_dialog->switch_security_lock_connect = GTK_SWITCH(GET_OBJECT("switch_security_lock_connect"));
+ remmina_pref_dialog->switch_security_lock_edit = GTK_SWITCH(GET_OBJECT("switch_security_lock_edit"));
+ remmina_pref_dialog->switch_security_lock_view_passwords = GTK_SWITCH(GET_OBJECT("switch_security_lock_view_passwords"));
+ remmina_pref_dialog->comboboxtext_security_enc_method = GTK_COMBO_BOX(GET_OBJECT("comboboxtext_security_enc_method"));
+
+ remmina_pref_dialog->switch_security_audit = GTK_SWITCH(GET_OBJECT("switch_security_audit"));
+ remmina_pref_dialog->switch_security_trust_all = GTK_SWITCH(GET_OBJECT("switch_security_trust_all"));
+ remmina_pref_dialog->checkbutton_options_save_settings = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_options_save_settings"));
+ remmina_pref_dialog->checkbutton_appearance_fullscreen_on_auto = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_appearance_fullscreen_on_auto"));
+ remmina_pref_dialog->checkbutton_appearance_show_tabs = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_appearance_show_tabs"));
+ remmina_pref_dialog->checkbutton_appearance_show_notes = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_appearance_show_notes"));
+ remmina_pref_dialog->checkbutton_appearance_hide_toolbar = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_appearance_hide_toolbar"));
+ remmina_pref_dialog->checkbutton_appearance_hide_searchbar = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_appearance_hide_searchbar"));
+
+ remmina_pref_dialog->switch_disable_news = GTK_SWITCH(GET_OBJECT("switch_disable_news"));
+ remmina_pref_dialog->switch_disable_stats = GTK_SWITCH(GET_OBJECT("switch_disable_stats"));
+ remmina_pref_dialog->switch_disable_tip = GTK_SWITCH(GET_OBJECT("switch_disable_tip"));
+ remmina_pref_dialog->label_disable_news = GTK_LABEL(GET_OBJECT("remmina_info_disable_news_label"));
+ remmina_pref_dialog->label_disable_stats = GTK_LABEL(GET_OBJECT("remmina_info_disable_stats_label"));
+ remmina_pref_dialog->label_disable_tip = GTK_LABEL(GET_OBJECT("remmina_info_disable_tip"));
+
+ remmina_pref_dialog->comboboxtext_options_double_click = GTK_COMBO_BOX(GET_OBJECT("comboboxtext_options_double_click"));
+ remmina_pref_dialog->comboboxtext_appearance_view_mode = GTK_COMBO_BOX(GET_OBJECT("comboboxtext_appearance_view_mode"));
+ remmina_pref_dialog->comboboxtext_appearance_tab_interface = GTK_COMBO_BOX(GET_OBJECT("comboboxtext_appearance_tab_interface"));
+ remmina_pref_dialog->comboboxtext_appearance_fullscreen_toolbar_visibility = GTK_COMBO_BOX(GET_OBJECT("comboboxtext_appearance_fullscreen_toolbar_visibility"));
+ remmina_pref_dialog->comboboxtext_options_scale_quality = GTK_COMBO_BOX(GET_OBJECT("comboboxtext_options_scale_quality"));
+ remmina_pref_dialog->checkbutton_options_ssh_parseconfig = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_options_ssh_parseconfig"));
+ remmina_pref_dialog->comboboxtext_options_ssh_loglevel = GTK_COMBO_BOX(GET_OBJECT("comboboxtext_options_ssh_loglevel"));
+ remmina_pref_dialog->entry_options_ssh_port = GTK_ENTRY(GET_OBJECT("entry_options_ssh_port"));
+ remmina_pref_dialog->entry_options_ssh_tcp_keepidle = GTK_ENTRY(GET_OBJECT("entry_options_ssh_tcp_keepidle"));
+ remmina_pref_dialog->entry_options_ssh_tcp_keepintvl = GTK_ENTRY(GET_OBJECT("entry_options_ssh_tcp_keepintvl"));
+ remmina_pref_dialog->entry_options_ssh_tcp_keepcnt = GTK_ENTRY(GET_OBJECT("entry_options_ssh_tcp_keepcnt"));
+ remmina_pref_dialog->entry_options_ssh_tcp_usrtimeout = GTK_ENTRY(GET_OBJECT("entry_options_ssh_tcp_usrtimeout"));
+ remmina_pref_dialog->entry_options_scroll = GTK_ENTRY(GET_OBJECT("entry_options_scroll"));
+ remmina_pref_dialog->entry_options_recent_items = GTK_ENTRY(GET_OBJECT("entry_options_recent_items"));
+ remmina_pref_dialog->entry_grab_color = GTK_ENTRY(GET_OBJECT("entry_grab_color"));
+ remmina_pref_dialog->switch_appearance_grab_color = GTK_SWITCH(GET_OBJECT("switch_appearance_grab_color"));
+ remmina_pref_dialog->button_options_recent_items_clear = GTK_BUTTON(GET_OBJECT("button_options_recent_items_clear"));
+
+ remmina_pref_dialog->checkbutton_applet_new_connection_on_top = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_applet_new_connection_on_top"));
+ remmina_pref_dialog->checkbutton_applet_hide_totals = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_applet_hide_totals"));
+ remmina_pref_dialog->checkbutton_applet_disable_tray = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_applet_disable_tray"));
+ remmina_pref_dialog->checkbutton_dark_theme = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_dark_theme"));
+ remmina_pref_dialog->checkbutton_applet_start_in_tray = GTK_CHECK_BUTTON(GET_OBJECT("checkbutton_applet_start_in_tray"));
+
+ remmina_pref_dialog->button_keyboard_host_key = GTK_BUTTON(GET_OBJECT("button_keyboard_host_key"));
+ remmina_pref_dialog->button_keyboard_fullscreen = GTK_BUTTON(GET_OBJECT("button_keyboard_fullscreen"));
+ remmina_pref_dialog->button_keyboard_auto_fit = GTK_BUTTON(GET_OBJECT("button_keyboard_auto_fit"));
+ remmina_pref_dialog->button_keyboard_switch_tab_left = GTK_BUTTON(GET_OBJECT("button_keyboard_switch_tab_left"));
+ remmina_pref_dialog->button_keyboard_switch_tab_right = GTK_BUTTON(GET_OBJECT("button_keyboard_switch_tabright"));
+ remmina_pref_dialog->button_keyboard_scaled = GTK_BUTTON(GET_OBJECT("button_keyboard_scaled"));
+ remmina_pref_dialog->button_keyboard_clipboard = GTK_BUTTON(GET_OBJECT("button_keyboard_clipboard"));
+ remmina_pref_dialog->button_keyboard_grab_keyboard = GTK_BUTTON(GET_OBJECT("button_keyboard_grab_keyboard"));
+ remmina_pref_dialog->button_keyboard_multimon = GTK_BUTTON(GET_OBJECT("button_keyboard_multimon"));
+ remmina_pref_dialog->button_keyboard_screenshot = GTK_BUTTON(GET_OBJECT("button_keyboard_screenshot"));
+ remmina_pref_dialog->button_keyboard_viewonly = GTK_BUTTON(GET_OBJECT("button_keyboard_viewonly"));
+ remmina_pref_dialog->button_keyboard_minimize = GTK_BUTTON(GET_OBJECT("button_keyboard_minimize"));
+ remmina_pref_dialog->button_keyboard_disconnect = GTK_BUTTON(GET_OBJECT("button_keyboard_disconnect"));
+ remmina_pref_dialog->button_keyboard_toolbar = GTK_BUTTON(GET_OBJECT("button_keyboard_toolbar"));
+
+ remmina_pref_dialog->switch_terminal_font_system = GTK_SWITCH(GET_OBJECT("switch_terminal_font_system"));
+ remmina_pref_dialog->fontbutton_terminal_font = GTK_FONT_BUTTON(GET_OBJECT("fontbutton_terminal_font"));
+ remmina_pref_dialog->switch_terminal_bold = GTK_SWITCH(GET_OBJECT("switch_terminal_bold"));
+ remmina_pref_dialog->entry_scrollback_lines = GTK_ENTRY(GET_OBJECT("entry_scrollback_lines"));
+ remmina_pref_dialog->button_keyboard_copy = GTK_BUTTON(GET_OBJECT("button_keyboard_copy"));
+ remmina_pref_dialog->button_keyboard_paste = GTK_BUTTON(GET_OBJECT("button_keyboard_paste"));
+ remmina_pref_dialog->button_keyboard_select_all = GTK_BUTTON(GET_OBJECT("button_keyboard_select_all"));
+ remmina_pref_dialog->button_keyboard_increase_font = GTK_BUTTON(GET_OBJECT("button_keyboard_increase_font"));
+ remmina_pref_dialog->button_keyboard_decrease_font = GTK_BUTTON(GET_OBJECT("button_keyboard_decrease_font"));
+ remmina_pref_dialog->button_keyboard_search_text = GTK_BUTTON(GET_OBJECT("button_keyboard_search_text"));
+ remmina_pref_dialog->label_terminal_foreground = GTK_LABEL(GET_OBJECT("label_terminal_foreground"));
+ remmina_pref_dialog->colorbutton_foreground = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_foreground"));
+ remmina_pref_dialog->label_terminal_background = GTK_LABEL(GET_OBJECT("label_terminal_background"));
+ remmina_pref_dialog->colorbutton_background = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_background"));
+ remmina_pref_dialog->label_terminal_cursor_color = GTK_LABEL(GET_OBJECT("label_terminal_cursor_color"));
+ remmina_pref_dialog->colorbutton_cursor = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_cursor"));
+ remmina_pref_dialog->colorbutton_cursor_foreground = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_cursor_foreground"));
+ remmina_pref_dialog->colorbutton_highlight = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_highlight"));
+ remmina_pref_dialog->colorbutton_highlight_foreground = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_highlight_foreground"));
+ remmina_pref_dialog->label_terminal_bold_color = GTK_LABEL(GET_OBJECT("label_terminal_bold_color"));
+ remmina_pref_dialog->colorbutton_colorBD = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_colorBD"));
+ remmina_pref_dialog->label_terminal_normal_colors = GTK_LABEL(GET_OBJECT("label_terminal_normal_colors"));
+ remmina_pref_dialog->colorbutton_color0 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color0"));
+ remmina_pref_dialog->colorbutton_color1 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color1"));
+ remmina_pref_dialog->colorbutton_color2 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color2"));
+ remmina_pref_dialog->colorbutton_color3 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color3"));
+ remmina_pref_dialog->colorbutton_color4 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color4"));
+ remmina_pref_dialog->colorbutton_color5 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color5"));
+ remmina_pref_dialog->colorbutton_color6 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color6"));
+ remmina_pref_dialog->colorbutton_color7 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color7"));
+ remmina_pref_dialog->label_terminal_bright_colors = GTK_LABEL(GET_OBJECT("label_terminal_bright_colors"));
+ remmina_pref_dialog->colorbutton_color8 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color8"));
+ remmina_pref_dialog->colorbutton_color9 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color9"));
+ remmina_pref_dialog->colorbutton_color10 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color10"));
+ remmina_pref_dialog->colorbutton_color11 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color11"));
+ remmina_pref_dialog->colorbutton_color12 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color12"));
+ remmina_pref_dialog->colorbutton_color13 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color13"));
+ remmina_pref_dialog->colorbutton_color14 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color14"));
+ remmina_pref_dialog->colorbutton_color15 = GTK_COLOR_BUTTON(GET_OBJECT("colorbutton_color15"));
+#if defined (HAVE_LIBSSH) && defined (HAVE_LIBVTE)
+#if VTE_CHECK_VERSION(0, 38, 0)
+ const gchar *remmina_dir;
+ gchar *destpath;
+ remmina_dir = g_build_path("/", g_get_user_config_dir(), "remmina", NULL);
+ destpath = g_strdup_printf("%s/remmina.colors", remmina_dir);
+ remmina_pref_dialog->button_term_cs = GTK_FILE_CHOOSER(GET_OBJECT("button_term_cs"));
+ const gchar *fc_tooltip_text = g_strconcat(_("Picking a terminal colouring file replaces the file: "),
+ "\n",
+ destpath,
+ "\n",
+ _("This file contains the “Custom” terminal colour scheme selectable from the “Advanced” tab of terminal connections and editable in the “Terminal” tab in the settings."),
+ NULL);
+ gtk_widget_set_tooltip_text(GTK_WIDGET(remmina_pref_dialog->button_term_cs), fc_tooltip_text);
+ gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(remmina_pref_dialog->button_term_cs), REMMINA_RUNTIME_TERM_CS_DIR);
+ g_free(destpath);
+#endif
+#endif
+ /* Non widget objects */
+ actions = g_simple_action_group_new();
+ g_action_map_add_action_entries(G_ACTION_MAP(actions), pref_actions, G_N_ELEMENTS(pref_actions), remmina_pref_dialog->dialog);
+ gtk_widget_insert_action_group(GTK_WIDGET(remmina_pref_dialog->dialog), "pref", G_ACTION_GROUP(actions));
+ g_action_map_add_action_entries(G_ACTION_MAP(actions), pref_actions, G_N_ELEMENTS(pref_actions), remmina_pref_dialog->dialog);
+ g_object_unref(actions);
+ /* Accelerators */
+ accel_group = gtk_accel_group_new();
+ gtk_window_add_accel_group(GTK_WINDOW(remmina_pref_dialog->dialog), accel_group);
+ gtk_accel_group_connect(accel_group, GDK_KEY_Q, GDK_CONTROL_MASK, 0,
+ g_cclosure_new_swap(G_CALLBACK(remmina_pref_dialog_on_action_close), NULL, NULL));
+
+ /* Connect signals */
+ gtk_builder_connect_signals(remmina_pref_dialog->builder, NULL);
+ /* Initialize the window and load the preferences */
+ remmina_pref_dialog_init();
+
+ if (default_tab > 0)
+ gtk_notebook_set_current_page(remmina_pref_dialog->notebook_preferences, default_tab);
+ return remmina_pref_dialog->dialog;
+}
+
+GtkWidget *remmina_pref_dialog_get_dialog()
+{
+ if (!remmina_pref_dialog)
+ return NULL;
+ return remmina_pref_dialog->dialog;
+}
diff --git a/src/remmina_pref_dialog.h b/src/remmina_pref_dialog.h
new file mode 100644
index 0000000..04af8c9
--- /dev/null
+++ b/src/remmina_pref_dialog.h
@@ -0,0 +1,180 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-2011 Vic Lee
+ * Copyright (C) 2017-2022 Antenore Gatta, Giovanni Panozzo
+ * Copyright (C) 2022-2023 Antenore Gatta, Giovanni Panozzo, Hiroyuki Tanaka
+ * 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.
+ *
+ */
+
+#pragma once
+#include <gtk/gtk.h>
+
+/*
+ * Remmina Preferences Dialog
+ */
+
+typedef struct _RemminaPrefDialogPriv {
+ GtkWidget *resolutions_list;
+} RemminaPrefDialogPriv;
+
+typedef struct _RemminaPrefDialog {
+ GtkBuilder * builder;
+ GtkWidget * dialog;
+ GtkNotebook * notebook_preferences;
+
+ GtkFileChooser * filechooserbutton_options_datadir_path;
+ GtkEntry * entry_options_file_name;
+ GtkFileChooser * filechooserbutton_options_screenshots_path;
+ GtkEntry * entry_options_screenshot_name;
+ GtkSwitch * switch_appearance_grab_color;
+ GtkSwitch * switch_options_deny_screenshot_clipboard;
+ GtkSwitch * switch_options_remember_last_view_mode;
+ GtkSwitch * switch_options_confirm_close;
+ GtkSwitch * switch_security_use_primary_password;
+ GtkEntry * unlock_timeout;
+ GtkSwitch * switch_security_lock_connect;
+ GtkSwitch * switch_security_lock_edit;
+ GtkSwitch * switch_security_lock_view_passwords;
+ GtkSwitch * switch_security_audit;
+ GtkSwitch * switch_security_trust_all;
+ GtkSwitch * switch_disable_news;
+ GtkSwitch * switch_disable_stats;
+ GtkSwitch * switch_disable_tip;
+ GtkLabel * label_disable_news;
+ GtkLabel * label_disable_stats;
+ GtkLabel * label_disable_tip;
+ GtkCheckButton * checkbutton_options_save_settings;
+ GtkCheckButton * checkbutton_appearance_fullscreen_on_auto;
+ GtkCheckButton * checkbutton_appearance_show_tabs;
+ GtkCheckButton * checkbutton_appearance_show_notes;
+ GtkCheckButton * checkbutton_appearance_hide_toolbar;
+ GtkCheckButton * checkbutton_appearance_hide_searchbar;
+ GtkComboBox * comboboxtext_options_double_click;
+ GtkComboBox * comboboxtext_appearance_view_mode;
+ GtkComboBox * comboboxtext_appearance_tab_interface;
+ GtkComboBox * comboboxtext_options_scale_quality;
+ GtkComboBox * comboboxtext_options_ssh_loglevel;
+ GtkComboBox * comboboxtext_appearance_fullscreen_toolbar_visibility;
+ GtkComboBox * comboboxtext_security_enc_method;
+ GtkCheckButton * checkbutton_options_ssh_parseconfig;
+ GtkEntry * entry_options_ssh_port;
+ GtkEntry * entry_options_ssh_tcp_keepidle;
+ GtkEntry * entry_options_ssh_tcp_keepintvl;
+ GtkEntry * entry_options_ssh_tcp_keepcnt;
+ GtkEntry * entry_options_ssh_tcp_usrtimeout;
+ GtkEntry * entry_options_scroll;
+ GtkEntry * entry_options_recent_items;
+ GtkEntry * entry_grab_color;
+ GtkButton * button_options_recent_items_clear;
+ GtkButton * button_options_resolutions;
+
+ GtkCheckButton * checkbutton_applet_new_connection_on_top;
+ GtkCheckButton * checkbutton_applet_hide_totals;
+ GtkCheckButton * checkbutton_applet_disable_tray;
+ GtkCheckButton * checkbutton_dark_theme;
+ GtkCheckButton * checkbutton_applet_start_in_tray;
+
+ GtkButton * button_keyboard_host_key;
+ GtkButton * button_keyboard_fullscreen;
+ GtkButton * button_keyboard_auto_fit;
+ GtkButton * button_keyboard_switch_tab_left;
+ GtkButton * button_keyboard_switch_tab_right;
+ GtkButton * button_keyboard_scaled;
+ GtkButton * button_keyboard_clipboard;
+ GtkButton * button_keyboard_multimon;
+ GtkButton * button_keyboard_grab_keyboard;
+ GtkButton * button_keyboard_screenshot;
+ GtkButton * button_keyboard_viewonly;
+ GtkButton * button_keyboard_minimize;
+ GtkButton * button_keyboard_disconnect;
+ GtkButton * button_keyboard_toolbar;
+
+ GtkSwitch * switch_terminal_font_system;
+ GtkFontButton * fontbutton_terminal_font;
+ GtkSwitch * switch_terminal_bold;
+ GtkLabel * label_terminal_foreground;
+ GtkColorButton * colorbutton_foreground;
+ GtkLabel * label_terminal_background;
+ GtkColorButton * colorbutton_background;
+ GtkEntry * entry_scrollback_lines;
+ GtkButton * button_keyboard_copy;
+ GtkButton * button_keyboard_paste;
+ GtkButton * button_keyboard_select_all;
+ GtkButton * button_keyboard_increase_font;
+ GtkButton * button_keyboard_decrease_font;
+ GtkButton * button_keyboard_search_text;
+ GtkLabel * label_terminal_cursor_color;
+ GtkLabel * label_terminal_bold_color;
+ GtkLabel * label_terminal_normal_colors;
+ GtkLabel * label_terminal_bright_colors;
+ GtkColorButton * colorbutton_cursor;
+ GtkColorButton * colorbutton_cursor_foreground;
+ GtkColorButton * colorbutton_highlight;
+ GtkColorButton * colorbutton_highlight_foreground;
+ GtkColorButton * colorbutton_colorBD;
+ GtkColorButton * colorbutton_color0;
+ GtkColorButton * colorbutton_color1;
+ GtkColorButton * colorbutton_color2;
+ GtkColorButton * colorbutton_color3;
+ GtkColorButton * colorbutton_color4;
+ GtkColorButton * colorbutton_color5;
+ GtkColorButton * colorbutton_color6;
+ GtkColorButton * colorbutton_color7;
+ GtkColorButton * colorbutton_color8;
+ GtkColorButton * colorbutton_color9;
+ GtkColorButton * colorbutton_color10;
+ GtkColorButton * colorbutton_color11;
+ GtkColorButton * colorbutton_color12;
+ GtkColorButton * colorbutton_color13;
+ GtkColorButton * colorbutton_color14;
+ GtkColorButton * colorbutton_color15;
+ GtkFileChooser * button_term_cs;
+
+ RemminaPrefDialogPriv * priv;
+} RemminaPrefDialog;
+
+enum {
+ REMMINA_PREF_OPTIONS_TAB = 0,
+ REMMINA_PREF_APPEARANCE = 1,
+ REMMINA_PREF_APPLET_TAB = 2
+};
+
+G_BEGIN_DECLS
+
+/* RemminaPrefDialog instance */
+GtkWidget *remmina_pref_dialog_new(gint default_tab, GtkWindow *parent);
+/* Get the current PrefDialog or NULL if not initialized */
+GtkWidget *remmina_pref_dialog_get_dialog(void);
+void remmina_prefdiag_unlock_repwd_on_changed(GtkEditable *editable, RemminaPrefDialog *dialog);
+void remmina_pref_dialog_on_action_close(GSimpleAction *action, GVariant *param, gpointer data);
+
+G_END_DECLS
diff --git a/src/remmina_protocol_widget.c b/src/remmina_protocol_widget.c
new file mode 100644
index 0000000..7becda0
--- /dev/null
+++ b/src/remmina_protocol_widget.c
@@ -0,0 +1,2226 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 "config.h"
+
+#include <gtk/gtk.h>
+#include <gtk/gtkx.h>
+#include <glib/gi18n.h>
+#include <gmodule.h>
+#include <stdlib.h>
+
+#include "remmina_chat_window.h"
+#include "remmina_masterthread_exec.h"
+#include "remmina_ext_exec.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_pref.h"
+#include "remmina_protocol_widget.h"
+#include "remmina_public.h"
+#include "remmina_ssh.h"
+#include "remmina_log.h"
+#include "remmina/remmina_trace_calls.h"
+
+#ifdef GDK_WINDOWING_WAYLAND
+#include <gdk/gdkwayland.h>
+#endif
+
+struct _RemminaProtocolWidgetPriv {
+ RemminaFile * remmina_file;
+ RemminaProtocolPlugin * plugin;
+ RemminaProtocolFeature *features;
+
+ gint width;
+ gint height;
+ RemminaScaleMode scalemode;
+ gboolean scaler_expand;
+
+ gboolean has_error;
+ gchar * error_message;
+ gboolean user_disconnect;
+ /* ssh_tunnels is an array of RemminaSSHTunnel*
+ * the 1st one is the "main" tunnel, other tunnels are used for example in sftp commands */
+ GPtrArray * ssh_tunnels;
+ RemminaTunnelInitFunc init_func;
+
+ GtkWidget * chat_window;
+
+ gboolean closed;
+
+ RemminaHostkeyFunc hostkey_func;
+
+ gint profile_remote_width;
+ gint profile_remote_height;
+ gint multimon;
+
+ RemminaMessagePanel * connect_message_panel;
+ RemminaMessagePanel * listen_message_panel;
+ RemminaMessagePanel * auth_message_panel;
+ RemminaMessagePanel * retry_message_panel;
+
+ /* Data saved from the last message_panel when the user confirm */
+ gchar * username;
+ gchar * password;
+ gchar * domain;
+ gboolean save_password;
+
+ gchar * cacert;
+ gchar * cacrl;
+ gchar * clientcert;
+ gchar * clientkey;
+};
+
+enum panel_type {
+ RPWDT_AUTH,
+ RPWDT_QUESTIONYESNO,
+ RPWDT_AUTHX509
+};
+
+G_DEFINE_TYPE(RemminaProtocolWidget, remmina_protocol_widget, GTK_TYPE_EVENT_BOX)
+
+enum {
+ CONNECT_SIGNAL,
+ DISCONNECT_SIGNAL,
+ DESKTOP_RESIZE_SIGNAL,
+ UPDATE_ALIGN_SIGNAL,
+ LOCK_DYNRES_SIGNAL,
+ UNLOCK_DYNRES_SIGNAL,
+ LAST_SIGNAL
+};
+
+typedef struct _RemminaProtocolWidgetSignalData {
+ RemminaProtocolWidget * gp;
+ const gchar * signal_name;
+} RemminaProtocolWidgetSignalData;
+
+static guint remmina_protocol_widget_signals[LAST_SIGNAL] =
+{ 0 };
+
+static void remmina_protocol_widget_class_init(RemminaProtocolWidgetClass *klass)
+{
+ TRACE_CALL(__func__);
+ remmina_protocol_widget_signals[CONNECT_SIGNAL] = g_signal_new("connect", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaProtocolWidgetClass, connect), NULL, NULL,
+ g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+ remmina_protocol_widget_signals[DISCONNECT_SIGNAL] = g_signal_new("disconnect", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaProtocolWidgetClass, disconnect), NULL, NULL,
+ g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+ remmina_protocol_widget_signals[DESKTOP_RESIZE_SIGNAL] = g_signal_new("desktop-resize", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaProtocolWidgetClass, desktop_resize), NULL, NULL,
+ g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+ remmina_protocol_widget_signals[UPDATE_ALIGN_SIGNAL] = g_signal_new("update-align", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaProtocolWidgetClass, update_align), NULL, NULL,
+ g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+ remmina_protocol_widget_signals[LOCK_DYNRES_SIGNAL] = g_signal_new("lock-dynres", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaProtocolWidgetClass, lock_dynres), NULL, NULL,
+ g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+ remmina_protocol_widget_signals[UNLOCK_DYNRES_SIGNAL] = g_signal_new("unlock-dynres", G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(RemminaProtocolWidgetClass, unlock_dynres), NULL, NULL,
+ g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
+}
+
+
+static void remmina_protocol_widget_close_all_tunnels(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ int i;
+
+ if (gp->priv && gp->priv->ssh_tunnels) {
+ for (i = 0; i < gp->priv->ssh_tunnels->len; i++) {
+#ifdef HAVE_LIBSSH
+ remmina_ssh_tunnel_free((RemminaSSHTunnel *)gp->priv->ssh_tunnels->pdata[i]);
+#else
+ REMMINA_DEBUG("LibSSH support turned off, no need to free SSH tunnel data");
+#endif
+ }
+ g_ptr_array_set_size(gp->priv->ssh_tunnels, 0);
+ }
+}
+
+
+static void remmina_protocol_widget_destroy(RemminaProtocolWidget *gp, gpointer data)
+{
+ TRACE_CALL(__func__);
+
+ g_free(gp->priv->username);
+ gp->priv->username = NULL;
+
+ g_free(gp->priv->password);
+ gp->priv->password = NULL;
+
+ g_free(gp->priv->domain);
+ gp->priv->domain = NULL;
+
+ g_free(gp->priv->cacert);
+ gp->priv->cacert = NULL;
+
+ g_free(gp->priv->cacrl);
+ gp->priv->cacrl = NULL;
+
+ g_free(gp->priv->clientcert);
+ gp->priv->clientcert = NULL;
+
+ g_free(gp->priv->clientkey);
+ gp->priv->clientkey = NULL;
+
+ g_free(gp->priv->features);
+ gp->priv->features = NULL;
+
+ g_free(gp->priv->error_message);
+ gp->priv->error_message = NULL;
+
+ g_free(gp->priv->remmina_file);
+ gp->priv->remmina_file = NULL;
+
+ g_free(gp->priv);
+ gp->priv = NULL;
+
+ remmina_protocol_widget_close_all_tunnels(gp);
+
+ if (gp->priv && gp->priv->ssh_tunnels) {
+ g_ptr_array_free(gp->priv->ssh_tunnels, TRUE);
+ gp->priv->ssh_tunnels = NULL;
+ }
+}
+
+void remmina_protocol_widget_grab_focus(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *child;
+
+ child = gtk_bin_get_child(GTK_BIN(gp));
+
+ if (child) {
+ gtk_widget_set_can_focus(child, TRUE);
+ gtk_widget_grab_focus(child);
+ }
+}
+
+static void remmina_protocol_widget_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidgetPriv *priv;
+
+ priv = g_new0(RemminaProtocolWidgetPriv, 1);
+ gp->priv = priv;
+ gp->priv->user_disconnect = FALSE;
+ gp->priv->closed = TRUE;
+ gp->priv->ssh_tunnels = g_ptr_array_new();
+
+ g_signal_connect(G_OBJECT(gp), "destroy", G_CALLBACK(remmina_protocol_widget_destroy), NULL);
+}
+
+void remmina_protocol_widget_open_connection_real(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(data);
+
+ REMMINA_DEBUG("Opening connection");
+
+ RemminaProtocolPlugin *plugin;
+ RemminaProtocolFeature *feature;
+ gint num_plugin;
+ gint num_ssh;
+
+ gp->priv->closed = FALSE;
+
+ plugin = gp->priv->plugin;
+ plugin->init(gp);
+
+ for (num_plugin = 0, feature = (RemminaProtocolFeature *)plugin->features; feature && feature->type; num_plugin++, feature++) {
+ }
+
+ num_ssh = 0;
+#ifdef HAVE_LIBSSH
+ if (remmina_file_get_int(gp->priv->remmina_file, "ssh_tunnel_enabled", FALSE))
+ num_ssh += 2;
+
+#endif
+ if (num_plugin + num_ssh == 0) {
+ gp->priv->features = NULL;
+ } else {
+ gp->priv->features = g_new0(RemminaProtocolFeature, num_plugin + num_ssh + 1);
+ feature = gp->priv->features;
+ if (plugin->features) {
+ memcpy(feature, plugin->features, sizeof(RemminaProtocolFeature) * num_plugin);
+ feature += num_plugin;
+ }
+#ifdef HAVE_LIBSSH
+ REMMINA_DEBUG("Have SSH");
+ if (num_ssh) {
+ feature->type = REMMINA_PROTOCOL_FEATURE_TYPE_TOOL;
+ feature->id = REMMINA_PROTOCOL_FEATURE_TOOL_SSH;
+ feature->opt1 = _("Connect via SSH from a new terminal");
+ feature->opt1_type_hint = REMMINA_TYPEHINT_STRING;
+ feature->opt2 = "utilities-terminal";
+ feature->opt2_type_hint = REMMINA_TYPEHINT_STRING;
+ feature->opt3 = NULL;
+ feature->opt3_type_hint = REMMINA_TYPEHINT_UNDEFINED;
+ feature++;
+
+ feature->type = REMMINA_PROTOCOL_FEATURE_TYPE_TOOL;
+ feature->id = REMMINA_PROTOCOL_FEATURE_TOOL_SFTP;
+ feature->opt1 = _("Open SFTP transfer…");
+ feature->opt1_type_hint = REMMINA_TYPEHINT_STRING;
+ feature->opt2 = "folder-remote";
+ feature->opt2_type_hint = REMMINA_TYPEHINT_STRING;
+ feature->opt3 = NULL;
+ feature->opt3_type_hint = REMMINA_TYPEHINT_UNDEFINED;
+ feature++;
+ }
+ feature->type = REMMINA_PROTOCOL_FEATURE_TYPE_END;
+#endif
+ }
+
+ if (!plugin->open_connection(gp))
+ remmina_protocol_widget_close_connection(gp);
+}
+
+static void cancel_open_connection_cb(void *cbdata, int btn)
+{
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)cbdata;
+
+ remmina_protocol_widget_close_connection(gp);
+}
+
+void remmina_protocol_widget_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+ const gchar *name;
+ RemminaMessagePanel *mp;
+
+ /* Exec precommand before everything else */
+ mp = remmina_message_panel_new();
+ remmina_message_panel_setup_progress(mp, _("Executing external commands…"), NULL, NULL);
+ rco_show_message_panel(gp->cnnobj, mp);
+
+ remmina_ext_exec_new(gp->priv->remmina_file, "precommand");
+ rco_destroy_message_panel(gp->cnnobj, mp);
+
+ name = remmina_file_get_string(gp->priv->remmina_file, "name");
+ // TRANSLATORS: “%s” is a placeholder for the connection profile name
+ s = g_strdup_printf(_("Connecting to “%s”…"), (name ? name : "*"));
+
+ mp = remmina_message_panel_new();
+ remmina_message_panel_setup_progress(mp, s, cancel_open_connection_cb, gp);
+ g_free(s);
+ gp->priv->connect_message_panel = mp;
+ rco_show_message_panel(gp->cnnobj, mp);
+
+ remmina_protocol_widget_open_connection_real(gp);
+}
+
+static gboolean conn_closed_real(gpointer data, int button){
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)data;
+
+ #ifdef HAVE_LIBSSH
+ /* This will close all tunnels */
+ remmina_protocol_widget_close_all_tunnels(gp);
+#endif
+ /* Exec postcommand */
+ remmina_ext_exec_new(gp->priv->remmina_file, "postcommand");
+ /* Notify listeners (usually rcw) that the connection is closed */
+ g_signal_emit_by_name(G_OBJECT(gp), "disconnect");
+ return G_SOURCE_REMOVE;
+
+}
+
+static gboolean conn_closed(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)data;
+ int disconnect_prompt = remmina_file_get_int(gp->priv->remmina_file, "disconnect-prompt", FALSE);
+ if (!gp->priv->user_disconnect && !gp->priv->has_error && disconnect_prompt){
+ const char* msg = "Plugin Disconnected";
+ if (gp->priv->has_error){
+ msg = remmina_protocol_widget_get_error_message(gp);
+ remmina_protocol_widget_set_error(gp, NULL);
+ }
+ gp->priv->user_disconnect = FALSE;
+ RemminaMessagePanel* mp = remmina_message_panel_new();
+ remmina_message_panel_setup_message(mp, msg, (RemminaMessagePanelCallback)conn_closed_real, gp);
+ rco_show_message_panel(gp->cnnobj, mp);
+ return G_SOURCE_REMOVE;
+ }
+ else{
+ return conn_closed_real(gp, 0);
+ }
+
+}
+
+void remmina_protocol_widget_signal_connection_closed(RemminaProtocolWidget *gp)
+{
+ /* User told us that they closed the connection,
+ * or the connection was closed with a known error,
+ * add async event to main thread to complete our close tasks */
+ TRACE_CALL(__func__);
+ gp->priv->closed = TRUE;
+ g_idle_add(conn_closed, (gpointer)gp);
+}
+
+static gboolean conn_opened(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)data;
+
+#ifdef HAVE_LIBSSH
+ if (gp->priv->ssh_tunnels) {
+ for (guint i = 0; i < gp->priv->ssh_tunnels->len; i++)
+ remmina_ssh_tunnel_cancel_accept((RemminaSSHTunnel *)gp->priv->ssh_tunnels->pdata[i]);
+ }
+#endif
+ if (gp->priv->listen_message_panel) {
+ rco_destroy_message_panel(gp->cnnobj, gp->priv->listen_message_panel);
+ gp->priv->listen_message_panel = NULL;
+ }
+ if (gp->priv->connect_message_panel) {
+ rco_destroy_message_panel(gp->cnnobj, gp->priv->connect_message_panel);
+ gp->priv->connect_message_panel = NULL;
+ }
+ if (gp->priv->retry_message_panel) {
+ rco_destroy_message_panel(gp->cnnobj, gp->priv->retry_message_panel);
+ gp->priv->retry_message_panel = NULL;
+ }
+ g_signal_emit_by_name(G_OBJECT(gp), "connect");
+ return G_SOURCE_REMOVE;
+}
+
+void remmina_protocol_widget_signal_connection_opened(RemminaProtocolWidget *gp)
+{
+ /* Plugin told us that it opened the connection,
+ * add async event to main thread to complete our close tasks */
+ TRACE_CALL(__func__);
+ g_idle_add(conn_opened, (gpointer)gp);
+}
+
+static gboolean update_align(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)data;
+
+ g_signal_emit_by_name(G_OBJECT(gp), "update-align");
+ return G_SOURCE_REMOVE;
+}
+
+void remmina_protocol_widget_update_align(RemminaProtocolWidget *gp)
+{
+ /* Called by the plugin to do updates on rcw */
+ TRACE_CALL(__func__);
+ g_idle_add(update_align, (gpointer)gp);
+}
+
+static gboolean lock_dynres(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)data;
+
+ g_signal_emit_by_name(G_OBJECT(gp), "lock-dynres");
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean unlock_dynres(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)data;
+
+ g_signal_emit_by_name(G_OBJECT(gp), "unlock-dynres");
+ return G_SOURCE_REMOVE;
+}
+
+void remmina_protocol_widget_lock_dynres(RemminaProtocolWidget *gp)
+{
+ /* Called by the plugin to do updates on rcw */
+ TRACE_CALL(__func__);
+ g_idle_add(lock_dynres, (gpointer)gp);
+}
+
+void remmina_protocol_widget_unlock_dynres(RemminaProtocolWidget *gp)
+{
+ /* Called by the plugin to do updates on rcw */
+ TRACE_CALL(__func__);
+ g_idle_add(unlock_dynres, (gpointer)gp);
+}
+
+static gboolean desktop_resize(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)data;
+
+ g_signal_emit_by_name(G_OBJECT(gp), "desktop-resize");
+ return G_SOURCE_REMOVE;
+}
+
+void remmina_protocol_widget_desktop_resize(RemminaProtocolWidget *gp)
+{
+ /* Called by the plugin to do updates on rcw */
+ TRACE_CALL(__func__);
+ g_idle_add(desktop_resize, (gpointer)gp);
+}
+
+
+void remmina_protocol_widget_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ /* kindly ask the protocol plugin to close the connection.
+ * Nothing else is done here. */
+
+ if (!GTK_IS_WIDGET(gp))
+ return;
+
+ if (gp->priv->chat_window) {
+ gtk_widget_destroy(gp->priv->chat_window);
+ gp->priv->chat_window = NULL;
+ }
+
+ if (gp->priv->closed) {
+ /* Connection is already closed by the plugin, but
+ * rcw is asking to close again (usually after an error panel)
+ */
+ /* Clear the current error, or "disconnect" signal func will reshow a panel */
+ remmina_protocol_widget_set_error(gp, NULL);
+ g_signal_emit_by_name(G_OBJECT(gp), "disconnect");
+ return;
+ }
+ gp->priv->user_disconnect = TRUE;
+
+ /* Ask the plugin to close, async.
+ * The plugin will emit a "disconnect" signal on gp to call our
+ * remmina_protocol_widget_on_disconnected() when done */
+ gp->priv->plugin->close_connection(gp);
+
+ return;
+}
+
+/** Check if the plugin accepts keystrokes.
+ */
+gboolean remmina_protocol_widget_plugin_receives_keystrokes(RemminaProtocolWidget *gp)
+{
+ return gp->priv->plugin->send_keystrokes ? TRUE : FALSE;
+}
+
+/**
+ * Send to the plugin some keystrokes.
+ */
+void remmina_protocol_widget_send_keystrokes(RemminaProtocolWidget *gp, GtkMenuItem *widget)
+{
+ TRACE_CALL(__func__);
+ gchar *keystrokes = g_object_get_data(G_OBJECT(widget), "keystrokes");
+ guint *keyvals;
+ gint i;
+ GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
+ gunichar character;
+ guint keyval;
+ GdkKeymapKey *keys;
+ gint n_keys;
+
+ /* Single keystroke replace */
+ typedef struct _KeystrokeReplace {
+ gchar * search;
+ gchar * replace;
+ guint keyval;
+ } KeystrokeReplace;
+ /* Special characters to replace */
+ KeystrokeReplace keystrokes_replaces[] =
+ {
+ { "\\n", "\n", GDK_KEY_Return },
+ { "\\t", "\t", GDK_KEY_Tab },
+ { "\\b", "\b", GDK_KEY_BackSpace },
+ { "\\e", "\e", GDK_KEY_Escape },
+ { "\\\\", "\\", GDK_KEY_backslash },
+ { NULL, NULL, 0 }
+ };
+
+ /* Keystrokes can only be sent to plugins that accepts them */
+ if (remmina_protocol_widget_plugin_receives_keystrokes(gp)) {
+ /* Replace special characters */
+ for (i = 0; keystrokes_replaces[i].replace; i++) {
+ REMMINA_DEBUG("Keystrokes before replacement is \'%s\'", keystrokes);
+ keystrokes = g_strdup(remmina_public_str_replace_in_place(keystrokes,
+ keystrokes_replaces[i].search,
+ keystrokes_replaces[i].replace));
+ REMMINA_DEBUG("Keystrokes after replacement is \'%s\'", keystrokes);
+ }
+ gchar *iter = g_strdup(keystrokes);
+ keyvals = (guint *)g_malloc(strlen(keystrokes));
+ while (TRUE) {
+ /* Process each character in the keystrokes */
+ character = g_utf8_get_char_validated(iter, -1);
+ if (character == 0)
+ break;
+ keyval = gdk_unicode_to_keyval(character);
+ /* Replace all the special character with its keyval */
+ for (i = 0; keystrokes_replaces[i].replace; i++) {
+ if (character == keystrokes_replaces[i].replace[0]) {
+ keys = g_new0(GdkKeymapKey, 1);
+ keyval = keystrokes_replaces[i].keyval;
+ /* A special character was generated, no keyval lookup needed */
+ character = 0;
+ break;
+ }
+ }
+ /* Decode character if it’s not a special character */
+ if (character) {
+ /* get keyval without modifications */
+ if (!gdk_keymap_get_entries_for_keyval(keymap, keyval, &keys, &n_keys)) {
+ g_warning("keyval 0x%04x has no keycode!", keyval);
+ iter = g_utf8_find_next_char(iter, NULL);
+ continue;
+ }
+ }
+ /* Add modifier keys */
+ n_keys = 0;
+ if (keys->level & 1)
+ keyvals[n_keys++] = GDK_KEY_Shift_L;
+ if (keys->level & 2)
+ keyvals[n_keys++] = GDK_KEY_Alt_R;
+ keyvals[n_keys++] = keyval;
+ /* Send keystroke to the plugin */
+ gp->priv->plugin->send_keystrokes(gp, keyvals, n_keys);
+ g_free(keys);
+ /* Process next character in the keystrokes */
+ iter = g_utf8_find_next_char(iter, NULL);
+ }
+ g_free(keyvals);
+ }
+ g_free(keystrokes);
+ return;
+}
+
+/**
+ * Send to the plugin some keystrokes from the content of the clipboard
+ * This is a copy of remmina_protocol_widget_send_keystrokes but it uses the clipboard content
+ * get from remmina_protocol_widget_send_clipboard
+ * Probably we don't need the replacement table.
+ */
+void remmina_protocol_widget_send_clip_strokes(GtkClipboard *clipboard, const gchar *clip_text, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(data);
+ gchar *text = g_utf8_normalize(clip_text, -1, G_NORMALIZE_DEFAULT_COMPOSE);
+ guint *keyvals;
+ gint i;
+ GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
+ gunichar character;
+ guint keyval;
+ GdkKeymapKey *keys;
+ gint n_keys;
+
+ /* Single keystroke replace */
+ typedef struct _KeystrokeReplace {
+ gchar * search;
+ gchar * replace;
+ guint keyval;
+ } KeystrokeReplace;
+ /* Special characters to replace */
+ KeystrokeReplace text_replaces[] =
+ {
+ { "\\n", "\n", GDK_KEY_Return },
+ { "\\t", "\t", GDK_KEY_Tab },
+ { "\\b", "\b", GDK_KEY_BackSpace },
+ { "\\e", "\e", GDK_KEY_Escape },
+ { "\\\\", "\\", GDK_KEY_backslash },
+ { NULL, NULL, 0 }
+ };
+
+ if (remmina_protocol_widget_plugin_receives_keystrokes(gp)) {
+ if (text) {
+ /* Replace special characters */
+ for (i = 0; text_replaces[i].replace; i++) {
+ REMMINA_DEBUG("Text clipboard before replacement is \'%s\'", text);
+ text = g_strdup(remmina_public_str_replace_in_place(text,
+ text_replaces[i].search,
+ text_replaces[i].replace));
+ REMMINA_DEBUG("Text clipboard after replacement is \'%s\'", text);
+ }
+ gchar *iter = g_strdup(text);
+ REMMINA_DEBUG("Iter: %s", iter),
+ keyvals = (guint *)g_malloc(strlen(text));
+ while (TRUE) {
+ /* Process each character in the keystrokes */
+ character = g_utf8_get_char_validated(iter, -1);
+ REMMINA_DEBUG("Char: U+%04" G_GINT32_FORMAT"X", character);
+ if (character == 0)
+ break;
+ keyval = gdk_unicode_to_keyval(character);
+ REMMINA_DEBUG("Keyval: %u", keyval);
+ /* Replace all the special character with its keyval */
+ for (i = 0; text_replaces[i].replace; i++) {
+ if (character == text_replaces[i].replace[0]) {
+ keys = g_new0(GdkKeymapKey, 1);
+ keyval = text_replaces[i].keyval;
+ REMMINA_DEBUG("Special Keyval: %u", keyval);
+ /* A special character was generated, no keyval lookup needed */
+ character = 0;
+ break;
+ }
+ }
+ /* Decode character if it’s not a special character */
+ if (character) {
+ /* get keyval without modifications */
+ if (!gdk_keymap_get_entries_for_keyval(keymap, keyval, &keys, &n_keys)) {
+ REMMINA_WARNING("keyval 0x%04x has no keycode!", keyval);
+ iter = g_utf8_find_next_char(iter, NULL);
+ continue;
+ }
+ }
+ /* Add modifier keys */
+ n_keys = 0;
+ if (keys->level & 1)
+ keyvals[n_keys++] = GDK_KEY_Shift_L;
+ if (keys->level & 2)
+ keyvals[n_keys++] = GDK_KEY_Alt_R;
+ /*
+ * @fixme heap buffer overflow
+ * In some cases, for example sending \t as the only sequence
+ * may lead to a buffer overflow
+ */
+ keyvals[n_keys++] = keyval;
+ /* Send keystroke to the plugin */
+ gp->priv->plugin->send_keystrokes(gp, keyvals, n_keys);
+ g_free(keys);
+ /* Process next character in the keystrokes */
+ iter = g_utf8_find_next_char(iter, NULL);
+ }
+ g_free(keyvals);
+ }
+ g_free(text);
+ }
+ return;
+}
+
+void remmina_protocol_widget_send_clipboard(RemminaProtocolWidget *gp, GObject*widget)
+{
+ TRACE_CALL(__func__);
+ GtkClipboard *clipboard;
+
+ clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
+
+ /* Request the contents of the clipboard, contents_received will be
+ * called when we do get the contents.
+ */
+ gtk_clipboard_request_text(clipboard,
+ remmina_protocol_widget_send_clip_strokes, gp);
+}
+
+gboolean remmina_protocol_widget_plugin_screenshot(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd)
+{
+ TRACE_CALL(__func__);
+ if (!gp->priv->plugin->get_plugin_screenshot) {
+ REMMINA_DEBUG("plugin screenshot function is not implemented, using core Remmina functionality");
+ return FALSE;
+ }
+
+ return gp->priv->plugin->get_plugin_screenshot(gp, rpsd);
+}
+
+gboolean remmina_protocol_widget_map_event(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ if (!gp->priv->plugin->map_event) {
+ REMMINA_DEBUG("Map plugin function not implemented");
+ return FALSE;
+ }
+
+ REMMINA_DEBUG("Calling plugin mapping function");
+ return gp->priv->plugin->map_event(gp);
+}
+
+gboolean remmina_protocol_widget_unmap_event(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ if (!gp->priv->plugin->unmap_event) {
+ REMMINA_DEBUG("Unmap plugin function not implemented");
+ return FALSE;
+ }
+
+ REMMINA_DEBUG("Calling plugin unmapping function");
+ return gp->priv->plugin->unmap_event(gp);
+}
+
+void remmina_protocol_widget_emit_signal(RemminaProtocolWidget *gp, const gchar *signal_name)
+{
+ TRACE_CALL(__func__);
+
+ REMMINA_DEBUG("Emitting signals should be used from the object itself, not from another object");
+ raise(SIGINT);
+
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_PROTOCOLWIDGET_EMIT_SIGNAL;
+ d->p.protocolwidget_emit_signal.signal_name = signal_name;
+ d->p.protocolwidget_emit_signal.gp = gp;
+ remmina_masterthread_exec_and_wait(d);
+ g_free(d);
+ return;
+ }
+ g_signal_emit_by_name(G_OBJECT(gp), signal_name);
+}
+
+const RemminaProtocolFeature *remmina_protocol_widget_get_features(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->features;
+}
+
+gboolean remmina_protocol_widget_query_feature_by_type(RemminaProtocolWidget *gp, RemminaProtocolFeatureType type)
+{
+ TRACE_CALL(__func__);
+ const RemminaProtocolFeature *feature;
+
+#ifdef HAVE_LIBSSH
+ if (type == REMMINA_PROTOCOL_FEATURE_TYPE_TOOL &&
+ remmina_file_get_int(gp->priv->remmina_file, "ssh_tunnel_enabled", FALSE))
+ return TRUE;
+
+#endif
+ for (feature = gp->priv->plugin->features; feature && feature->type; feature++)
+ if (feature->type == type)
+ return TRUE;
+ return FALSE;
+}
+
+gboolean remmina_protocol_widget_query_feature_by_ref(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->plugin->query_feature(gp, feature);
+}
+
+void remmina_protocol_widget_call_feature_by_type(RemminaProtocolWidget *gp, RemminaProtocolFeatureType type, gint id)
+{
+ TRACE_CALL(__func__);
+ const RemminaProtocolFeature *feature;
+
+ for (feature = gp->priv->plugin->features; feature && feature->type; feature++) {
+ if (feature->type == type && (id == 0 || feature->id == id)) {
+ remmina_protocol_widget_call_feature_by_ref(gp, feature);
+ break;
+ }
+ }
+}
+
+void remmina_protocol_widget_call_feature_by_ref(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ switch (feature->id) {
+#ifdef HAVE_LIBSSH
+ case REMMINA_PROTOCOL_FEATURE_TOOL_SSH:
+ if (gp->priv->ssh_tunnels && gp->priv->ssh_tunnels->len > 0) {
+ rcw_open_from_file_full(
+ remmina_file_dup_temp_protocol(gp->priv->remmina_file, "SSH"), NULL,
+ (RemminaSSHTunnel *)gp->priv->ssh_tunnels->pdata[0], NULL);
+ return;
+ }
+ break;
+
+ case REMMINA_PROTOCOL_FEATURE_TOOL_SFTP:
+ if (gp->priv->ssh_tunnels && gp->priv->ssh_tunnels->len > 0) {
+ rcw_open_from_file_full(
+ remmina_file_dup_temp_protocol(gp->priv->remmina_file, "SFTP"), NULL,
+ gp->priv->ssh_tunnels->pdata[0], NULL);
+ return;
+ }
+ break;
+#endif
+ default:
+ break;
+ }
+ gp->priv->plugin->call_feature(gp, feature);
+}
+
+static gboolean remmina_protocol_widget_on_key_press(GtkWidget *widget, GdkEventKey *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ if (gp->priv->hostkey_func)
+ return gp->priv->hostkey_func(gp, event->keyval, FALSE);
+ return FALSE;
+}
+
+static gboolean remmina_protocol_widget_on_key_release(GtkWidget *widget, GdkEventKey *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ if (gp->priv->hostkey_func)
+ return gp->priv->hostkey_func(gp, event->keyval, TRUE);
+
+ return FALSE;
+}
+
+void remmina_protocol_widget_register_hostkey(RemminaProtocolWidget *gp, GtkWidget *widget)
+{
+ TRACE_CALL(__func__);
+ g_signal_connect(G_OBJECT(widget), "key-press-event", G_CALLBACK(remmina_protocol_widget_on_key_press), gp);
+ g_signal_connect(G_OBJECT(widget), "key-release-event", G_CALLBACK(remmina_protocol_widget_on_key_release), gp);
+}
+
+void remmina_protocol_widget_set_hostkey_func(RemminaProtocolWidget *gp, RemminaHostkeyFunc func)
+{
+ TRACE_CALL(__func__);
+ gp->priv->hostkey_func = func;
+}
+
+RemminaMessagePanel *remmina_protocol_widget_mpprogress(RemminaConnectionObject *cnnobj, const gchar *msg, RemminaMessagePanelCallback response_callback, gpointer response_callback_data)
+{
+ RemminaMessagePanel *mp;
+
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_PROTOCOLWIDGET_MPPROGRESS;
+ d->p.protocolwidget_mpprogress.cnnobj = cnnobj;
+ d->p.protocolwidget_mpprogress.message = msg;
+ d->p.protocolwidget_mpprogress.response_callback = response_callback;
+ d->p.protocolwidget_mpprogress.response_callback_data = response_callback_data;
+ remmina_masterthread_exec_and_wait(d);
+ mp = d->p.protocolwidget_mpprogress.ret_mp;
+ g_free(d);
+ return mp;
+ }
+
+ mp = remmina_message_panel_new();
+ remmina_message_panel_setup_progress(mp, msg, response_callback, response_callback_data);
+ rco_show_message_panel(cnnobj, mp);
+ return mp;
+}
+
+void remmina_protocol_widget_mpdestroy(RemminaConnectionObject *cnnobj, RemminaMessagePanel *mp)
+{
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_PROTOCOLWIDGET_MPDESTROY;
+ d->p.protocolwidget_mpdestroy.cnnobj = cnnobj;
+ d->p.protocolwidget_mpdestroy.mp = mp;
+ remmina_masterthread_exec_and_wait(d);
+ g_free(d);
+ return;
+ }
+ rco_destroy_message_panel(cnnobj, mp);
+}
+
+#ifdef HAVE_LIBSSH
+static void cancel_init_tunnel_cb(void *cbdata, int btn)
+{
+ printf("Remmina: Cancelling an opening tunnel is not implemented\n");
+}
+
+static RemminaSSHTunnel *remmina_protocol_widget_init_tunnel(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaSSHTunnel *tunnel;
+ gint ret;
+ gchar *msg;
+ RemminaMessagePanel *mp;
+ gboolean partial = FALSE;
+ gboolean cont = FALSE;
+
+ tunnel = remmina_ssh_tunnel_new_from_file(gp->priv->remmina_file);
+
+ REMMINA_DEBUG("Creating SSH tunnel to “%s” via SSH…", REMMINA_SSH(tunnel)->server);
+ // TRANSLATORS: “%s” is a placeholder for an hostname or an IP address.
+ msg = g_strdup_printf(_("Connecting to “%s” via SSH…"), REMMINA_SSH(tunnel)->server);
+
+ mp = remmina_protocol_widget_mpprogress(gp->cnnobj, msg, cancel_init_tunnel_cb, NULL);
+ g_free(msg);
+
+
+
+ while (1) {
+ if (!partial) {
+ if (!remmina_ssh_init_session(REMMINA_SSH(tunnel))) {
+ REMMINA_DEBUG("SSH Tunnel init session error: %s", REMMINA_SSH(tunnel)->error);
+ remmina_protocol_widget_set_error(gp, REMMINA_SSH(tunnel)->error);
+ // exit the loop here: OK
+ break;
+ }
+ }
+
+ ret = remmina_ssh_auth_gui(REMMINA_SSH(tunnel), gp, gp->priv->remmina_file);
+ REMMINA_DEBUG("Tunnel auth returned %d", ret);
+ switch (ret) {
+ case REMMINA_SSH_AUTH_SUCCESS:
+ REMMINA_DEBUG("Authentication success");
+ break;
+ case REMMINA_SSH_AUTH_PARTIAL:
+ REMMINA_DEBUG("Continue with the next auth method");
+ partial = TRUE;
+ // Continue the loop: OK
+ continue;
+ break;
+ case REMMINA_SSH_AUTH_RECONNECT:
+ REMMINA_DEBUG("Reconnecting…");
+ if (REMMINA_SSH(tunnel)->session) {
+ ssh_disconnect(REMMINA_SSH(tunnel)->session);
+ ssh_free(REMMINA_SSH(tunnel)->session);
+ REMMINA_SSH(tunnel)->session = NULL;
+ }
+ g_free(REMMINA_SSH(tunnel)->callback);
+ // Continue the loop: OK
+ continue;
+ break;
+ case REMMINA_SSH_AUTH_USERCANCEL:
+ REMMINA_DEBUG("Interrupted by the user");
+ // exit the loop here: OK
+ goto BREAK;
+ break;
+ default:
+ REMMINA_DEBUG("Error during the authentication: %s", REMMINA_SSH(tunnel)->error);
+ remmina_protocol_widget_set_error(gp, REMMINA_SSH(tunnel)->error);
+ // exit the loop here: OK
+ goto BREAK;
+ }
+
+
+ cont = TRUE;
+ break;
+ }
+
+#if 0
+
+ if (!remmina_ssh_init_session(REMMINA_SSH(tunnel))) {
+ REMMINA_DEBUG("Cannot init SSH session with tunnel struct");
+ remmina_protocol_widget_set_error(gp, REMMINA_SSH(tunnel)->error);
+ remmina_ssh_tunnel_free(tunnel);
+ return NULL;
+ }
+
+ ret = remmina_ssh_auth_gui(REMMINA_SSH(tunnel), gp, gp->priv->remmina_file);
+ REMMINA_DEBUG("Tunnel auth returned %d", ret);
+ if (ret != REMMINA_SSH_AUTH_SUCCESS) {
+ if (ret != REMMINA_SSH_AUTH_USERCANCEL)
+ remmina_protocol_widget_set_error(gp, REMMINA_SSH(tunnel)->error);
+ remmina_ssh_tunnel_free(tunnel);
+ return NULL;
+ }
+
+#endif
+
+BREAK:
+ if (!cont) {
+ remmina_ssh_tunnel_free(tunnel);
+ return NULL;
+ }
+ remmina_protocol_widget_mpdestroy(gp->cnnobj, mp);
+
+ return tunnel;
+}
+#endif
+
+
+#ifdef HAVE_LIBSSH
+static void cancel_start_direct_tunnel_cb(void *cbdata, int btn)
+{
+ printf("Remmina: Cancelling start_direct_tunnel is not implemented\n");
+}
+
+static gboolean remmina_protocol_widget_tunnel_destroy(RemminaSSHTunnel *tunnel, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(data);
+ guint idx = 0;
+
+#if GLIB_CHECK_VERSION(2, 54, 0)
+ gboolean found = g_ptr_array_find(gp->priv->ssh_tunnels, tunnel, &idx);
+#else
+ int i;
+ gboolean found = FALSE;
+ for (i = 0; i < gp->priv->ssh_tunnels->len; i++) {
+ if ((RemminaSSHTunnel *)gp->priv->ssh_tunnels->pdata[i] == tunnel) {
+ found = TRUE;
+ idx = i;
+ }
+ }
+#endif
+
+ printf("Tunnel %s found at idx = %d\n", found ? "yes": "not", idx);
+
+ if (found) {
+#ifdef HAVE_LIBSSH
+ REMMINA_DEBUG("[Tunnel with idx %u has been disconnected", idx);
+ remmina_ssh_tunnel_free(tunnel);
+#endif
+ g_ptr_array_remove(gp->priv->ssh_tunnels, tunnel);
+ }
+ return TRUE;
+}
+#endif
+
+/**
+ * Start an SSH tunnel if possible and return the host:port string.
+ *
+ */
+gchar *remmina_protocol_widget_start_direct_tunnel(RemminaProtocolWidget *gp, gint default_port, gboolean port_plus)
+{
+ TRACE_CALL(__func__);
+ const gchar *server;
+ const gchar *ssh_tunnel_server;
+ gchar *ssh_tunnel_host, *srv_host, *dest;
+ gint srv_port, ssh_tunnel_port = 0;
+
+ REMMINA_DEBUG("SSH tunnel initialization…");
+
+ server = remmina_file_get_string(gp->priv->remmina_file, "server");
+ ssh_tunnel_server = remmina_file_get_string(gp->priv->remmina_file, "ssh_tunnel_server");
+
+ if (!server)
+ return g_strdup("");
+
+ if (strstr(g_strdup(server), "unix:///") != NULL) {
+ REMMINA_DEBUG("%s is a UNIX socket", server);
+ return g_strdup(server);
+ }
+
+ REMMINA_DEBUG("Calling remmina_public_get_server_port");
+ remmina_public_get_server_port(server, default_port, &srv_host, &srv_port);
+ REMMINA_DEBUG("Calling remmina_public_get_server_port (tunnel)");
+ remmina_public_get_server_port(ssh_tunnel_server, 22, &ssh_tunnel_host, &ssh_tunnel_port);
+ REMMINA_DEBUG("server: %s, port: %d", srv_host, srv_port);
+
+ if (port_plus && srv_port < 100)
+ /* Protocols like VNC supports using instance number :0, :1, etc. as port number. */
+ srv_port += default_port;
+
+#ifdef HAVE_LIBSSH
+ gchar *msg;
+ RemminaMessagePanel *mp;
+ RemminaSSHTunnel *tunnel;
+
+ if (!remmina_file_get_int(gp->priv->remmina_file, "ssh_tunnel_enabled", FALSE)) {
+ dest = g_strdup_printf("[%s]:%i", srv_host, srv_port);
+ g_free(srv_host);
+ g_free(ssh_tunnel_host);
+ return dest;
+ }
+
+ tunnel = remmina_protocol_widget_init_tunnel(gp);
+ if (!tunnel) {
+ g_free(srv_host);
+ g_free(ssh_tunnel_host);
+ REMMINA_DEBUG("remmina_protocol_widget_init_tunnel failed with error is %s",
+ remmina_protocol_widget_get_error_message(gp));
+ return NULL;
+ }
+
+ // TRANSLATORS: “%s” is a placeholder for an hostname or an IP address.
+ msg = g_strdup_printf(_("Connecting to “%s” via SSH…"), server);
+ mp = remmina_protocol_widget_mpprogress(gp->cnnobj, msg, cancel_start_direct_tunnel_cb, NULL);
+ g_free(msg);
+
+ if (remmina_file_get_int(gp->priv->remmina_file, "ssh_tunnel_loopback", FALSE)) {
+ g_free(srv_host);
+ g_free(ssh_tunnel_host);
+ ssh_tunnel_host = NULL;
+ srv_host = g_strdup("127.0.0.1");
+ }
+
+ REMMINA_DEBUG("Starting tunnel to: %s, port: %d", ssh_tunnel_host, ssh_tunnel_port);
+ if (!remmina_ssh_tunnel_open(tunnel, srv_host, srv_port, remmina_pref.sshtunnel_port)) {
+ g_free(srv_host);
+ g_free(ssh_tunnel_host);
+ remmina_protocol_widget_set_error(gp, REMMINA_SSH(tunnel)->error);
+ remmina_ssh_tunnel_free(tunnel);
+ return NULL;
+ }
+ g_free(srv_host);
+ g_free(ssh_tunnel_host);
+
+ remmina_protocol_widget_mpdestroy(gp->cnnobj, mp);
+
+ tunnel->destroy_func = remmina_protocol_widget_tunnel_destroy;
+ tunnel->destroy_func_callback_data = (gpointer)gp;
+
+ g_ptr_array_add(gp->priv->ssh_tunnels, tunnel);
+
+ return g_strdup_printf("127.0.0.1:%i", remmina_pref.sshtunnel_port);
+
+#else
+
+ dest = g_strdup_printf("[%s]:%i", srv_host, srv_port);
+ g_free(srv_host);
+ g_free(ssh_tunnel_host);
+ return dest;
+
+#endif
+}
+
+#ifdef HAVE_LIBSSH
+static void cancel_start_reverse_tunnel_cb(void *cbdata, int btn)
+{
+ printf("Remmina: Cancelling start_reverse_tunnel is not implemented\n");
+}
+#endif
+
+
+gboolean remmina_protocol_widget_start_reverse_tunnel(RemminaProtocolWidget *gp, gint local_port)
+{
+ TRACE_CALL(__func__);
+#ifdef HAVE_LIBSSH
+ gchar *msg;
+ RemminaMessagePanel *mp;
+ RemminaSSHTunnel *tunnel;
+
+ if (!remmina_file_get_int(gp->priv->remmina_file, "ssh_tunnel_enabled", FALSE))
+ return TRUE;
+
+ if (!(tunnel = remmina_protocol_widget_init_tunnel(gp)))
+ return FALSE;
+
+ // TRANSLATORS: “%i” is a placeholder for a TCP port number.
+ msg = g_strdup_printf(_("Awaiting incoming SSH connection on port %i…"), remmina_file_get_int(gp->priv->remmina_file, "listenport", 0));
+ mp = remmina_protocol_widget_mpprogress(gp->cnnobj, msg, cancel_start_reverse_tunnel_cb, NULL);
+ g_free(msg);
+
+ if (!remmina_ssh_tunnel_reverse(tunnel, remmina_file_get_int(gp->priv->remmina_file, "listenport", 0), local_port)) {
+ remmina_ssh_tunnel_free(tunnel);
+ remmina_protocol_widget_set_error(gp, REMMINA_SSH(tunnel)->error);
+ return FALSE;
+ }
+ remmina_protocol_widget_mpdestroy(gp->cnnobj, mp);
+ g_ptr_array_add(gp->priv->ssh_tunnels, tunnel);
+#endif
+
+ return TRUE;
+}
+
+gboolean remmina_protocol_widget_ssh_exec(RemminaProtocolWidget *gp, gboolean wait, const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+#ifdef HAVE_LIBSSH
+ RemminaSSHTunnel *tunnel;
+ ssh_channel channel;
+ gint status;
+ gboolean ret = FALSE;
+ gchar *cmd, *ptr;
+ va_list args;
+
+ if (gp->priv->ssh_tunnels->len < 1)
+ return FALSE;
+
+ tunnel = (RemminaSSHTunnel *)gp->priv->ssh_tunnels->pdata[0];
+
+ if ((channel = ssh_channel_new(REMMINA_SSH(tunnel)->session)) == NULL)
+ return FALSE;
+
+ va_start(args, fmt);
+ cmd = g_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ if (ssh_channel_open_session(channel) == SSH_OK &&
+ ssh_channel_request_exec(channel, cmd) == SSH_OK) {
+ if (wait) {
+ ssh_channel_send_eof(channel);
+ status = ssh_channel_get_exit_status(channel);
+ ptr = strchr(cmd, ' ');
+ if (ptr) *ptr = '\0';
+ switch (status) {
+ case 0:
+ ret = TRUE;
+ break;
+ case 127:
+ // TRANSLATORS: “%s” is a place holder for a unix command path.
+ remmina_ssh_set_application_error(REMMINA_SSH(tunnel),
+ _("The “%s” command is not available on the SSH server."), cmd);
+ break;
+ default:
+ // TRANSLATORS: “%s” is a place holder for a unix command path. “%i” is a placeholder for an error code number.
+ remmina_ssh_set_application_error(REMMINA_SSH(tunnel),
+ _("Could not run the “%s” command on the SSH server (status = %i)."), cmd, status);
+ break;
+ }
+ } else {
+ ret = TRUE;
+ }
+ } else {
+ // TRANSLATORS: %s is a placeholder for an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not run command. %s"));
+ }
+ g_free(cmd);
+ if (wait)
+ ssh_channel_close(channel);
+ ssh_channel_free(channel);
+ return ret;
+
+#else
+
+ return FALSE;
+
+#endif
+}
+
+#ifdef HAVE_LIBSSH
+static gboolean remmina_protocol_widget_xport_tunnel_init_callback(RemminaSSHTunnel *tunnel, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(data);
+ gchar *server;
+ gint port;
+ gboolean ret;
+
+ REMMINA_DEBUG("Calling remmina_public_get_server_port");
+ remmina_public_get_server_port(remmina_file_get_string(gp->priv->remmina_file, "server"), 177, &server, &port);
+ ret = ((RemminaXPortTunnelInitFunc)gp->priv->init_func)(gp,
+ tunnel->remotedisplay, (tunnel->bindlocalhost ? "localhost" : server), port);
+ g_free(server);
+
+ return ret;
+}
+
+static gboolean remmina_protocol_widget_xport_tunnel_connect_callback(RemminaSSHTunnel *tunnel, gpointer data)
+{
+ TRACE_CALL(__func__);
+ return TRUE;
+}
+
+static gboolean remmina_protocol_widget_xport_tunnel_disconnect_callback(RemminaSSHTunnel *tunnel, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = REMMINA_PROTOCOL_WIDGET(data);
+
+ if (REMMINA_SSH(tunnel)->error)
+ remmina_protocol_widget_set_error(gp, "%s", REMMINA_SSH(tunnel)->error);
+
+ IDLE_ADD((GSourceFunc)remmina_protocol_widget_close_connection, gp);
+ return TRUE;
+}
+#endif
+#ifdef HAVE_LIBSSH
+static void cancel_connect_xport_cb(void *cbdata, int btn)
+{
+ printf("Remmina: Cancelling an XPort connection is not implemented\n");
+}
+#endif
+gboolean remmina_protocol_widget_start_xport_tunnel(RemminaProtocolWidget *gp, RemminaXPortTunnelInitFunc init_func)
+{
+ TRACE_CALL(__func__);
+#ifdef HAVE_LIBSSH
+ gboolean bindlocalhost;
+ gchar *server;
+ gchar *msg;
+ RemminaMessagePanel *mp;
+ RemminaSSHTunnel *tunnel;
+
+ if (!(tunnel = remmina_protocol_widget_init_tunnel(gp))) return FALSE;
+
+ // TRANSLATORS: “%s” is a placeholder for a hostname or IP address.
+ msg = g_strdup_printf(_("Connecting to %s via SSH…"), remmina_file_get_string(gp->priv->remmina_file, "server"));
+ mp = remmina_protocol_widget_mpprogress(gp->cnnobj, msg, cancel_connect_xport_cb, NULL);
+ g_free(msg);
+
+ gp->priv->init_func = init_func;
+ tunnel->init_func = remmina_protocol_widget_xport_tunnel_init_callback;
+ tunnel->connect_func = remmina_protocol_widget_xport_tunnel_connect_callback;
+ tunnel->disconnect_func = remmina_protocol_widget_xport_tunnel_disconnect_callback;
+ tunnel->callback_data = gp;
+
+ REMMINA_DEBUG("Calling remmina_public_get_server_port");
+ remmina_public_get_server_port(remmina_file_get_string(gp->priv->remmina_file, "server"), 0, &server, NULL);
+ bindlocalhost = (g_strcmp0(REMMINA_SSH(tunnel)->server, server) == 0);
+ g_free(server);
+
+ if (!remmina_ssh_tunnel_xport(tunnel, bindlocalhost)) {
+ remmina_protocol_widget_set_error(gp, "Could not open channel, %s",
+ ssh_get_error(REMMINA_SSH(tunnel)->session));
+ remmina_ssh_tunnel_free(tunnel);
+ return FALSE;
+ }
+
+ remmina_protocol_widget_mpdestroy(gp->cnnobj, mp);
+ g_ptr_array_add(gp->priv->ssh_tunnels, tunnel);
+
+ return TRUE;
+
+#else
+ return FALSE;
+#endif
+}
+
+void remmina_protocol_widget_set_display(RemminaProtocolWidget *gp, gint display)
+{
+ TRACE_CALL(__func__);
+#ifdef HAVE_LIBSSH
+ RemminaSSHTunnel *tunnel;
+ if (gp->priv->ssh_tunnels->len < 1)
+ return;
+ tunnel = (RemminaSSHTunnel *)gp->priv->ssh_tunnels->pdata[0];
+ if (tunnel->localdisplay) g_free(tunnel->localdisplay);
+ tunnel->localdisplay = g_strdup_printf("unix:%i", display);
+#endif
+}
+
+gint remmina_protocol_widget_get_profile_remote_width(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ /* Returns the width of remote desktop as chosen by the user profile */
+ return gp->priv->profile_remote_width;
+}
+
+gint remmina_protocol_widget_get_multimon(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ /* Returns ehenever multi monitor is enabled (1) */
+ gp->priv->multimon = remmina_file_get_int(gp->priv->remmina_file, "multimon", -1);
+ REMMINA_DEBUG("Multi monitor is set to %d", gp->priv->multimon);
+ return gp->priv->multimon;
+}
+
+gint remmina_protocol_widget_get_profile_remote_height(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ /* Returns the height of remote desktop as chosen by the user profile */
+ return gp->priv->profile_remote_height;
+}
+
+const gchar* remmina_protocol_widget_get_name(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp ? gp->plugin ? gp->plugin->name : NULL : NULL;
+}
+
+gint remmina_protocol_widget_get_width(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->width;
+}
+
+void remmina_protocol_widget_set_width(RemminaProtocolWidget *gp, gint width)
+{
+ TRACE_CALL(__func__);
+ gp->priv->width = width;
+}
+
+gint remmina_protocol_widget_get_height(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->height;
+}
+
+void remmina_protocol_widget_set_height(RemminaProtocolWidget *gp, gint height)
+{
+ TRACE_CALL(__func__);
+ gp->priv->height = height;
+}
+
+RemminaScaleMode remmina_protocol_widget_get_current_scale_mode(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->scalemode;
+}
+
+void remmina_protocol_widget_set_current_scale_mode(RemminaProtocolWidget *gp, RemminaScaleMode scalemode)
+{
+ TRACE_CALL(__func__);
+ gp->priv->scalemode = scalemode;
+}
+
+gboolean remmina_protocol_widget_get_expand(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->scaler_expand;
+}
+
+void remmina_protocol_widget_set_expand(RemminaProtocolWidget *gp, gboolean expand)
+{
+ TRACE_CALL(__func__);
+ gp->priv->scaler_expand = expand;
+ return;
+}
+
+gboolean remmina_protocol_widget_has_error(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->has_error;
+}
+
+const gchar *remmina_protocol_widget_get_error_message(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->error_message;
+}
+
+void remmina_protocol_widget_set_error(RemminaProtocolWidget *gp, const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+ va_list args;
+
+ if (gp->priv->error_message) g_free(gp->priv->error_message);
+
+ if (fmt == NULL) {
+ gp->priv->has_error = FALSE;
+ gp->priv->error_message = NULL;
+ return;
+ }
+
+ va_start(args, fmt);
+ gp->priv->error_message = g_strdup_vprintf(fmt, args);
+ va_end(args);
+
+ gp->priv->has_error = TRUE;
+}
+
+gboolean remmina_protocol_widget_is_closed(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->closed;
+}
+
+RemminaFile *remmina_protocol_widget_get_file(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->remmina_file;
+}
+
+struct remmina_protocol_widget_dialog_mt_data_t {
+ /* Input data */
+ RemminaProtocolWidget * gp;
+ gchar * title;
+ gchar * default_username;
+ gchar * default_password;
+ gchar * default_domain;
+ gchar * strpasswordlabel;
+ enum panel_type dtype;
+ RemminaMessagePanelFlags pflags;
+ gboolean called_from_subthread;
+ /* Running status */
+ pthread_mutex_t pt_mutex;
+ pthread_cond_t pt_cond;
+ /* Output/retval */
+ int rcbutton;
+};
+
+static void authpanel_mt_cb(void *user_data, int button)
+{
+ struct remmina_protocol_widget_dialog_mt_data_t *d = (struct remmina_protocol_widget_dialog_mt_data_t *)user_data;
+
+ d->rcbutton = button;
+ if (button == GTK_RESPONSE_OK) {
+ if (d->dtype == RPWDT_AUTH) {
+ d->gp->priv->password = remmina_message_panel_field_get_string(d->gp->priv->auth_message_panel, REMMINA_MESSAGE_PANEL_PASSWORD);
+ d->gp->priv->username = remmina_message_panel_field_get_string(d->gp->priv->auth_message_panel, REMMINA_MESSAGE_PANEL_USERNAME);
+ d->gp->priv->domain = remmina_message_panel_field_get_string(d->gp->priv->auth_message_panel, REMMINA_MESSAGE_PANEL_DOMAIN);
+ d->gp->priv->save_password = remmina_message_panel_field_get_switch_state(d->gp->priv->auth_message_panel, REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD);
+ } else if (d->dtype == RPWDT_AUTHX509) {
+ d->gp->priv->cacert = remmina_message_panel_field_get_filename(d->gp->priv->auth_message_panel, REMMINA_MESSAGE_PANEL_CACERTFILE);
+ d->gp->priv->cacrl = remmina_message_panel_field_get_filename(d->gp->priv->auth_message_panel, REMMINA_MESSAGE_PANEL_CACRLFILE);
+ d->gp->priv->clientcert = remmina_message_panel_field_get_filename(d->gp->priv->auth_message_panel, REMMINA_MESSAGE_PANEL_CLIENTCERTFILE);
+ d->gp->priv->clientkey = remmina_message_panel_field_get_filename(d->gp->priv->auth_message_panel, REMMINA_MESSAGE_PANEL_CLIENTKEYFILE);
+ }
+ }
+
+ if (d->called_from_subthread) {
+ /* Hide and destroy message panel, we can do it now because we are on the main thread */
+ rco_destroy_message_panel(d->gp->cnnobj, d->gp->priv->auth_message_panel);
+
+ /* Awake the locked subthread, when called from subthread */
+ pthread_mutex_lock(&d->pt_mutex);
+ pthread_cond_signal(&d->pt_cond);
+ pthread_mutex_unlock(&d->pt_mutex);
+ } else {
+ /* Signal completion, when called from main thread. Message panel will be destroyed by the caller */
+ remmina_message_panel_response(d->gp->priv->auth_message_panel, button);
+ }
+}
+
+static gboolean remmina_protocol_widget_dialog_mt_setup(gpointer user_data)
+{
+ struct remmina_protocol_widget_dialog_mt_data_t *d = (struct remmina_protocol_widget_dialog_mt_data_t *)user_data;
+
+ RemminaFile *remminafile = d->gp->priv->remmina_file;
+ RemminaMessagePanel *mp;
+ const gchar *s;
+
+ if (d->gp->cnnobj == NULL)
+ return FALSE;
+
+ mp = remmina_message_panel_new();
+
+ if (d->dtype == RPWDT_AUTH) {
+ if (d->pflags & REMMINA_MESSAGE_PANEL_FLAG_USERNAME) {
+ remmina_message_panel_field_set_string(mp, REMMINA_MESSAGE_PANEL_USERNAME, d->default_username);
+ }
+ remmina_message_panel_setup_auth(mp, authpanel_mt_cb, d, d->title, d->strpasswordlabel, d->pflags);
+ remmina_message_panel_field_set_string(mp, REMMINA_MESSAGE_PANEL_USERNAME, d->default_username);
+ if (d->pflags & REMMINA_MESSAGE_PANEL_FLAG_DOMAIN)
+ remmina_message_panel_field_set_string(mp, REMMINA_MESSAGE_PANEL_DOMAIN, d->default_domain);
+ remmina_message_panel_field_set_string(mp, REMMINA_MESSAGE_PANEL_PASSWORD, d->default_password);
+ if (d->pflags & REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD)
+ remmina_message_panel_field_set_switch(mp, REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD, (d->default_password == NULL || d->default_password[0] == 0) ? FALSE: TRUE);
+ } else if (d->dtype == RPWDT_QUESTIONYESNO) {
+ remmina_message_panel_setup_question(mp, d->title, authpanel_mt_cb, d);
+ } else if (d->dtype == RPWDT_AUTHX509) {
+ remmina_message_panel_setup_auth_x509(mp, authpanel_mt_cb, d);
+ if ((s = remmina_file_get_string(remminafile, "cacert")) != NULL)
+ remmina_message_panel_field_set_filename(mp, REMMINA_MESSAGE_PANEL_CACERTFILE, s);
+ if ((s = remmina_file_get_string(remminafile, "cacrl")) != NULL)
+ remmina_message_panel_field_set_filename(mp, REMMINA_MESSAGE_PANEL_CACRLFILE, s);
+ if ((s = remmina_file_get_string(remminafile, "clientcert")) != NULL)
+ remmina_message_panel_field_set_filename(mp, REMMINA_MESSAGE_PANEL_CLIENTCERTFILE, s);
+ if ((s = remmina_file_get_string(remminafile, "clientkey")) != NULL)
+ remmina_message_panel_field_set_filename(mp, REMMINA_MESSAGE_PANEL_CLIENTKEYFILE, s);
+ }
+
+ d->gp->priv->auth_message_panel = mp;
+ rco_show_message_panel(d->gp->cnnobj, mp);
+
+ return FALSE;
+}
+
+typedef struct {
+ RemminaMessagePanel * mp;
+ GMainLoop * loop;
+ gint response;
+ gboolean destroyed;
+} MpRunInfo;
+
+static void shutdown_loop(MpRunInfo *mpri)
+{
+ if (g_main_loop_is_running(mpri->loop))
+ g_main_loop_quit(mpri->loop);
+}
+
+static void run_response_handler(RemminaMessagePanel *mp, gint response_id, gpointer data)
+{
+ MpRunInfo *mpri = (MpRunInfo *)data;
+
+ mpri->response = response_id;
+ shutdown_loop(mpri);
+}
+
+static void run_unmap_handler(RemminaMessagePanel *mp, gpointer data)
+{
+ MpRunInfo *mpri = (MpRunInfo *)data;
+
+ mpri->response = GTK_RESPONSE_CANCEL;
+ shutdown_loop(mpri);
+}
+
+static void run_destroy_handler(RemminaMessagePanel *mp, gpointer data)
+{
+ MpRunInfo *mpri = (MpRunInfo *)data;
+
+ mpri->destroyed = TRUE;
+ mpri->response = GTK_RESPONSE_CANCEL;
+ shutdown_loop(mpri);
+}
+
+static int remmina_protocol_widget_dialog(enum panel_type dtype, RemminaProtocolWidget *gp, RemminaMessagePanelFlags pflags,
+ const gchar *title, const gchar *default_username, const gchar *default_password, const gchar *default_domain,
+ const gchar *strpasswordlabel)
+{
+ TRACE_CALL(__func__);
+
+ struct remmina_protocol_widget_dialog_mt_data_t *d = (struct remmina_protocol_widget_dialog_mt_data_t *)g_malloc(sizeof(struct remmina_protocol_widget_dialog_mt_data_t));
+ int rcbutton;
+
+ d->gp = gp;
+ d->pflags = pflags;
+ d->dtype = dtype;
+ d->title = g_strdup(title);
+ d->strpasswordlabel = g_strdup(strpasswordlabel);
+ d->default_username = g_strdup(default_username);
+ d->default_password = g_strdup(default_password);
+ d->default_domain = g_strdup(default_domain);
+ d->called_from_subthread = FALSE;
+
+ if (remmina_masterthread_exec_is_main_thread()) {
+ /* Run the MessagePanel in main thread, in a very similar way of gtk_dialog_run() */
+ MpRunInfo mpri = { NULL, NULL, GTK_RESPONSE_CANCEL, FALSE };
+
+ gulong unmap_handler;
+ gulong destroy_handler;
+ gulong response_handler;
+
+ remmina_protocol_widget_dialog_mt_setup(d);
+
+ mpri.mp = d->gp->priv->auth_message_panel;
+
+ if (!gtk_widget_get_visible(GTK_WIDGET(mpri.mp)))
+ gtk_widget_show(GTK_WIDGET(mpri.mp));
+ response_handler = g_signal_connect(mpri.mp, "response", G_CALLBACK(run_response_handler), &mpri);
+ unmap_handler = g_signal_connect(mpri.mp, "unmap", G_CALLBACK(run_unmap_handler), &mpri);
+ destroy_handler = g_signal_connect(mpri.mp, "destroy", G_CALLBACK(run_destroy_handler), &mpri);
+
+ g_object_ref(mpri.mp);
+
+ mpri.loop = g_main_loop_new(NULL, FALSE);
+ g_main_loop_run(mpri.loop);
+ g_main_loop_unref(mpri.loop);
+
+ if (!mpri.destroyed) {
+ g_signal_handler_disconnect(mpri.mp, response_handler);
+ g_signal_handler_disconnect(mpri.mp, destroy_handler);
+ g_signal_handler_disconnect(mpri.mp, unmap_handler);
+ }
+ g_object_unref(mpri.mp);
+
+ rco_destroy_message_panel(d->gp->cnnobj, d->gp->priv->auth_message_panel);
+
+ rcbutton = mpri.response;
+ } else {
+ d->called_from_subthread = TRUE;
+ // pthread_cleanup_push(ptcleanup, (void*)d);
+ pthread_cond_init(&d->pt_cond, NULL);
+ pthread_mutex_init(&d->pt_mutex, NULL);
+ g_idle_add(remmina_protocol_widget_dialog_mt_setup, d);
+ pthread_mutex_lock(&d->pt_mutex);
+ pthread_cond_wait(&d->pt_cond, &d->pt_mutex);
+ // pthread_cleanup_pop(0);
+ pthread_mutex_destroy(&d->pt_mutex);
+ pthread_cond_destroy(&d->pt_cond);
+
+ rcbutton = d->rcbutton;
+ }
+
+ g_free(d->title);
+ g_free(d->strpasswordlabel);
+ g_free(d->default_username);
+ g_free(d->default_password);
+ g_free(d->default_domain);
+ g_free(d);
+ return rcbutton;
+}
+
+gint remmina_protocol_widget_panel_question_yesno(RemminaProtocolWidget *gp, const char *msg)
+{
+ return remmina_protocol_widget_dialog(RPWDT_QUESTIONYESNO, gp, 0, msg, NULL, NULL, NULL, NULL);
+}
+
+gint remmina_protocol_widget_panel_auth(RemminaProtocolWidget *gp, RemminaMessagePanelFlags pflags,
+ const gchar *title, const gchar *default_username, const gchar *default_password, const gchar *default_domain, const gchar *password_prompt)
+{
+ TRACE_CALL(__func__);
+ return remmina_protocol_widget_dialog(RPWDT_AUTH, gp, pflags, title, default_username,
+ default_password, default_domain, password_prompt == NULL ? _("Password") : password_prompt);
+}
+
+gint remmina_protocol_widget_panel_authuserpwd_ssh_tunnel(RemminaProtocolWidget *gp, gboolean want_domain, gboolean allow_password_saving)
+{
+ TRACE_CALL(__func__);
+ unsigned pflags;
+ RemminaFile *remminafile = gp->priv->remmina_file;
+ const gchar *username, *password;
+
+ pflags = REMMINA_MESSAGE_PANEL_FLAG_USERNAME;
+ if (remmina_file_get_filename(remminafile) != NULL &&
+ !remminafile->prevent_saving && allow_password_saving)
+ pflags |= REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD;
+
+ username = remmina_file_get_string(remminafile, "ssh_tunnel_username");
+ password = remmina_file_get_string(remminafile, "ssh_tunnel_password");
+
+ return remmina_protocol_widget_dialog(RPWDT_AUTH, gp, pflags, _("Type in SSH username and password."), username,
+ password, NULL, _("Password"));
+}
+
+/*
+ * gint remmina_protocol_widget_panel_authpwd(RemminaProtocolWidget* gp, RemminaAuthpwdType authpwd_type, gboolean allow_password_saving)
+ * {
+ * TRACE_CALL(__func__);
+ * unsigned pflags;
+ * RemminaFile* remminafile = gp->priv->remmina_file;
+ * char *password_prompt;
+ * int rc;
+ *
+ * pflags = 0;
+ * if (remmina_file_get_filename(remminafile) != NULL &&
+ * !remminafile->prevent_saving && allow_password_saving)
+ * pflags |= REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD;
+ *
+ * switch (authpwd_type) {
+ * case REMMINA_AUTHPWD_TYPE_PROTOCOL:
+ * password_prompt = g_strdup_printf(_("%s password"), remmina_file_get_string(remminafile, "protocol"));
+ * break;
+ * case REMMINA_AUTHPWD_TYPE_SSH_PWD:
+ * password_prompt = g_strdup(_("SSH password"));
+ * break;
+ * case REMMINA_AUTHPWD_TYPE_SSH_PRIVKEY:
+ * password_prompt = g_strdup(_("SSH private key passphrase"));
+ * break;
+ * default:
+ * password_prompt = g_strdup(_("Password"));
+ * break;
+ * }
+ *
+ * rc = remmina_protocol_widget_dialog(RPWDT_AUTH, gp, pflags, password_prompt);
+ * g_free(password_prompt);
+ * return rc;
+ *
+ * }
+ */
+gint remmina_protocol_widget_panel_authx509(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ return remmina_protocol_widget_dialog(RPWDT_AUTHX509, gp, 0, NULL, NULL, NULL, NULL, NULL);
+}
+
+
+gint remmina_protocol_widget_panel_new_certificate(RemminaProtocolWidget *gp, const gchar *subject, const gchar *issuer, const gchar *fingerprint)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+ int rc;
+
+ if (remmina_pref_get_boolean("trust_all")) {
+ /* For compatibility with plugin API: The plugin expects GTK_RESPONSE_OK when user confirms new cert */
+ remmina_public_send_notification("remmina-security-trust-all-id", _("Fingerprint automatically accepted"), fingerprint);
+ rc = GTK_RESPONSE_OK;
+ return rc;
+ }
+ // For markup see https://developer.gnome.org/pygtk/stable/pango-markup-language.html
+ s = g_strdup_printf(
+ "<big>%s</big>\n\n%s %s\n%s %s\n%s %s\n\n<big>%s</big>",
+ // TRANSLATORS: The user is asked to verify a new SSL certificate.
+ _("Certificate details:"),
+ // TRANSLATORS: An SSL certificate subject is usually the remote server the user connect to.
+ _("Subject:"), subject,
+ // TRANSLATORS: The name or email of the entity that have issued the SSL certificate
+ _("Issuer:"), issuer,
+ // TRANSLATORS: An SSL certificate fingerprint, is a hash of a certificate calculated on all certificate's data and its signature.
+ _("Fingerprint:"), fingerprint,
+ // TRANSLATORS: The user is asked to accept or refuse a new SSL certificate.
+ _("Accept certificate?"));
+ rc = remmina_protocol_widget_dialog(RPWDT_QUESTIONYESNO, gp, 0, s, NULL, NULL, NULL, NULL);
+ g_free(s);
+
+ /* For compatibility with plugin API: the plugin expects GTK_RESPONSE_OK when user confirms new cert */
+ return rc == GTK_RESPONSE_YES ? GTK_RESPONSE_OK : rc;
+}
+
+gint remmina_protocol_widget_panel_changed_certificate(RemminaProtocolWidget *gp, const gchar *subject, const gchar *issuer, const gchar *new_fingerprint, const gchar *old_fingerprint)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+ int rc;
+
+ if (remmina_pref_get_boolean("trust_all")) {
+ /* For compatibility with plugin API: The plugin expects GTK_RESPONSE_OK when user confirms new cert */
+ remmina_public_send_notification("remmina-security-trust-all-id", _("Fingerprint automatically accepted"), new_fingerprint);
+ rc = GTK_RESPONSE_OK;
+ return rc;
+ }
+ // For markup see https://developer.gnome.org/pygtk/stable/pango-markup-language.html
+ s = g_strdup_printf(
+ "<big>%s</big>\n\n%s %s\n%s %s\n%s %s\n%s %s\n\n<big>%s</big>",
+ // TRANSLATORS: The user is asked to verify a new SSL certificate.
+ _("The certificate changed! Details:"),
+ // TRANSLATORS: An SSL certificate subject is usually the remote server the user connect to.
+ _("Subject:"), subject,
+ // TRANSLATORS: The name or email of the entity that have issued the SSL certificate
+ _("Issuer:"), issuer,
+ // TRANSLATORS: An SSL certificate fingerprint, is a hash of a certificate calculated on all certificate's data and its signature.
+ _("Old fingerprint:"), old_fingerprint,
+ // TRANSLATORS: An SSL certificate fingerprint, is a hash of a certificate calculated on all certificate's data and its signature.
+ _("New fingerprint:"), new_fingerprint,
+ // TRANSLATORS: The user is asked to accept or refuse a new SSL certificate.
+ _("Accept changed certificate?"));
+ rc = remmina_protocol_widget_dialog(RPWDT_QUESTIONYESNO, gp, 0, s, NULL, NULL, NULL, NULL);
+ g_free(s);
+
+ /* For compatibility with plugin API: The plugin expects GTK_RESPONSE_OK when user confirms new cert */
+ return rc == GTK_RESPONSE_YES ? GTK_RESPONSE_OK : rc;
+}
+
+gchar *remmina_protocol_widget_get_username(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return g_strdup(gp->priv->username);
+}
+
+gchar *remmina_protocol_widget_get_password(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return g_strdup(gp->priv->password);
+}
+
+gchar *remmina_protocol_widget_get_domain(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return g_strdup(gp->priv->domain);
+}
+
+gboolean remmina_protocol_widget_get_savepassword(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ return gp->priv->save_password;
+}
+
+gchar *remmina_protocol_widget_get_cacert(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+
+ s = gp->priv->cacert;
+ return s && s[0] ? g_strdup(s) : NULL;
+}
+
+gchar *remmina_protocol_widget_get_cacrl(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+
+ s = gp->priv->cacrl;
+ return s && s[0] ? g_strdup(s) : NULL;
+}
+
+gchar *remmina_protocol_widget_get_clientcert(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+
+ s = gp->priv->clientcert;
+ return s && s[0] ? g_strdup(s) : NULL;
+}
+
+gchar *remmina_protocol_widget_get_clientkey(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gchar *s;
+
+ s = gp->priv->clientkey;
+ return s && s[0] ? g_strdup(s) : NULL;
+}
+
+void remmina_protocol_widget_save_cred(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaFile *remminafile = gp->priv->remmina_file;
+ gchar *s;
+ gboolean save = FALSE;
+
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_INIT_SAVE_CRED;
+ d->p.init_save_creds.gp = gp;
+ remmina_masterthread_exec_and_wait(d);
+ g_free(d);
+ return;
+ }
+
+ /* Save username and certificates if any; save the password if it’s requested to do so */
+ s = gp->priv->username;
+ if (s && s[0]) {
+ remmina_file_set_string(remminafile, "username", s);
+ save = TRUE;
+ }
+ s = gp->priv->cacert;
+ if (s && s[0]) {
+ remmina_file_set_string(remminafile, "cacert", s);
+ save = TRUE;
+ }
+ s = gp->priv->cacrl;
+ if (s && s[0]) {
+ remmina_file_set_string(remminafile, "cacrl", s);
+ save = TRUE;
+ }
+ s = gp->priv->clientcert;
+ if (s && s[0]) {
+ remmina_file_set_string(remminafile, "clientcert", s);
+ save = TRUE;
+ }
+ s = gp->priv->clientkey;
+ if (s && s[0]) {
+ remmina_file_set_string(remminafile, "clientkey", s);
+ save = TRUE;
+ }
+ if (gp->priv->save_password) {
+ remmina_file_set_string(remminafile, "password", gp->priv->password);
+ save = TRUE;
+ }
+ if (save)
+ remmina_file_save(remminafile);
+}
+
+
+void remmina_protocol_widget_panel_show_listen(RemminaProtocolWidget *gp, gint port)
+{
+ TRACE_CALL(__func__);
+ RemminaMessagePanel *mp;
+ gchar *s;
+
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_PROTOCOLWIDGET_PANELSHOWLISTEN;
+ d->p.protocolwidget_panelshowlisten.gp = gp;
+ d->p.protocolwidget_panelshowlisten.port = port;
+ remmina_masterthread_exec_and_wait(d);
+ g_free(d);
+ return;
+ }
+
+ mp = remmina_message_panel_new();
+ s = g_strdup_printf(
+ // TRANSLATORS: “%i” is a placeholder for a port number. “%s” is a placeholder for a protocol name (VNC).
+ _("Listening on port %i for an incoming %s connection…"), port,
+ remmina_file_get_string(gp->priv->remmina_file, "protocol"));
+ remmina_message_panel_setup_progress(mp, s, NULL, NULL);
+ g_free(s);
+ gp->priv->listen_message_panel = mp;
+ rco_show_message_panel(gp->cnnobj, mp);
+}
+
+void remmina_protocol_widget_panel_show_retry(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaMessagePanel *mp;
+
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_PROTOCOLWIDGET_MPSHOWRETRY;
+ d->p.protocolwidget_mpshowretry.gp = gp;
+ remmina_masterthread_exec_and_wait(d);
+ g_free(d);
+ return;
+ }
+
+ mp = remmina_message_panel_new();
+ remmina_message_panel_setup_progress(mp, _("Could not authenticate, attempting reconnection…"), NULL, NULL);
+ gp->priv->retry_message_panel = mp;
+ rco_show_message_panel(gp->cnnobj, mp);
+}
+
+void remmina_protocol_widget_panel_show(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ printf("Remmina: The %s function is not implemented, and is left here only for plugin API compatibility.\n", __func__);
+}
+
+void remmina_protocol_widget_panel_hide(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ printf("Remmina: The %s function is not implemented, and is left here only for plugin API compatibility.\n", __func__);
+}
+
+static void remmina_protocol_widget_chat_on_destroy(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ gp->priv->chat_window = NULL;
+}
+
+void remmina_protocol_widget_chat_open(RemminaProtocolWidget *gp, const gchar *name,
+ void (*on_send)(RemminaProtocolWidget *gp, const gchar *text), void (*on_destroy)(RemminaProtocolWidget *gp))
+{
+ TRACE_CALL(__func__);
+ if (gp->priv->chat_window) {
+ gtk_window_present(GTK_WINDOW(gp->priv->chat_window));
+ } else {
+ gp->priv->chat_window = remmina_chat_window_new(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(gp))), name);
+ g_signal_connect_swapped(G_OBJECT(gp->priv->chat_window), "send", G_CALLBACK(on_send), gp);
+ g_signal_connect_swapped(G_OBJECT(gp->priv->chat_window), "destroy",
+ G_CALLBACK(remmina_protocol_widget_chat_on_destroy), gp);
+ g_signal_connect_swapped(G_OBJECT(gp->priv->chat_window), "destroy", G_CALLBACK(on_destroy), gp);
+ gtk_widget_show(gp->priv->chat_window);
+ }
+}
+
+void remmina_protocol_widget_chat_close(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ if (gp->priv->chat_window)
+ gtk_widget_destroy(gp->priv->chat_window);
+}
+
+void remmina_protocol_widget_chat_receive(RemminaProtocolWidget *gp, const gchar *text)
+{
+ TRACE_CALL(__func__);
+ /* This function can be called from a non main thread */
+
+ if (gp->priv->chat_window) {
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_CHAT_RECEIVE;
+ d->p.chat_receive.gp = gp;
+ d->p.chat_receive.text = text;
+ remmina_masterthread_exec_and_wait(d);
+ g_free(d);
+ return;
+ }
+ remmina_chat_window_receive(REMMINA_CHAT_WINDOW(gp->priv->chat_window), _("Server"), text);
+ gtk_window_present(GTK_WINDOW(gp->priv->chat_window));
+ }
+}
+
+void remmina_protocol_widget_setup(RemminaProtocolWidget *gp, RemminaFile *remminafile, RemminaConnectionObject *cnnobj)
+{
+ RemminaProtocolPlugin *plugin;
+
+ gp->priv->remmina_file = remminafile;
+ gp->cnnobj = cnnobj;
+
+ /* Locate the protocol plugin */
+ plugin = (RemminaProtocolPlugin *)remmina_plugin_manager_get_plugin(REMMINA_PLUGIN_TYPE_PROTOCOL,
+ remmina_file_get_string(remminafile, "protocol"));
+
+ if (!plugin || !plugin->init || !plugin->open_connection) {
+ // TRANSLATORS: “%s” is a placeholder for a protocol name, like “RDP”.
+ remmina_protocol_widget_set_error(gp, _("Install the %s protocol plugin first."),
+ remmina_file_get_string(remminafile, "protocol"));
+ gp->priv->plugin = NULL;
+ return;
+ }
+ gp->priv->plugin = plugin;
+ gp->plugin = plugin;
+
+ gp->priv->scalemode = remmina_file_get_int(gp->priv->remmina_file, "scale", FALSE);
+ gp->priv->scaler_expand = remmina_file_get_int(gp->priv->remmina_file, "scaler_expand", FALSE);
+}
+
+GtkWindow *remmina_protocol_widget_get_gtkwindow(RemminaProtocolWidget *gp)
+{
+ return rcw_get_gtkwindow(gp->cnnobj);
+}
+
+GtkWidget *remmina_protocol_widget_gtkviewport(RemminaProtocolWidget *gp)
+{
+ return rcw_get_gtkviewport(gp->cnnobj);
+}
+
+GtkWidget *remmina_protocol_widget_new(void)
+{
+ return GTK_WIDGET(g_object_new(REMMINA_TYPE_PROTOCOL_WIDGET, NULL));
+}
+
+/* Send one or more keystrokes to a specific widget by firing key-press and
+ * key-release events.
+ * GdkEventType action can be GDK_KEY_PRESS or GDK_KEY_RELEASE or both to
+ * press the keys and release them in reversed order. */
+void remmina_protocol_widget_send_keys_signals(GtkWidget *widget, const guint *keyvals, int keyvals_length, GdkEventType action)
+{
+ TRACE_CALL(__func__);
+ int i;
+ GdkEventKey event;
+ gboolean result;
+ GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
+
+ event.window = gtk_widget_get_window(widget);
+ event.send_event = TRUE;
+ event.time = GDK_CURRENT_TIME;
+ event.state = 0;
+ event.length = 0;
+ event.string = "";
+ event.group = 0;
+
+ if (action & GDK_KEY_PRESS) {
+ /* Press the requested buttons */
+ event.type = GDK_KEY_PRESS;
+ for (i = 0; i < keyvals_length; i++) {
+ event.keyval = keyvals[i];
+ event.hardware_keycode = remmina_public_get_keycode_for_keyval(keymap, event.keyval);
+ event.is_modifier = (int)remmina_public_get_modifier_for_keycode(keymap, event.hardware_keycode);
+ REMMINA_DEBUG("Sending keyval: %u, hardware_keycode: %u", event.keyval, event.hardware_keycode);
+ g_signal_emit_by_name(G_OBJECT(widget), "key-press-event", &event, &result);
+ }
+ }
+
+ if (action & GDK_KEY_RELEASE) {
+ /* Release the requested buttons in reverse order */
+ event.type = GDK_KEY_RELEASE;
+ for (i = (keyvals_length - 1); i >= 0; i--) {
+ event.keyval = keyvals[i];
+ event.hardware_keycode = remmina_public_get_keycode_for_keyval(keymap, event.keyval);
+ event.is_modifier = (int)remmina_public_get_modifier_for_keycode(keymap, event.hardware_keycode);
+ g_signal_emit_by_name(G_OBJECT(widget), "key-release-event", &event, &result);
+ }
+ }
+}
+
+void remmina_protocol_widget_update_remote_resolution(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ GdkRectangle rect;
+ gint w, h;
+ gint wfile, hfile;
+ RemminaProtocolWidgetResolutionMode res_mode;
+ RemminaScaleMode scalemode;
+
+ rco_get_monitor_geometry(gp->cnnobj, &rect);
+
+ /* Integrity check: check that we have a cnnwin visible and get t */
+
+ res_mode = remmina_file_get_int(gp->priv->remmina_file, "resolution_mode", RES_INVALID);
+ scalemode = remmina_file_get_int(gp->priv->remmina_file, "scale", REMMINA_PROTOCOL_WIDGET_SCALE_MODE_NONE);
+ wfile = remmina_file_get_int(gp->priv->remmina_file, "resolution_width", -1);
+ hfile = remmina_file_get_int(gp->priv->remmina_file, "resolution_height", -1);
+
+ /* If resolution_mode is non-existent (-1), then we try to calculate it
+ * as we did before having resolution_mode */
+ if (res_mode == RES_INVALID) {
+ if (wfile <= 0 || hfile <= 0)
+ res_mode = RES_USE_INITIAL_WINDOW_SIZE;
+ else
+ res_mode = RES_USE_CUSTOM;
+ }
+
+ if (res_mode == RES_USE_INITIAL_WINDOW_SIZE || scalemode == REMMINA_PROTOCOL_WIDGET_SCALE_MODE_DYNRES) {
+ /* Use internal window size as remote desktop size */
+ GtkAllocation al;
+ gtk_widget_get_allocation(GTK_WIDGET(gp), &al);
+ /* use a multiple of four to mitigate scaling when remote host rounds up */
+ w = al.width - al.width % 4;
+ h = al.height - al.height % 4;
+ if (w < 10) {
+ printf("Remmina warning: %s RemminaProtocolWidget w=%d h=%d are too small, adjusting to 640x480\n", __func__, w, h);
+ w = 640;
+ h = 480;
+ }
+ /* Due to approximations while GTK calculates scaling, (w x h) may exceed our monitor geometry
+ * Adjust to fit. */
+ if (w > rect.width)
+ w = rect.width;
+ if (h > rect.height)
+ h = rect.height;
+ } else if (res_mode == RES_USE_CLIENT) {
+ w = rect.width;
+ h = rect.height;
+ } else {
+ w = wfile;
+ h = hfile;
+ }
+ gp->priv->profile_remote_width = w;
+ gp->priv->profile_remote_height = h;
+}
diff --git a/src/remmina_protocol_widget.h b/src/remmina_protocol_widget.h
new file mode 100644
index 0000000..6047a0d
--- /dev/null
+++ b/src/remmina_protocol_widget.h
@@ -0,0 +1,187 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 "rcw.h"
+#include "remmina_file.h"
+#include "remmina_ssh.h"
+
+G_BEGIN_DECLS
+
+#define REMMINA_PROTOCOL_FEATURE_TOOL_SSH -1
+#define REMMINA_PROTOCOL_FEATURE_TOOL_SFTP -2
+
+#define REMMINA_TYPE_PROTOCOL_WIDGET (remmina_protocol_widget_get_type())
+#define REMMINA_PROTOCOL_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_PROTOCOL_WIDGET, RemminaProtocolWidget))
+#define REMMINA_PROTOCOL_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_PROTOCOL_WIDGET, RemminaProtocolWidgetClass))
+#define REMMINA_IS_PROTOCOL_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_PROTOCOL_WIDGET))
+#define REMMINA_IS_PROTOCOL_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_PROTOCOL_WIDGET))
+#define REMMINA_PROTOCOL_WIDGET_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_PROTOCOL_WIDGET, RemminaProtocolWidgetClass))
+
+typedef struct _RemminaProtocolWidgetPriv RemminaProtocolWidgetPriv;
+typedef struct _RemminaProtocolPlugin RemminaProtocolPlugin;
+
+struct _RemminaProtocolWidget {
+ GtkEventBox event_box;
+ RemminaConnectionObject * cnnobj;
+ RemminaProtocolWidgetPriv * priv;
+ RemminaProtocolPlugin * plugin;
+};
+
+struct _RemminaProtocolWidgetClass {
+ GtkEventBoxClass parent_class;
+
+ void (*connect)(RemminaProtocolWidget *gp);
+ void (*disconnect)(RemminaProtocolWidget *gp);
+ void (*desktop_resize)(RemminaProtocolWidget *gp);
+ void (*update_align)(RemminaProtocolWidget *gp);
+ void (*lock_dynres)(RemminaProtocolWidget *gp);
+ void (*unlock_dynres)(RemminaProtocolWidget *gp);
+};
+
+GType remmina_protocol_widget_get_type(void)
+G_GNUC_CONST;
+
+GtkWindow* remmina_protocol_widget_get_gtkwindow(RemminaProtocolWidget *gp);
+GtkWidget* remmina_protocol_widget_gtkviewport(RemminaProtocolWidget *gp);
+
+GtkWidget *remmina_protocol_widget_new(void);
+void remmina_protocol_widget_setup(RemminaProtocolWidget *gp, RemminaFile *remminafile, RemminaConnectionObject *cnnobj);
+
+const gchar* remmina_protocol_widget_get_name(RemminaProtocolWidget *gp);
+gint remmina_protocol_widget_get_width(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_set_width(RemminaProtocolWidget *gp, gint width);
+gint remmina_protocol_widget_get_height(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_set_height(RemminaProtocolWidget *gp, gint height);
+gint remmina_protocol_widget_get_profile_remote_width(RemminaProtocolWidget *gp);
+gint remmina_protocol_widget_get_profile_remote_height(RemminaProtocolWidget *gp);
+gint remmina_protocol_widget_get_multimon(RemminaProtocolWidget *gp);
+
+RemminaScaleMode remmina_protocol_widget_get_current_scale_mode(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_set_current_scale_mode(RemminaProtocolWidget *gp, RemminaScaleMode scalemode);
+gboolean remmina_protocol_widget_get_expand(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_set_expand(RemminaProtocolWidget *gp, gboolean expand);
+gboolean remmina_protocol_widget_has_error(RemminaProtocolWidget *gp);
+const gchar *remmina_protocol_widget_get_error_message(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_set_error(RemminaProtocolWidget *gp, const gchar *fmt, ...);
+gboolean remmina_protocol_widget_is_closed(RemminaProtocolWidget *gp);
+RemminaFile *remmina_protocol_widget_get_file(RemminaProtocolWidget *gp);
+
+void remmina_protocol_widget_open_connection(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_close_connection(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_signal_connection_closed(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_signal_connection_opened(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_update_align(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_lock_dynres(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_unlock_dynres(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_desktop_resize(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_grab_focus(RemminaProtocolWidget *gp);
+const RemminaProtocolFeature *remmina_protocol_widget_get_features(RemminaProtocolWidget *gp);
+gboolean remmina_protocol_widget_query_feature_by_type(RemminaProtocolWidget *gp, RemminaProtocolFeatureType type);
+gboolean remmina_protocol_widget_query_feature_by_ref(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature);
+void remmina_protocol_widget_call_feature_by_type(RemminaProtocolWidget *gp, RemminaProtocolFeatureType type, gint id);
+void remmina_protocol_widget_call_feature_by_ref(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature);
+/* Provide thread-safe way to emit signals */
+void remmina_protocol_widget_emit_signal(RemminaProtocolWidget *gp, const gchar *signal_name);
+void remmina_protocol_widget_register_hostkey(RemminaProtocolWidget *gp, GtkWidget *widget);
+
+typedef gboolean (*RemminaHostkeyFunc)(RemminaProtocolWidget *gp, guint keyval, gboolean release);
+void remmina_protocol_widget_set_hostkey_func(RemminaProtocolWidget *gp, RemminaHostkeyFunc func);
+
+gboolean remmina_protocol_widget_ssh_exec(RemminaProtocolWidget *gp, gboolean wait, const gchar *fmt, ...);
+
+/* Start a SSH tunnel if it’s enabled. Returns a newly allocated string indicating:
+ * 1. The actual destination (host:port) if SSH tunnel is disable
+ * 2. The tunnel local destination (127.0.0.1:port) if SSH tunnel is enabled
+ */
+gchar *remmina_protocol_widget_start_direct_tunnel(RemminaProtocolWidget *gp, gint default_port, gboolean port_plus);
+
+gboolean remmina_protocol_widget_start_reverse_tunnel(RemminaProtocolWidget *gp, gint local_port);
+gboolean remmina_protocol_widget_start_xport_tunnel(RemminaProtocolWidget *gp, RemminaXPortTunnelInitFunc init_func);
+void remmina_protocol_widget_set_display(RemminaProtocolWidget *gp, gint display);
+
+/* Extension for remmina_protocol_widget_panel_authuserpwd() not currently exported to plugins */
+gint remmina_protocol_widget_panel_authuserpwd_ssh_tunnel(RemminaProtocolWidget *gp, gboolean want_domain, gboolean allow_password_saving);
+
+/* Dialog panel API used by the plugins */
+
+gint remmina_protocol_widget_panel_auth(RemminaProtocolWidget *gp, RemminaMessagePanelFlags pflags, const gchar *title, const gchar *default_username, const gchar *default_password, const gchar *default_domain, const gchar *password_prompt);
+gint remmina_protocol_widget_panel_new_certificate(RemminaProtocolWidget *gp, const gchar *subject, const gchar *issuer, const gchar *fingerprint);
+gint remmina_protocol_widget_panel_changed_certificate(RemminaProtocolWidget *gp, const gchar *subject, const gchar *issuer, const gchar *new_fingerprint, const gchar *old_fingerprint);
+gint remmina_protocol_widget_panel_question_yesno(RemminaProtocolWidget *gp, const char *msg);
+
+void remmina_protocol_widget_panel_show(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_panel_hide(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_panel_destroy(RemminaProtocolWidget *gp);
+gint remmina_protocol_widget_panel_authx509(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_panel_show_listen(RemminaProtocolWidget *gp, gint port);
+void remmina_protocol_widget_panel_show_retry(RemminaProtocolWidget *gp);
+
+void remmina_protocol_widget_save_cred(RemminaProtocolWidget *gp);
+
+gchar *remmina_protocol_widget_get_username(RemminaProtocolWidget *gp);
+gchar *remmina_protocol_widget_get_password(RemminaProtocolWidget *gp);
+gchar *remmina_protocol_widget_get_domain(RemminaProtocolWidget *gp);
+gboolean remmina_protocol_widget_get_savepassword(RemminaProtocolWidget *gp);
+gchar *remmina_protocol_widget_get_cacert(RemminaProtocolWidget *gp);
+gchar *remmina_protocol_widget_get_cacrl(RemminaProtocolWidget *gp);
+gchar *remmina_protocol_widget_get_clientcert(RemminaProtocolWidget *gp);
+gchar *remmina_protocol_widget_get_clientkey(RemminaProtocolWidget *gp);
+
+void remmina_protocol_widget_chat_open(RemminaProtocolWidget *gp, const gchar *name, void (*on_send)(RemminaProtocolWidget *gp, const gchar *text), void (*on_destroy)(RemminaProtocolWidget *gp));
+void remmina_protocol_widget_chat_close(RemminaProtocolWidget *gp);
+void remmina_protocol_widget_chat_receive(RemminaProtocolWidget *gp, const gchar *text);
+void remmina_protocol_widget_send_keys_signals(GtkWidget *widget, const guint *keyvals, int keyvals_length, GdkEventType action);
+/* Check if the plugin accepts keystrokes */
+gboolean remmina_protocol_widget_plugin_receives_keystrokes(RemminaProtocolWidget *gp);
+/* Send to the plugin some keystrokes */
+void remmina_protocol_widget_send_keystrokes(RemminaProtocolWidget *gp, GtkMenuItem *widget);
+void remmina_protocol_widget_send_clipboard(RemminaProtocolWidget *gp, GObject *widget);
+/* Take screenshot of plugin */
+gboolean remmina_protocol_widget_plugin_screenshot(RemminaProtocolWidget *gp, RemminaPluginScreenshotData *rpsd);
+/* Deal with the remimna connection window map/unmap events */
+gboolean remmina_protocol_widget_map_event(RemminaProtocolWidget *gp);
+gboolean remmina_protocol_widget_unmap_event(RemminaProtocolWidget *gp);
+
+void remmina_protocol_widget_update_remote_resolution(RemminaProtocolWidget *gp);
+
+/* Functions to support execution of GTK code on master thread */
+RemminaMessagePanel *remmina_protocol_widget_mpprogress(RemminaConnectionObject *cnnobj, const gchar *msg, RemminaMessagePanelCallback response_callback, gpointer response_callback_data);
+void remmina_protocol_widget_mpdestroy(RemminaConnectionObject *cnnobj, RemminaMessagePanel *mp);
+
+
+G_END_DECLS
diff --git a/src/remmina_public.c b/src/remmina_public.c
new file mode 100644
index 0000000..cdb40a7
--- /dev/null
+++ b/src/remmina_public.c
@@ -0,0 +1,735 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 "config.h"
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#ifdef HAVE_NETDB_H
+#include <netdb.h>
+#endif
+#ifdef HAVE_SYS_SOCKET_H
+#include <sys/socket.h>
+#endif
+#ifdef HAVE_SYS_UN_H
+#include <sys/un.h>
+#endif
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xatom.h>
+#elif defined(GDK_WINDOWING_WAYLAND)
+#include <gdk/gdkwayland.h>
+#endif
+#include "remmina_public.h"
+#include "remmina/remmina_trace_calls.h"
+
+GtkWidget*
+remmina_public_create_combo_entry(const gchar *text, const gchar *def, gboolean descending)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *combo;
+ gboolean found;
+ gchar *buf, *ptr1, *ptr2;
+ gint i;
+
+ //g_debug("text: %s\n", text);
+ //g_debug("def: %s\n", def);
+
+ combo = gtk_combo_box_text_new_with_entry();
+ found = FALSE;
+
+ if (text && text[0] != '\0') {
+ buf = g_strdup(text);
+ ptr1 = buf;
+ i = 0;
+ while (ptr1 && *ptr1 != '\0') {
+ ptr2 = strchr(ptr1, CHAR_DELIMITOR);
+ if (ptr2)
+ *ptr2++ = '\0';
+
+ if (descending) {
+ gtk_combo_box_text_prepend_text(GTK_COMBO_BOX_TEXT(combo), ptr1);
+ if (!found && g_strcmp0(ptr1, def) == 0) {
+ gtk_combo_box_set_active(GTK_COMBO_BOX(combo), 0);
+ found = TRUE;
+ }
+ }else {
+ gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo), ptr1);
+ if (!found && g_strcmp0(ptr1, def) == 0) {
+ gtk_combo_box_set_active(GTK_COMBO_BOX(combo), i);
+ found = TRUE;
+ }
+ }
+
+ ptr1 = ptr2;
+ i++;
+ }
+
+ g_free(buf);
+ }
+
+ if (!found && def && def[0] != '\0') {
+ gtk_entry_set_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(combo))), def);
+ }
+
+ return combo;
+}
+
+GtkWidget*
+remmina_public_create_combo_text_d(const gchar *text, const gchar *def, const gchar *empty_choice)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *combo;
+ GtkListStore *store;
+ GtkCellRenderer *text_renderer;
+
+ store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
+ combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
+
+ text_renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_end(GTK_CELL_LAYOUT(combo), text_renderer, TRUE);
+ gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(combo), text_renderer, "text", 1);
+
+ remmina_public_load_combo_text_d(combo, text, def, empty_choice);
+
+ return combo;
+}
+
+void remmina_public_load_combo_text_d(GtkWidget *combo, const gchar *text, const gchar *def, const gchar *empty_choice)
+{
+ TRACE_CALL(__func__);
+ GtkListStore *store;
+ GtkTreeIter iter;
+ gint i;
+ gchar *buf, *ptr1, *ptr2;
+
+ store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+ gtk_list_store_clear(store);
+
+ i = 0;
+
+ if (empty_choice) {
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, "", 1, empty_choice, -1);
+ gtk_combo_box_set_active(GTK_COMBO_BOX(combo), i);
+ i++;
+ }
+
+ if (text == NULL || text[0] == '\0')
+ return;
+
+ buf = g_strdup(text);
+ ptr1 = buf;
+ while (ptr1 && *ptr1 != '\0') {
+ ptr2 = strchr(ptr1, CHAR_DELIMITOR);
+ if (ptr2)
+ *ptr2++ = '\0';
+
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(store, &iter, 0, ptr1, 1, ptr1, -1);
+
+ if (i == 0 || g_strcmp0(ptr1, def) == 0) {
+ gtk_combo_box_set_active(GTK_COMBO_BOX(combo), i);
+ }
+
+ i++;
+ ptr1 = ptr2;
+ }
+
+ g_free(buf);
+}
+
+GtkWidget*
+remmina_public_create_combo(gboolean use_icon)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *combo;
+ GtkListStore *store;
+ GtkCellRenderer *renderer;
+
+ if (use_icon) {
+ store = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+ }else {
+ store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
+ }
+ combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
+ gtk_widget_set_hexpand(combo, TRUE);
+
+ if (use_icon) {
+ renderer = gtk_cell_renderer_pixbuf_new();
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, FALSE);
+ gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(combo), renderer, "icon-name", 2);
+ }
+ renderer = gtk_cell_renderer_text_new();
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE);
+ gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(combo), renderer, "text", 1);
+ if (use_icon)
+ g_object_set(G_OBJECT(renderer), "xpad", 5, NULL);
+
+ return combo;
+}
+
+GtkWidget*
+remmina_public_create_combo_map(const gpointer *key_value_list, const gchar *def, gboolean use_icon, const gchar *domain)
+{
+ TRACE_CALL(__func__);
+ gint i;
+ GtkWidget *combo;
+ GtkListStore *store;
+ GtkTreeIter iter;
+
+ combo = remmina_public_create_combo(use_icon);
+ store = GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(combo)));
+
+ for (i = 0; key_value_list[i]; i += (use_icon ? 3 : 2)) {
+ gtk_list_store_append(store, &iter);
+ gtk_list_store_set(
+ store,
+ &iter,
+ 0,
+ key_value_list[i],
+ 1,
+ key_value_list[i + 1] && ((char*)key_value_list[i + 1])[0] ?
+ g_dgettext(domain, key_value_list[i + 1]) : "", -1);
+ if (use_icon) {
+ gtk_list_store_set(store, &iter, 2, key_value_list[i + 2], -1);
+ }
+ if (i == 0 || g_strcmp0(key_value_list[i], def) == 0) {
+ gtk_combo_box_set_active(GTK_COMBO_BOX(combo), i / (use_icon ? 3 : 2));
+ }
+ }
+ return combo;
+}
+
+GtkWidget*
+remmina_public_create_combo_mapint(const gpointer *key_value_list, gint def, gboolean use_icon, const gchar *domain)
+{
+ TRACE_CALL(__func__);
+ gchar buf[20];
+ g_snprintf(buf, sizeof(buf), "%i", def);
+ return remmina_public_create_combo_map(key_value_list, buf, use_icon, domain);
+}
+
+void remmina_public_create_group(GtkGrid *grid, const gchar *group, gint row, gint rows, gint cols)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+ gchar *str;
+
+ widget = gtk_label_new(NULL);
+ gtk_widget_show(widget);
+ gtk_widget_set_halign(GTK_WIDGET(widget), GTK_ALIGN_START);
+ gtk_widget_set_valign(GTK_WIDGET(widget), GTK_ALIGN_CENTER);
+ str = g_markup_printf_escaped("<b>%s</b>", group);
+ gtk_label_set_markup(GTK_LABEL(widget), str);
+ g_free(str);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row, 1, 2);
+
+ widget = gtk_label_new(NULL);
+ gtk_widget_show(widget);
+ gtk_grid_attach(GTK_GRID(grid), widget, 0, row + 1, 1, 1);
+}
+
+gchar*
+remmina_public_combo_get_active_text(GtkComboBox *combo)
+{
+ TRACE_CALL(__func__);
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ gchar *s;
+
+ if (GTK_IS_COMBO_BOX_TEXT(combo)) {
+ return gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(combo));
+ }
+
+ if (!gtk_combo_box_get_active_iter(combo, &iter))
+ return NULL;
+
+ model = gtk_combo_box_get_model(combo);
+ gtk_tree_model_get(model, &iter, 0, &s, -1);
+
+ return s;
+}
+
+#if !GTK_CHECK_VERSION(3, 22, 0)
+void remmina_public_popup_position(GtkMenu *menu, gint *x, gint *y, gboolean *push_in, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+ gint tx, ty;
+ GtkAllocation allocation;
+
+ widget = GTK_WIDGET(user_data);
+ if (gtk_widget_get_window(widget) == NULL) {
+ *x = 0;
+ *y = 0;
+ *push_in = TRUE;
+ return;
+ }
+ gdk_window_get_origin(gtk_widget_get_window(widget), &tx, &ty);
+ gtk_widget_get_allocation(widget, &allocation);
+ /* I’m unsure why the author made the check about a GdkWindow inside the
+ * widget argument. This function generally is called passing by a ToolButton
+ * which hasn’t any GdkWindow, therefore the positioning is wrong
+ * I think the gtk_widget_get_has_window() check should be removed
+ *
+ * While leaving the previous check intact I’m checking also if the provided
+ * widget is a GtkToggleToolButton and position the menu accordingly. */
+ if (gtk_widget_get_has_window(widget) ||
+ g_strcmp0(gtk_widget_get_name(widget), "GtkToggleToolButton") == 0) {
+ tx += allocation.x;
+ ty += allocation.y;
+ }
+
+ *x = tx;
+ *y = ty + allocation.height - 1;
+ *push_in = TRUE;
+}
+#endif
+
+gchar*
+remmina_public_combine_path(const gchar *path1, const gchar *path2)
+{
+ TRACE_CALL(__func__);
+ if (!path1 || path1[0] == '\0')
+ return g_strdup(path2);
+ if (path1[strlen(path1) - 1] == '/')
+ return g_strdup_printf("%s%s", path1, path2);
+ return g_strdup_printf("%s/%s", path1, path2);
+}
+
+//static int remmina_public_open_unix_sock(const char *unixsock, GError **error)
+gint remmina_public_open_unix_sock(const char *unixsock)
+{
+ struct sockaddr_un addr;
+ int fd;
+
+ if (strlen(unixsock) + 1 > sizeof(addr.sun_path)) {
+ //g_set_error(error, REMMINA_ERROR, REMMINA_ERROR_FAILED,
+ g_debug(_("Address is too long for UNIX socket_path: %s"), unixsock);
+ return -1;
+ }
+
+ memset(&addr, 0, sizeof addr);
+ addr.sun_family = AF_UNIX;
+ strcpy(addr.sun_path, unixsock);
+
+ if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
+ //g_set_error(error, REMMINA_ERROR, REMMINA_ERROR_FAILED,
+ g_debug(_("Creating UNIX socket failed: %s"), g_strerror(errno));
+ return -1;
+ }
+
+ if (connect(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
+ //g_set_error(error, REMMINA_ERROR, REMMINA_ERROR_FAILED,
+ g_debug(_("Connecting to UNIX socket failed: %s"), g_strerror(errno));
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+void remmina_public_get_server_port_old(const gchar *server, gint defaultport, gchar **host, gint *port)
+{
+ TRACE_CALL(__func__);
+ gchar *str, *ptr, *ptr2;
+
+ str = g_strdup(server);
+
+ if (str) {
+ /* [server]:port format */
+ ptr = strchr(str, '[');
+ if (ptr) {
+ ptr++;
+ ptr2 = strchr(ptr, ']');
+ if (ptr2) {
+ *ptr2++ = '\0';
+ if (*ptr2 == ':')
+ defaultport = atoi(ptr2 + 1);
+ }
+ if (host)
+ *host = g_strdup(ptr);
+ if (port)
+ *port = defaultport;
+ g_free(str);
+ g_debug ("(%s) - host: %s", __func__, *host);
+ g_debug ("(%s) - port: %d", __func__, *port);
+ return;
+ }
+
+ /* server:port format, IPv6 cannot use this format */
+ ptr = strchr(str, ':');
+ if (ptr) {
+ ptr2 = strchr(ptr + 1, ':');
+ if (ptr2 == NULL) {
+ *ptr++ = '\0';
+ defaultport = atoi(ptr);
+ }
+ /* More than one ':' means this is IPv6 address. Treat it as a whole address */
+ }
+ }
+
+ if (host)
+ *host = str;
+ else
+ g_free(str);
+ if (port)
+ *port = defaultport;
+
+ g_debug ("(%s) - host: %s", __func__, *host);
+ g_debug ("(%s) - port: %d", __func__, *port);
+}
+
+void remmina_public_get_server_port(const gchar *server, gint defaultport, gchar **host, gint *port)
+{
+ TRACE_CALL(__func__);
+
+ const gchar *nul_terminated_server = NULL;
+ if (server != NULL) {
+ if(strstr(g_strdup(server), "ID:") != NULL) {
+ g_debug ("(%s) - Using remmina_public_get_server_port_old to parse the repeater ID", __func__);
+ remmina_public_get_server_port_old (server, defaultport, host, port);
+ return;
+ }
+
+ GNetworkAddress *address;
+ GError *err = NULL;
+
+ nul_terminated_server = g_strdup (server);
+ g_debug ("(%s) - Parsing server: %s, default port: %d", __func__, server, defaultport);
+ address = (GNetworkAddress*)g_network_address_parse ((const gchar *)nul_terminated_server, defaultport, &err);
+
+ if (address == NULL) {
+ g_debug ("(%s) - Error converting server string: %s, with error: %s", __func__, nul_terminated_server, err->message);
+ *host = NULL;
+ if (err)
+ g_error_free(err);
+ } else {
+
+ *host = g_strdup(g_network_address_get_hostname (address));
+ *port = g_network_address_get_port (address);
+ }
+ } else
+ *host = NULL;
+
+ if (port == 0)
+ *port = defaultport;
+
+ g_debug ("(%s) - host: %s", __func__, *host);
+ g_debug ("(%s) - port: %d", __func__, *port);
+
+ return;
+}
+
+gboolean remmina_public_get_xauth_cookie(const gchar *display, gchar **msg)
+{
+ TRACE_CALL(__func__);
+ gchar buf[200];
+ gchar *out = NULL;
+ gchar *ptr;
+ GError *error = NULL;
+ gboolean ret;
+
+ if (!display)
+ display = gdk_display_get_name(gdk_display_get_default());
+
+ g_snprintf(buf, sizeof(buf), "xauth list %s", display);
+ ret = g_spawn_command_line_sync(buf, &out, NULL, NULL, &error);
+ if (ret) {
+ if ((ptr = g_strrstr(out, "MIT-MAGIC-COOKIE-1")) == NULL) {
+ *msg = g_strdup_printf("xauth returns %s", out);
+ ret = FALSE;
+ }else {
+ ptr += 19;
+ while (*ptr == ' ')
+ ptr++;
+ *msg = g_strndup(ptr, 32);
+ }
+ g_free(out);
+ }else {
+ *msg = g_strdup(error->message);
+ }
+ return ret;
+}
+
+gint remmina_public_open_xdisplay(const gchar *disp)
+{
+ TRACE_CALL(__func__);
+ gchar *display;
+ gchar *ptr;
+ gint port;
+ struct sockaddr_un addr;
+ gint sock = -1;
+
+ display = g_strdup(disp);
+ ptr = g_strrstr(display, ":");
+ if (ptr) {
+ *ptr++ = '\0';
+ /* Assume you are using a local display… might need to implement remote display in the future */
+ if (display[0] == '\0' || strcmp(display, "unix") == 0) {
+ port = atoi(ptr);
+ sock = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (sock >= 0) {
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ snprintf(addr.sun_path, sizeof(addr.sun_path), X_UNIX_SOCKET, port);
+ if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
+ close(sock);
+ sock = -1;
+ }
+ }
+ }
+ }
+
+ g_free(display);
+ return sock;
+}
+
+/* Find hardware keycode for the requested keyval */
+guint16 remmina_public_get_keycode_for_keyval(GdkKeymap *keymap, guint keyval)
+{
+ TRACE_CALL(__func__);
+ GdkKeymapKey *keys = NULL;
+ gint length = 0;
+ guint16 keycode = 0;
+
+ if (gdk_keymap_get_entries_for_keyval(keymap, keyval, &keys, &length)) {
+ keycode = keys[0].keycode;
+ g_free(keys);
+ }
+ return keycode;
+}
+
+/* Check if the requested keycode is a key modifier */
+gboolean remmina_public_get_modifier_for_keycode(GdkKeymap *keymap, guint16 keycode)
+{
+ TRACE_CALL(__func__);
+ //g_return_val_if_fail(keycode > 0, FALSE);
+ if (keycode > 0) return FALSE;
+#ifdef GDK_WINDOWING_X11
+ return gdk_x11_keymap_key_is_modifier(keymap, keycode);
+#else
+ return FALSE;
+#endif
+}
+
+/* Load a GtkBuilder object from a filename */
+GtkBuilder* remmina_public_gtk_builder_new_from_file(gchar *filename)
+{
+ TRACE_CALL(__func__);
+ GError *err = NULL;
+ gchar *ui_path = g_strconcat(REMMINA_RUNTIME_UIDIR, G_DIR_SEPARATOR_S, filename, NULL);
+ GtkBuilder *builder = gtk_builder_new();
+ gtk_builder_add_from_file(builder, ui_path, &err);
+ if (err != NULL) {
+ g_print("Error adding build from file. Error: %s", err->message);
+ g_error_free(err);
+ }
+ g_free(ui_path);
+ return builder;
+}
+
+/* Load a GtkBuilder object from a resource */
+GtkBuilder* remmina_public_gtk_builder_new_from_resource(gchar *resource)
+{
+ TRACE_CALL(__func__);
+ GError *err = NULL;
+ GtkBuilder *builder = gtk_builder_new();
+ gtk_builder_add_from_resource (builder, resource, &err);
+ //GtkBuilder *builder = gtk_builder_new_from_resource (resource);
+ if (err != NULL) {
+ g_print("Error adding build from resource. Error: %s", err->message);
+ g_error_free(err);
+ }
+ return builder;
+}
+
+/* Change parent container for a widget
+ * If possible use this function instead of the deprecated gtk_widget_reparent */
+void remmina_public_gtk_widget_reparent(GtkWidget *widget, GtkContainer *container)
+{
+ TRACE_CALL(__func__);
+ g_object_ref(widget);
+ gtk_container_remove(GTK_CONTAINER(gtk_widget_get_parent(widget)), widget);
+ gtk_container_add(container, widget);
+ g_object_unref(widget);
+}
+
+/* Validate the inserted value for a new resolution */
+gboolean remmina_public_resolution_validation_func(const gchar *new_str, gchar **error)
+{
+ TRACE_CALL(__func__);
+ gint i;
+ gint width, height;
+ gboolean splitted;
+ gboolean result;
+
+ width = 0;
+ height = 0;
+ splitted = FALSE;
+ result = TRUE;
+ for (i = 0; new_str[i] != '\0'; i++) {
+ if (new_str[i] == 'x') {
+ if (splitted) {
+ result = FALSE;
+ break;
+ }
+ splitted = TRUE;
+ continue;
+ }
+ if (new_str[i] < '0' || new_str[i] > '9') {
+ result = FALSE;
+ break;
+ }
+ if (splitted) {
+ height = 1;
+ }else {
+ width = 1;
+ }
+ }
+
+ if (width == 0 || height == 0)
+ result = FALSE;
+
+ if (!result)
+ *error = g_strdup(_("Please enter format 'widthxheight'."));
+ return result;
+}
+
+/* Used to send desktop notifications */
+void remmina_public_send_notification(const gchar *notification_id,
+ const gchar *notification_title, const gchar *notification_message)
+{
+ TRACE_CALL(__func__);
+
+ g_autoptr(GNotification) n = NULL;
+ gint priority = G_NOTIFICATION_PRIORITY_NORMAL;
+
+ n = g_notification_new(notification_title);
+ g_notification_set_body(n, notification_message);
+ if (g_strcmp0 (notification_id, "remmina-security-trust-all-id") == 0) {
+ g_debug ("remmina_public_send_notification: We got a remmina-security-trust-all-id notification");
+ priority = G_NOTIFICATION_PRIORITY_HIGH;
+ /** parameter 5 is the tab index for the security tab in the preferences
+ * TODO: Do not hardcode the parameter
+ * TODO: Do not hardcode implement DBus interface correctly of this won't work*/
+ g_notification_set_default_action_and_target (n, "app.preferences", "i", 5);
+ g_notification_add_button_with_target (n, _("Change security settings"), "app.preferences", "i", 5);
+ }
+#if GLIB_CHECK_VERSION(2, 42, 0)
+ g_notification_set_priority(n, priority);
+#endif
+ g_application_send_notification(g_application_get_default(), notification_id, n);
+}
+
+/* Replaces all occurrences of search in a new copy of string by replacement. */
+gchar* remmina_public_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;
+}
+
+/* Replaces all occurrences of search in a new copy of string by replacement
+ * and overwrites the original string */
+gchar* remmina_public_str_replace_in_place(gchar *string, const gchar *search, const gchar *replacement)
+{
+ TRACE_CALL(__func__);
+ gchar *new_string = remmina_public_str_replace(string, search, replacement);
+ string = g_strdup(new_string);
+ g_free(new_string);
+ return string;
+}
+
+int remmina_public_split_resolution_string(const char *resolution_string, int *w, int *h)
+{
+ int lw, lh;
+
+ if (resolution_string == NULL || resolution_string[0] == 0)
+ return 0;
+ if (sscanf(resolution_string, "%dx%d", &lw, &lh) != 2)
+ return 0;
+ *w = lw;
+ *h = lh;
+ return 1;
+}
+
+/* Return TRUE if current gtk version library in use is greater or equal than
+ * the required major.minor.micro */
+gboolean remmina_gtk_check_version(guint major, guint minor, guint micro)
+{
+ guint rtmajor, rtminor, rtmicro;
+ rtmajor = gtk_get_major_version();
+ if (rtmajor > major) {
+ return TRUE;
+ }else if (rtmajor == major) {
+ rtminor = gtk_get_minor_version();
+ if (rtminor > minor) {
+ return TRUE;
+ }else if (rtminor == minor) {
+ rtmicro = gtk_get_micro_version();
+ if (rtmicro >= micro) {
+ return TRUE;
+ }else {
+ return FALSE;
+ }
+ }else {
+ return FALSE;
+ }
+ }else {
+ return FALSE;
+ }
+}
diff --git a/src/remmina_public.h b/src/remmina_public.h
new file mode 100644
index 0000000..49ad2fc
--- /dev/null
+++ b/src/remmina_public.h
@@ -0,0 +1,126 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 "config.h"
+#include <gtk/gtk.h>
+
+#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_PATH_LEN 255
+
+#define MAX_X_DISPLAY_NUMBER 99
+#define X_UNIX_SOCKET "/tmp/.X11-unix/X%d"
+
+#define CHAR_DELIMITOR ','
+#define STRING_DELIMITOR ","
+#define STRING_DELIMITOR2 "§"
+
+#define MOUSE_BUTTON_LEFT 1
+#define MOUSE_BUTTON_MIDDLE 2
+#define MOUSE_BUTTON_RIGHT 3
+
+/* Bind a template widget to its class member and callback */
+#define BIND_TEMPLATE_CHILD(wc, type, action, callback) \
+ gtk_widget_class_bind_template_child(wc, type, action); \
+ gtk_widget_class_bind_template_callback(wc, callback);
+
+G_BEGIN_DECLS
+
+/* items is separated by STRING_DELIMTOR */
+GtkWidget *remmina_public_create_combo_entry(const gchar *text, const gchar *def, gboolean descending);
+GtkWidget *remmina_public_create_combo_text_d(const gchar *text, const gchar *def, const gchar *empty_choice);
+void remmina_public_load_combo_text_d(GtkWidget *combo, const gchar *text, const gchar *def, const gchar *empty_choice);
+GtkWidget *remmina_public_create_combo(gboolean use_icon);
+GtkWidget *remmina_public_create_combo_map(const gpointer *key_value_list, const gchar *def, gboolean use_icon, const gchar *domain);
+GtkWidget *remmina_public_create_combo_mapint(const gpointer *key_value_list, gint def, gboolean use_icon, const gchar *domain);
+
+void remmina_public_create_group(GtkGrid *grid, const gchar *group, gint row, gint rows, gint cols);
+
+gchar *remmina_public_combo_get_active_text(GtkComboBox *combo);
+
+#if !GTK_CHECK_VERSION(3, 22, 0)
+/* A function for gtk_menu_popup to get the position right below the widget specified by user_data */
+void remmina_public_popup_position(GtkMenu *menu, gint *x, gint *y, gboolean *push_in, gpointer user_data);
+#endif
+
+/* Combine two paths into one by correctly handling trailing slash. Return newly allocated string */
+gchar *remmina_public_combine_path(const gchar *path1, const gchar *path2);
+
+/**
+ * Return a file descriptor handle for a unix socket
+ * @return a file descriptor (socket) or -1
+ *
+ * */
+gint remmina_public_open_unix_sock(const char *unixsock);
+
+/* Parse a server entry with server name and port */
+void remmina_public_get_server_port_old(const gchar *server, gint defaultport, gchar **host, gint *port);
+void remmina_public_get_server_port(const gchar *server, gint defaultport, gchar **host, gint *port);
+
+/* X */
+gboolean remmina_public_get_xauth_cookie(const gchar *display, gchar **msg);
+gint remmina_public_open_xdisplay(const gchar *disp);
+
+/* Find hardware keycode for the requested keyval */
+guint16 remmina_public_get_keycode_for_keyval(GdkKeymap *keymap, guint keyval);
+/* Check if the requested keycode is a key modifier */
+gboolean remmina_public_get_modifier_for_keycode(GdkKeymap *keymap, guint16 keycode);
+/* Load a GtkBuilder object from a filename */
+GtkBuilder *remmina_public_gtk_builder_new_from_file(gchar *filename);
+/* Load a GtkBuilder object from a resource */
+GtkBuilder *remmina_public_gtk_builder_new_from_resource(gchar *resource);
+/* Change parent container for a widget */
+void remmina_public_gtk_widget_reparent(GtkWidget *widget, GtkContainer *container);
+/* Used to send desktop notifications */
+void remmina_public_send_notification(const gchar *notification_id, const gchar *notification_title, const gchar *notification_message);
+/* Validate the inserted value for a new resolution */
+gboolean remmina_public_resolution_validation_func(const gchar *new_str, gchar **error);
+/* Replaces all occurrences of search in a new copy of string by replacement. */
+gchar *remmina_public_str_replace(const gchar *string, const gchar *search, const gchar *replacement);
+/* Replaces all occurrences of search in a new copy of string by replacement
+ * and overwrites the original string */
+gchar *remmina_public_str_replace_in_place(gchar *string, const gchar *search, const gchar *replacement);
+int remmina_public_split_resolution_string(const char *resolution_string, int *w, int *h);
+gboolean remmina_gtk_check_version(guint major, guint minor, guint micro);
diff --git a/src/remmina_scheduler.c b/src/remmina_scheduler.c
new file mode 100644
index 0000000..bdabc71
--- /dev/null
+++ b/src/remmina_scheduler.c
@@ -0,0 +1,90 @@
+/*
+ * 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 <stdlib.h>
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+#include <string.h>
+#include "remmina_scheduler.h"
+#include "remmina/remmina_trace_calls.h"
+
+static gboolean remmina_scheduler_periodic_check(gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ rsSchedData *rssd = (rsSchedData *)user_data;
+
+ rssd->count++;
+ if (rssd->cb_func_ptr(rssd->cb_func_data) == G_SOURCE_REMOVE) {
+ g_free(rssd);
+ return G_SOURCE_REMOVE;
+ }
+ if (rssd->count <= 1) {
+ rssd->source = g_timeout_add_full(G_PRIORITY_LOW,
+ rssd->interval,
+ remmina_scheduler_periodic_check,
+ rssd,
+ NULL);
+ return G_SOURCE_REMOVE;
+ }
+ return G_SOURCE_CONTINUE;
+}
+
+void *remmina_scheduler_setup(GSourceFunc cb,
+ gpointer cb_data,
+ guint first_interval,
+ guint interval)
+{
+ TRACE_CALL(__func__);
+ rsSchedData *rssd;
+ rssd = g_malloc(sizeof(rsSchedData));
+ rssd->cb_func_ptr = cb;
+ rssd->cb_func_data = cb_data;
+ rssd->interval = interval;
+ rssd->count = 0;
+ rssd->source = g_timeout_add_full(G_PRIORITY_LOW,
+ first_interval,
+ remmina_scheduler_periodic_check,
+ rssd,
+ NULL);
+ return (void *)rssd;
+}
+
+void remmina_schedluer_remove(void *s)
+{
+ TRACE_CALL(__func__);
+ rsSchedData *rssd = (rsSchedData *)s;
+ g_source_remove(rssd->source);
+ g_free(rssd);
+}
diff --git a/src/remmina_scheduler.h b/src/remmina_scheduler.h
new file mode 100644
index 0000000..10809a4
--- /dev/null
+++ b/src/remmina_scheduler.h
@@ -0,0 +1,51 @@
+/*
+ * 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 <glib.h>
+
+typedef struct {
+ GSourceFunc cb_func_ptr;
+ gpointer cb_func_data;
+ guint interval;
+ guint source;
+ guint count;
+} rsSchedData;
+
+G_BEGIN_DECLS
+
+void *remmina_scheduler_setup(GSourceFunc cb, gpointer cb_data, guint first_interval, guint interval);
+
+G_END_DECLS
diff --git a/src/remmina_scrolled_viewport.c b/src/remmina_scrolled_viewport.c
new file mode 100644
index 0000000..cb858e8
--- /dev/null
+++ b/src/remmina_scrolled_viewport.c
@@ -0,0 +1,218 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <gtk/gtk.h>
+#include "config.h"
+#include "remmina_scrolled_viewport.h"
+#include "remmina_pref.h"
+#include "remmina_log.h"
+#include "remmina/remmina_trace_calls.h"
+
+G_DEFINE_TYPE( RemminaScrolledViewport, remmina_scrolled_viewport, GTK_TYPE_EVENT_BOX)
+
+static void remmina_scrolled_viewport_get_preferred_width(GtkWidget* widget, gint* minimum_width, gint* natural_width)
+{
+ TRACE_CALL(__func__);
+ /* Just return a fake small size, so gtk_window_fullscreen() will not fail
+ * because our content is too big*/
+ if (minimum_width != NULL) *minimum_width = 100;
+ if (natural_width != NULL) *natural_width = 100;
+}
+
+static void remmina_scrolled_viewport_get_preferred_height(GtkWidget* widget, gint* minimum_height, gint* natural_height)
+{
+ TRACE_CALL(__func__);
+ /* Just return a fake small size, so gtk_window_fullscreen() will not fail
+ * because our content is too big*/
+ if (minimum_height != NULL) *minimum_height = 100;
+ if (natural_height != NULL) *natural_height = 100;
+}
+
+/* Event handler when mouse move on borders
+ * Note that this handler may repeat itself. Zeroing out viewport_motion_handler before returning FALSE will
+ * relay the fact that this specific occurrence of timeout has been cancelled.
+ * A new one may be scheduled if the mouse pointer moves to the edge again.
+ */
+static gboolean remmina_scrolled_viewport_motion_timeout(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaScrolledViewport *gsv;
+ GtkWidget *child;
+ GdkDisplay *display;
+#if GTK_CHECK_VERSION(3, 20, 0)
+ GdkSeat *seat;
+#else
+ GdkDeviceManager *device_manager;
+#endif
+ GdkDevice *pointer;
+ GdkScreen *screen;
+ GdkWindow *gsvwin;
+ gint x, y, mx, my, w, h, rootx, rooty;
+ GtkAdjustment *adj;
+ gdouble value;
+
+ gsv = REMMINA_SCROLLED_VIEWPORT(data);
+ if (!gsv || !gsv->viewport_motion_handler)
+ // Either the pointer is nullptr or the source id is already 0
+ return FALSE;
+ if (!REMMINA_IS_SCROLLED_VIEWPORT(data)) {
+ gsv->viewport_motion_handler = 0;
+ return FALSE;
+ }
+ if (!GTK_IS_BIN(data)) {
+ gsv->viewport_motion_handler = 0;
+ return FALSE;
+ }
+
+ child = gtk_bin_get_child(GTK_BIN(gsv));
+ if (!GTK_IS_VIEWPORT(child)) {
+ gsv->viewport_motion_handler = 0;
+ return FALSE;
+ }
+
+ gsvwin = gtk_widget_get_window(GTK_WIDGET(gsv));
+ display = gdk_display_get_default();
+ if (!display) {
+ gsv->viewport_motion_handler = 0;
+ return FALSE;
+ }
+
+#if GTK_CHECK_VERSION(3, 20, 0)
+ seat = gdk_display_get_default_seat(display);
+ pointer = gdk_seat_get_pointer(seat);
+#else
+ device_manager = gdk_display_get_device_manager(display);
+ pointer = gdk_device_manager_get_client_pointer(device_manager);
+#endif
+ gdk_device_get_position(pointer, &screen, &x, &y);
+
+ w = gdk_window_get_width(gsvwin) + SCROLL_BORDER_SIZE; // Add 2px of black scroll border
+ h = gdk_window_get_height(gsvwin) + SCROLL_BORDER_SIZE; // Add 2px of black scroll border
+
+ gdk_window_get_root_origin(gsvwin, &rootx, &rooty );
+
+ x -= rootx;
+ y -= rooty;
+
+ mx = (x <= 0 ? -1 : (x >= w - 1 ? 1 : 0));
+ my = (y <= 0 ? -1 : (y >= h - 1 ? 1 : 0));
+ if (mx != 0) {
+ gint step = MAX(10, MIN(remmina_pref.auto_scroll_step, w / 5));
+ adj = gtk_scrollable_get_hadjustment(GTK_SCROLLABLE(child));
+ value = gtk_adjustment_get_value(GTK_ADJUSTMENT(adj)) + (gdouble)(mx * step);
+ value = MAX(0, MIN(value, gtk_adjustment_get_upper(GTK_ADJUSTMENT(adj)) - (gdouble)w + 2.0));
+ gtk_adjustment_set_value(GTK_ADJUSTMENT(adj), value);
+ }
+ if (my != 0) {
+ gint step = MAX(10, MIN(remmina_pref.auto_scroll_step, h / 5));
+ adj = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(child));
+ value = gtk_adjustment_get_value(GTK_ADJUSTMENT(adj)) + (gdouble)(my * step);
+ value = MAX(0, MIN(value, gtk_adjustment_get_upper(GTK_ADJUSTMENT(adj)) - (gdouble)h + 2.0));
+ gtk_adjustment_set_value(GTK_ADJUSTMENT(adj), value);
+ }
+ return TRUE;
+}
+
+static gboolean remmina_scrolled_viewport_enter(GtkWidget *widget, GdkEventCrossing *event, gpointer data)
+{
+ TRACE_CALL(__func__);
+ remmina_scrolled_viewport_remove_motion(REMMINA_SCROLLED_VIEWPORT(widget));
+ return FALSE;
+}
+
+static gboolean remmina_scrolled_viewport_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaScrolledViewport *gsv = REMMINA_SCROLLED_VIEWPORT(widget);
+ if (gsv->viewport_motion_handler) {
+ REMMINA_DEBUG("cleaning motion ...");
+ remmina_scrolled_viewport_remove_motion(gsv);
+ }
+ gsv->viewport_motion_handler = g_timeout_add(20, remmina_scrolled_viewport_motion_timeout, gsv);
+ return FALSE;
+}
+
+static void remmina_scrolled_viewport_destroy(GtkWidget *widget, gpointer data)
+{
+ TRACE_CALL(__func__);
+ remmina_scrolled_viewport_remove_motion(REMMINA_SCROLLED_VIEWPORT(widget));
+}
+
+static void remmina_scrolled_viewport_class_init(RemminaScrolledViewportClass *klass)
+{
+ TRACE_CALL(__func__);
+ GtkWidgetClass *widget_class;
+ widget_class = (GtkWidgetClass*)klass;
+
+ widget_class->get_preferred_width = remmina_scrolled_viewport_get_preferred_width;
+ widget_class->get_preferred_height = remmina_scrolled_viewport_get_preferred_height;
+
+}
+
+static void remmina_scrolled_viewport_init(RemminaScrolledViewport *gsv)
+{
+ TRACE_CALL(__func__);
+}
+
+void remmina_scrolled_viewport_remove_motion(RemminaScrolledViewport *gsv)
+{
+ TRACE_CALL(__func__);
+ guint handler = gsv->viewport_motion_handler;
+ if (handler) {
+ gsv->viewport_motion_handler = 0;
+ g_source_remove(handler);
+ }
+}
+
+GtkWidget*
+remmina_scrolled_viewport_new(void)
+{
+ TRACE_CALL(__func__);
+ RemminaScrolledViewport *gsv;
+
+ gsv = REMMINA_SCROLLED_VIEWPORT(g_object_new(REMMINA_TYPE_SCROLLED_VIEWPORT, NULL));
+
+ gsv->viewport_motion_handler = 0;
+
+ gtk_widget_set_size_request(GTK_WIDGET(gsv), 1, 1);
+ gtk_widget_add_events(GTK_WIDGET(gsv), GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
+ g_signal_connect(G_OBJECT(gsv), "destroy", G_CALLBACK(remmina_scrolled_viewport_destroy), NULL);
+ g_signal_connect(G_OBJECT(gsv), "enter-notify-event", G_CALLBACK(remmina_scrolled_viewport_enter), NULL);
+ g_signal_connect(G_OBJECT(gsv), "leave-notify-event", G_CALLBACK(remmina_scrolled_viewport_leave), NULL);
+
+ return GTK_WIDGET(gsv);
+}
+
diff --git a/src/remmina_scrolled_viewport.h b/src/remmina_scrolled_viewport.h
new file mode 100644
index 0000000..39088d4
--- /dev/null
+++ b/src/remmina_scrolled_viewport.h
@@ -0,0 +1,74 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define REMMINA_TYPE_SCROLLED_VIEWPORT \
+ (remmina_scrolled_viewport_get_type())
+#define REMMINA_SCROLLED_VIEWPORT(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_SCROLLED_VIEWPORT, RemminaScrolledViewport))
+#define REMMINA_SCROLLED_VIEWPORT_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_SCROLLED_VIEWPORT, RemminaScrolledViewportClass))
+#define REMMINA_IS_SCROLLED_VIEWPORT(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_SCROLLED_VIEWPORT))
+#define REMMINA_IS_SCROLLED_VIEWPORT_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_SCROLLED_VIEWPORT))
+#define REMMINA_SCROLLED_VIEWPORT_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_SCROLLED_VIEWPORT, RemminaScrolledViewportClass))
+
+typedef struct _RemminaScrolledViewport {
+ GtkEventBox event_box;
+
+ /* Motion activates in Viewport Fullscreen mode */
+ guint viewport_motion_handler;
+} RemminaScrolledViewport;
+
+typedef struct _RemminaScrolledViewportClass {
+ GtkEventBoxClass parent_class;
+} RemminaScrolledViewportClass;
+
+GType remmina_scrolled_viewport_get_type(void)
+G_GNUC_CONST;
+
+GtkWidget *remmina_scrolled_viewport_new(void);
+void remmina_scrolled_viewport_remove_motion(RemminaScrolledViewport *gsv);
+
+#define SCROLL_BORDER_SIZE 1
+
+G_END_DECLS
diff --git a/src/remmina_sftp_client.c b/src/remmina_sftp_client.c
new file mode 100644
index 0000000..1f1e0b9
--- /dev/null
+++ b/src/remmina_sftp_client.c
@@ -0,0 +1,1057 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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.
+ *
+ */
+
+#define _FILE_OFFSET_BITS 64
+#include "config.h"
+
+#ifdef HAVE_LIBSSH
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <gmodule.h>
+#include <pthread.h>
+#ifdef HAVE_FCNTL_H
+#include <fcntl.h>
+#endif
+#include "remmina_public.h"
+#include "remmina_log.h"
+#include "remmina_pref.h"
+#include "remmina_ssh.h"
+#include "remmina_sftp_client.h"
+#include "remmina_sftp_plugin.h"
+#include "remmina_masterthread_exec.h"
+#include "remmina/remmina_trace_calls.h"
+
+G_DEFINE_TYPE(RemminaSFTPClient, remmina_sftp_client, REMMINA_TYPE_FTP_CLIENT)
+
+#define SET_CURSOR(cur) \
+ if (GDK_IS_WINDOW(gtk_widget_get_window(GTK_WIDGET(client)))) \
+ { \
+ gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(client)), cur); \
+ }
+
+static void
+remmina_sftp_client_class_init(RemminaSFTPClientClass *klass)
+{
+ TRACE_CALL(__func__);
+}
+
+#define GET_SFTPATTR_TYPE(a, type) \
+ if (a->type == 0) \
+ { \
+ type = ((a->permissions & 040000) ? REMMINA_FTP_FILE_TYPE_DIR : REMMINA_FTP_FILE_TYPE_FILE); \
+ type = ((a->permissions & 0120000) ? REMMINA_FTP_FILE_TYPE_LINK : REMMINA_FTP_FILE_TYPE_FILE); \
+ } \
+ else \
+ { \
+ type = (a->type == SSH_FILEXFER_TYPE_DIRECTORY ? REMMINA_FTP_FILE_TYPE_DIR : REMMINA_FTP_FILE_TYPE_FILE); \
+ if (a->type == SSH_FILEXFER_TYPE_SYMLINK ) type = REMMINA_FTP_FILE_TYPE_LINK; \
+ }
+
+/* ------------------------ The Task Thread routines ----------------------------- */
+
+static gboolean remmina_sftp_client_refresh(RemminaSFTPClient *client);
+
+#define THREAD_CHECK_EXIT \
+ (!client->taskid || client->thread_abort)
+
+
+
+static gboolean
+remmina_sftp_client_thread_update_task(RemminaSFTPClient *client, RemminaFTPTask *task)
+{
+ TRACE_CALL(__func__);
+ if (THREAD_CHECK_EXIT) return FALSE;
+
+ remmina_ftp_client_update_task(REMMINA_FTP_CLIENT(client), task);
+
+ return TRUE;
+}
+
+static void
+remmina_sftp_client_thread_set_error(RemminaSFTPClient *client, RemminaFTPTask *task, const gchar *error_format, ...)
+{
+ TRACE_CALL(__func__);
+ va_list args;
+
+ task->status = REMMINA_FTP_TASK_STATUS_ERROR;
+ g_free(task->tooltip);
+ if (error_format) {
+ va_start(args, error_format);
+ task->tooltip = g_strdup_vprintf(error_format, args);
+ va_end(args);
+ } else {
+ task->tooltip = NULL;
+ }
+
+ remmina_sftp_client_thread_update_task(client, task);
+}
+
+static void
+remmina_sftp_client_thread_set_finish(RemminaSFTPClient *client, RemminaFTPTask *task)
+{
+ TRACE_CALL(__func__);
+ task->status = REMMINA_FTP_TASK_STATUS_FINISH;
+ g_free(task->tooltip);
+ task->tooltip = NULL;
+
+ remmina_sftp_client_thread_update_task(client, task);
+}
+
+static RemminaFTPTask *
+remmina_sftp_client_thread_get_task(RemminaSFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ RemminaFTPTask *task;
+
+ if (client->thread_abort) return NULL;
+
+ task = remmina_ftp_client_get_waiting_task(REMMINA_FTP_CLIENT(client));
+ if (task) {
+ client->taskid = task->taskid;
+
+ task->status = REMMINA_FTP_TASK_STATUS_RUN;
+ remmina_ftp_client_update_task(REMMINA_FTP_CLIENT(client), task);
+ }
+
+ return task;
+}
+
+static gboolean
+remmina_sftp_client_thread_download_file(RemminaSFTPClient *client, RemminaSFTP *sftp, RemminaFTPTask *task,
+ const gchar *remote_path, const gchar *local_path, guint64 *donesize)
+{
+ TRACE_CALL(__func__);
+ sftp_file remote_file;
+ FILE *local_file;
+ gchar *tmp;
+ gchar buf[20480];
+ gint len;
+ gint response;
+ uint64_t size;
+
+ if (THREAD_CHECK_EXIT) return FALSE;
+
+ /* Ensure local dir exists */
+ g_strlcpy(buf, local_path, sizeof(buf));
+ tmp = g_strrstr(buf, "/");
+ if (tmp && tmp != buf) {
+ *tmp = '\0';
+ if (g_mkdir_with_parents(buf, 0755) < 0) {
+ // TRANSLATORS: The placeholder %s is a directory path
+ remmina_sftp_client_thread_set_error(client, task, _("Could not create the folder “%s”."), buf);
+ return FALSE;
+ }
+ }
+
+ local_file = g_fopen(local_path, "ab");
+ if (!local_file) {
+ // TRANSLATORS: The placeholder %s is a file path
+ remmina_sftp_client_thread_set_error(client, task, _("Could not create the file “%s”."), local_path);
+ return FALSE;
+ }
+
+ fseeko(local_file, 0, SEEK_END);
+ size = ftello(local_file);
+ if (size > 0) {
+ response = remmina_sftp_client_confirm_resume(client, local_path);
+
+ switch (response) {
+ case GTK_RESPONSE_CANCEL:
+ case GTK_RESPONSE_DELETE_EVENT:
+ fclose(local_file);
+ remmina_sftp_client_thread_set_error(client, task, NULL);
+ return FALSE;
+
+ case GTK_RESPONSE_ACCEPT:
+ fclose(local_file);
+ local_file = g_fopen(local_path, "wb");
+ if (!local_file) {
+ // TRANSLATORS: The placeholder %s is a file path
+ remmina_sftp_client_thread_set_error(client, task, _("Could not create the file “%s”."), local_path);
+ return FALSE;
+ }
+ size = 0;
+ break;
+
+ case GTK_RESPONSE_APPLY:
+ break;
+ }
+ }
+
+ tmp = remmina_ssh_unconvert(REMMINA_SSH(sftp), remote_path);
+ remote_file = sftp_open(sftp->sftp_sess, tmp, O_RDONLY, 0);
+ g_free(tmp);
+
+ if (!remote_file) {
+ fclose(local_file);
+ // TRANSLATORS: The placeholders %s are a file path, and an error message.
+ remmina_sftp_client_thread_set_error(client, task, _("Could not open the file “%s” on the server. %s"),
+ remote_path, ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ return FALSE;
+ }
+
+ if (size > 0) {
+ if (sftp_seek64(remote_file, size) < 0) {
+ sftp_close(remote_file);
+ fclose(local_file);
+ remmina_sftp_client_thread_set_error(client, task, "Could not download the file “%s”. %s",
+ remote_path, ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ return FALSE;
+ }
+ *donesize = size;
+ }
+
+ while (!THREAD_CHECK_EXIT && (len = sftp_read(remote_file, buf, sizeof(buf))) > 0) {
+ if (THREAD_CHECK_EXIT) break;
+
+ if (fwrite(buf, 1, len, local_file) < len) {
+ sftp_close(remote_file);
+ fclose(local_file);
+ remmina_sftp_client_thread_set_error(client, task, _("Could not save the file “%s”."), local_path);
+ return FALSE;
+ }
+
+ *donesize += (guint64)len;
+ task->donesize = (gfloat)(*donesize);
+
+ if (!remmina_sftp_client_thread_update_task(client, task)) break;
+ }
+
+ sftp_close(remote_file);
+ fclose(local_file);
+ return TRUE;
+}
+
+static gboolean
+remmina_sftp_client_thread_recursive_dir(RemminaSFTPClient *client, RemminaSFTP *sftp, RemminaFTPTask *task,
+ const gchar *rootdir_path, const gchar *subdir_path, GPtrArray *array)
+{
+ TRACE_CALL(__func__);
+ sftp_dir sftpdir;
+ sftp_attributes sftpattr;
+ gchar *tmp;
+ gchar *dir_path;
+ gchar *file_path;
+ gint type;
+ gboolean ret = TRUE;
+
+ if (THREAD_CHECK_EXIT) return FALSE;
+
+ if (subdir_path)
+ dir_path = remmina_public_combine_path(rootdir_path, subdir_path);
+ else
+ dir_path = g_strdup(rootdir_path);
+ tmp = remmina_ssh_unconvert(REMMINA_SSH(sftp), dir_path);
+
+ REMMINA_DEBUG ("%s HELLO", __func__);
+#if 0
+ gchar *tlink = sftp_readlink (sftp->sftp_sess, tmp);
+ if (tlink) {
+ REMMINA_DEBUG ("%s is a link to %s", tmp, tlink);
+ tmp = g_strdup (tlink);
+ }
+ g_free(tlink);
+#endif
+ sftpdir = sftp_opendir(sftp->sftp_sess, tmp);
+ g_free(tmp);
+
+ if (!sftpdir) {
+ remmina_sftp_client_thread_set_error(client, task, _("Could not open the folder “%s”. %s"),
+ dir_path, ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ g_free(dir_path);
+ return FALSE;
+ }
+
+ g_free(dir_path);
+
+ while ((sftpattr = sftp_readdir(sftp->sftp_sess, sftpdir))) {
+ if (g_strcmp0(sftpattr->name, ".") != 0 &&
+ g_strcmp0(sftpattr->name, "..") != 0) {
+ GET_SFTPATTR_TYPE(sftpattr, type);
+
+ tmp = remmina_ssh_convert(REMMINA_SSH(sftp), sftpattr->name);
+ if (subdir_path) {
+ file_path = remmina_public_combine_path(subdir_path, tmp);
+ g_free(tmp);
+ } else {
+ file_path = tmp;
+ }
+
+ if (type == REMMINA_FTP_FILE_TYPE_DIR) {
+ ret = remmina_sftp_client_thread_recursive_dir(client, sftp, task, rootdir_path, file_path, array);
+ g_free(file_path);
+ if (!ret) {
+ sftp_attributes_free(sftpattr);
+ break;
+ }
+ } else {
+ task->size += (gfloat)sftpattr->size;
+ g_ptr_array_add(array, file_path);
+
+ if (!remmina_sftp_client_thread_update_task(client, task)) {
+ sftp_attributes_free(sftpattr);
+ break;
+ }
+ }
+ }
+ sftp_attributes_free(sftpattr);
+
+ if (THREAD_CHECK_EXIT) break;
+ }
+
+ sftp_closedir(sftpdir);
+ return ret;
+}
+
+static gboolean
+remmina_sftp_client_thread_recursive_localdir(RemminaSFTPClient *client, RemminaFTPTask *task,
+ const gchar *rootdir_path, const gchar *subdir_path, GPtrArray *array)
+{
+ TRACE_CALL(__func__);
+ GDir *dir;
+ gchar *path;
+ const gchar *name;
+ gchar *relpath;
+ gchar *abspath;
+ struct stat st;
+ gboolean ret = TRUE;
+
+ path = g_build_filename(rootdir_path, subdir_path, NULL);
+ dir = g_dir_open(path, 0, NULL);
+ if (dir == NULL) {
+ g_free(path);
+ return FALSE;
+ }
+ while ((name = g_dir_read_name(dir)) != NULL) {
+ if (THREAD_CHECK_EXIT) {
+ ret = FALSE;
+ break;
+ }
+ if (g_strcmp0(name, ".") == 0 || g_strcmp0(name, "..") == 0) continue;
+ abspath = g_build_filename(path, name, NULL);
+ if (g_stat(abspath, &st) < 0) {
+ g_free(abspath);
+ continue;
+ }
+ relpath = g_build_filename(subdir_path ? subdir_path : "", name, NULL);
+ g_ptr_array_add(array, relpath);
+ if (g_file_test(abspath, G_FILE_TEST_IS_DIR)) {
+ ret = remmina_sftp_client_thread_recursive_localdir(client, task, rootdir_path, relpath, array);
+ if (!ret) break;
+ } else {
+ task->size += (gfloat)st.st_size;
+ }
+ g_free(abspath);
+ }
+ g_free(path);
+ g_dir_close(dir);
+ return ret;
+}
+
+static gboolean
+remmina_sftp_client_thread_mkdir(RemminaSFTPClient *client, RemminaSFTP *sftp, RemminaFTPTask *task, const gchar *path)
+{
+ TRACE_CALL(__func__);
+ sftp_attributes sftpattr;
+
+ sftpattr = sftp_stat(sftp->sftp_sess, path);
+ if (sftpattr != NULL) {
+ sftp_attributes_free(sftpattr);
+ return TRUE;
+ }
+ if (sftp_mkdir(sftp->sftp_sess, path, 0755) < 0) {
+ remmina_sftp_client_thread_set_error(client, task, _("Could not create the folder “%s” on the server. %s"),
+ path, ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+remmina_sftp_client_thread_upload_file(RemminaSFTPClient *client, RemminaSFTP *sftp, RemminaFTPTask *task,
+ const gchar *remote_path, const gchar *local_path, guint64 *donesize)
+{
+ TRACE_CALL(__func__);
+ sftp_file remote_file;
+ FILE *local_file;
+ gchar *tmp;
+ gchar buf[20480];
+ gint len;
+ sftp_attributes attr;
+ gint response;
+ uint64_t size;
+
+ if (THREAD_CHECK_EXIT) return FALSE;
+
+ tmp = remmina_ssh_unconvert(REMMINA_SSH(sftp), remote_path);
+ remote_file = sftp_open(sftp->sftp_sess, tmp, O_WRONLY | O_CREAT, 0644);
+ g_free(tmp);
+
+ if (!remote_file) {
+ remmina_sftp_client_thread_set_error(client, task, _("Could not create the file “%s” on the server. %s"),
+ remote_path, ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ return FALSE;
+ }
+ attr = sftp_fstat(remote_file);
+ size = attr->size;
+ sftp_attributes_free(attr);
+ if (size > 0) {
+ response = remmina_sftp_client_confirm_resume(client, remote_path);
+ switch (response) {
+ case GTK_RESPONSE_CANCEL:
+ case GTK_RESPONSE_DELETE_EVENT:
+ sftp_close(remote_file);
+ remmina_sftp_client_thread_set_error(client, task, NULL);
+ return FALSE;
+
+ case GTK_RESPONSE_ACCEPT:
+ sftp_close(remote_file);
+ tmp = remmina_ssh_unconvert(REMMINA_SSH(sftp), remote_path);
+ remote_file = sftp_open(sftp->sftp_sess, tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ g_free(tmp);
+ if (!remote_file) {
+ remmina_sftp_client_thread_set_error(client, task, _("Could not create the file “%s” on the server. %s"),
+ remote_path, ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ return FALSE;
+ }
+ size = 0;
+ break;
+
+ case GTK_RESPONSE_APPLY:
+ if (sftp_seek64(remote_file, size) < 0) {
+ sftp_close(remote_file);
+ remmina_sftp_client_thread_set_error(client, task, "Could not download the file “%s”. %s",
+ remote_path, ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ return FALSE;
+ }
+ break;
+ }
+ }
+
+ local_file = g_fopen(local_path, "rb");
+ if (!local_file) {
+ sftp_close(remote_file);
+ remmina_sftp_client_thread_set_error(client, task, _("Could not open the file “%s”."), local_path);
+ return FALSE;
+ }
+
+ if (size > 0) {
+ if (fseeko(local_file, size, SEEK_SET) < 0) {
+ sftp_close(remote_file);
+ fclose(local_file);
+ remmina_sftp_client_thread_set_error(client, task, "Could not find the local file “%s”.", local_path);
+ return FALSE;
+ }
+ *donesize = size;
+ }
+
+ while (!THREAD_CHECK_EXIT && (len = fread(buf, 1, sizeof(buf), local_file)) > 0) {
+ if (THREAD_CHECK_EXIT) break;
+
+ if (sftp_write(remote_file, buf, len) < len) {
+ sftp_close(remote_file);
+ fclose(local_file);
+ remmina_sftp_client_thread_set_error(client, task, _("Could not write to the file “%s” on the server. %s"),
+ remote_path, ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ return FALSE;
+ }
+
+ *donesize += (guint64)len;
+ task->donesize = (gfloat)(*donesize);
+
+ if (!remmina_sftp_client_thread_update_task(client, task)) break;
+ }
+
+ sftp_close(remote_file);
+ fclose(local_file);
+ return TRUE;
+}
+
+static gpointer
+remmina_sftp_client_thread_main(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaSFTPClient *client = REMMINA_SFTP_CLIENT(data);
+ RemminaSFTP *sftp = NULL;
+ RemminaFTPTask *task;
+ gchar *remote, *local;
+ guint64 size;
+ GPtrArray *array;
+ gint i;
+ gchar *remote_file, *local_file;
+ gboolean ret;
+ gchar *refreshdir = NULL;
+ gchar *tmp;
+ gboolean refresh = FALSE;
+ gchar *host;
+ int port;
+
+ task = remmina_sftp_client_thread_get_task(client);
+ while (task) {
+ size = 0;
+ if (!sftp) {
+ sftp = remmina_sftp_new_from_ssh(REMMINA_SSH(client->sftp));
+
+ /* we may need to open a new tunnel too */
+ host = NULL;
+ port = 0;
+ if (!remmina_plugin_sftp_start_direct_tunnel(client->gp, &host, &port))
+ return NULL;
+ (REMMINA_SSH(sftp))->tunnel_entrance_host = host;
+ (REMMINA_SSH(sftp))->tunnel_entrance_port = port;
+
+ /* Open a new connection for this subcommand */
+ g_debug("[SFTPCLI] %s opening ssh session to %s:%d", __func__, host, port);
+ if (!remmina_ssh_init_session(REMMINA_SSH(sftp))) {
+ g_debug("[SFTPCLI] remmina_ssh_init_session returned error %s\n", (REMMINA_SSH(sftp))->error);
+ remmina_sftp_client_thread_set_error(client, task, (REMMINA_SSH(sftp))->error);
+ remmina_ftp_task_free(task);
+ break;
+ }
+
+ if (remmina_ssh_auth(REMMINA_SSH(sftp), REMMINA_SSH(sftp)->password, client->gp, NULL) != REMMINA_SSH_AUTH_SUCCESS) {
+ g_debug("[SFTPCLI] remmina_ssh_auth returned error %s\n", (REMMINA_SSH(sftp))->error);
+ remmina_sftp_client_thread_set_error(client, task, (REMMINA_SSH(sftp))->error);
+ remmina_ftp_task_free(task);
+ break;
+ }
+
+ if (!remmina_sftp_open(sftp)) {
+ g_debug("[SFTPCLI] remmina_sftp_open returned error %s\n", (REMMINA_SSH(sftp))->error);
+ remmina_sftp_client_thread_set_error(client, task, (REMMINA_SSH(sftp))->error);
+ remmina_ftp_task_free(task);
+ break;
+ }
+ }
+
+ remote = remmina_public_combine_path(task->remotedir, task->name);
+ local = remmina_public_combine_path(task->localdir, task->name);
+
+ switch (task->tasktype) {
+ case REMMINA_FTP_TASK_TYPE_DOWNLOAD:
+ switch (task->type) {
+ case REMMINA_FTP_FILE_TYPE_FILE:
+ ret = remmina_sftp_client_thread_download_file(client, sftp, task,
+ remote, local, &size);
+ break;
+
+ case REMMINA_FTP_FILE_TYPE_DIR:
+ array = g_ptr_array_new();
+ ret = remmina_sftp_client_thread_recursive_dir(client, sftp, task, remote, NULL, array);
+ if (ret) {
+ for (i = 0; i < array->len; i++) {
+ if (THREAD_CHECK_EXIT) {
+ ret = FALSE;
+ break;
+ }
+ remote_file = remmina_public_combine_path(remote, (gchar *)g_ptr_array_index(array, i));
+ local_file = remmina_public_combine_path(local, (gchar *)g_ptr_array_index(array, i));
+ ret = remmina_sftp_client_thread_download_file(client, sftp, task,
+ remote_file, local_file, &size);
+ g_free(remote_file);
+ g_free(local_file);
+ if (!ret) break;
+ }
+ }
+ g_ptr_array_foreach(array, (GFunc)g_free, NULL);
+ g_ptr_array_free(array, TRUE);
+ break;
+
+ default:
+ ret = 0;
+ break;
+ }
+ if (ret)
+ remmina_sftp_client_thread_set_finish(client, task);
+ break;
+
+ case REMMINA_FTP_TASK_TYPE_UPLOAD:
+ switch (task->type) {
+ case REMMINA_FTP_FILE_TYPE_FILE:
+ ret = remmina_sftp_client_thread_upload_file(client, sftp, task,
+ remote, local, &size);
+ break;
+
+ case REMMINA_FTP_FILE_TYPE_DIR:
+ ret = remmina_sftp_client_thread_mkdir(client, sftp, task, remote);
+ if (!ret) break;
+ array = g_ptr_array_new();
+ ret = remmina_sftp_client_thread_recursive_localdir(client, task, local, NULL, array);
+ if (ret) {
+ for (i = 0; i < array->len; i++) {
+ if (THREAD_CHECK_EXIT) {
+ ret = FALSE;
+ break;
+ }
+ remote_file = remmina_public_combine_path(remote, (gchar *)g_ptr_array_index(array, i));
+ local_file = g_build_filename(local, (gchar *)g_ptr_array_index(array, i), NULL);
+ if (g_file_test(local_file, G_FILE_TEST_IS_DIR))
+ ret = remmina_sftp_client_thread_mkdir(client, sftp, task, remote_file);
+ else
+ ret = remmina_sftp_client_thread_upload_file(client, sftp, task,
+ remote_file, local_file, &size);
+ g_free(remote_file);
+ g_free(local_file);
+ if (!ret) break;
+ }
+ }
+ g_ptr_array_foreach(array, (GFunc)g_free, NULL);
+ g_ptr_array_free(array, TRUE);
+ break;
+
+ default:
+ ret = 0;
+ break;
+ }
+ if (ret) {
+ remmina_sftp_client_thread_set_finish(client, task);
+ tmp = remmina_ftp_client_get_dir(REMMINA_FTP_CLIENT(client));
+ if (g_strcmp0(tmp, task->remotedir) == 0) {
+ refresh = TRUE;
+ g_free(refreshdir);
+ refreshdir = tmp;
+ } else {
+ g_free(tmp);
+ }
+ }
+ break;
+ }
+
+ g_free(remote);
+ g_free(local);
+
+ remmina_ftp_task_free(task);
+ client->taskid = 0;
+
+ if (client->thread_abort) break;
+
+ task = remmina_sftp_client_thread_get_task(client);
+ }
+
+ if (sftp)
+ remmina_sftp_free(sftp);
+
+ if (!client->thread_abort && refresh) {
+ tmp = remmina_ftp_client_get_dir(REMMINA_FTP_CLIENT(client));
+ if (g_strcmp0(tmp, refreshdir) == 0)
+ IDLE_ADD((GSourceFunc)remmina_sftp_client_refresh, client);
+ g_free(tmp);
+ }
+ g_free(refreshdir);
+ client->thread = 0;
+
+ return NULL;
+}
+
+/* ------------------------ The SFTP Client routines ----------------------------- */
+
+static void
+remmina_sftp_client_destroy(RemminaSFTPClient *client, gpointer data)
+{
+ TRACE_CALL(__func__);
+ if (client->sftp) {
+ remmina_sftp_free(client->sftp);
+ client->sftp = NULL;
+ }
+ client->thread_abort = TRUE;
+ /* We will wait for the thread to quit itself, and hopefully the thread is handling things correctly */
+ while (client->thread) {
+ /* gdk_threads_leave (); */
+ sleep(1);
+ /* gdk_threads_enter (); */
+ }
+}
+
+static sftp_dir
+remmina_sftp_client_sftp_session_opendir(RemminaSFTPClient *client, const gchar *dir)
+{
+ TRACE_CALL(__func__);
+ sftp_dir sftpdir;
+
+ sftpdir = sftp_opendir(client->sftp->sftp_sess, (gchar *)dir);
+ if (!sftpdir) {
+ GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(client))),
+ GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
+ _("Could not open the folder “%s”. %s"), dir,
+ ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ return NULL;
+ }
+ return sftpdir;
+}
+
+static gboolean
+remmina_sftp_client_sftp_session_closedir(RemminaSFTPClient *client, sftp_dir sftpdir)
+{
+ TRACE_CALL(__func__);
+
+ if (!sftp_dir_eof(sftpdir)) {
+ GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(client))),
+ GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
+ _("Could not read from the folder. %s"), ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ return FALSE;
+ }
+ sftp_closedir(sftpdir);
+ return TRUE;
+}
+
+static void
+remmina_sftp_client_on_opendir(RemminaSFTPClient *client, gchar *dir, gpointer data)
+{
+ TRACE_CALL(__func__);
+ sftp_dir sftpdir;
+ sftp_attributes sftpattr;
+ GtkWidget *dialog;
+ gchar *newdir;
+ gchar *newdir_conv;
+ gchar *tmp;
+ gint type;
+
+ if (client->sftp == NULL) return;
+
+ if (!dir || dir[0] == '\0') {
+ newdir = g_strdup(".");
+ } else if (dir[0] == '/') {
+ newdir = g_strdup(dir);
+ } else {
+ tmp = remmina_ftp_client_get_dir(REMMINA_FTP_CLIENT(client));
+ if (tmp) {
+ newdir = remmina_public_combine_path(tmp, dir);
+ g_free(tmp);
+ } else {
+ newdir = g_strdup_printf("./%s", dir);
+ }
+ }
+
+ gchar *tlink = sftp_readlink (client->sftp->sftp_sess, newdir);
+ if (tlink) {
+ REMMINA_DEBUG ("%s is a link to %s", newdir, tlink);
+ newdir = g_strdup (tlink);
+ if (sftp_opendir (client->sftp->sftp_sess, newdir)) {
+ REMMINA_DEBUG ("%s is a link to a folder", tlink);
+ } else {
+ REMMINA_DEBUG ("%s is a link to a file", tlink);
+ return;
+ }
+ }
+ g_free(tlink);
+
+ tmp = remmina_ssh_unconvert(REMMINA_SSH(client->sftp), newdir);
+ newdir_conv = sftp_canonicalize_path(client->sftp->sftp_sess, tmp);
+ g_free(tmp);
+ g_free(newdir);
+ newdir = remmina_ssh_convert(REMMINA_SSH(client->sftp), newdir_conv);
+ if (!newdir) {
+ dialog = gtk_message_dialog_new(NULL,
+ GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
+ _("Could not open the folder “%s”. %s"), dir,
+ ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ gtk_widget_show(dialog);
+ g_signal_connect(G_OBJECT(dialog), "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ g_free(newdir_conv);
+ return;
+ }
+
+ sftpdir = remmina_sftp_client_sftp_session_opendir(client, newdir_conv);
+ g_free(newdir_conv);
+ if (!sftpdir) {
+ g_free(newdir);
+ return;
+ }
+
+ remmina_ftp_client_clear_file_list(REMMINA_FTP_CLIENT(client));
+
+ while ((sftpattr = sftp_readdir(client->sftp->sftp_sess, sftpdir))) {
+ if (g_strcmp0(sftpattr->name, ".") != 0 &&
+ g_strcmp0(sftpattr->name, "..") != 0) {
+ GET_SFTPATTR_TYPE(sftpattr, type);
+
+ tmp = remmina_ssh_convert(REMMINA_SSH(client->sftp), sftpattr->name);
+ remmina_ftp_client_add_file(REMMINA_FTP_CLIENT(client),
+ REMMINA_FTP_FILE_COLUMN_TYPE, type,
+ REMMINA_FTP_FILE_COLUMN_NAME, tmp,
+ REMMINA_FTP_FILE_COLUMN_SIZE, (gfloat)sftpattr->size,
+ REMMINA_FTP_FILE_COLUMN_USER, sftpattr->owner,
+ REMMINA_FTP_FILE_COLUMN_GROUP, sftpattr->group,
+ REMMINA_FTP_FILE_COLUMN_PERMISSION, sftpattr->permissions,
+ REMMINA_FTP_FILE_COLUMN_MODIFIED, sftpattr->mtime,
+ -1);
+ g_free(tmp);
+ }
+ sftp_attributes_free(sftpattr);
+ }
+ remmina_sftp_client_sftp_session_closedir(client, sftpdir);
+
+ remmina_ftp_client_set_dir(REMMINA_FTP_CLIENT(client), newdir);
+ g_free(newdir);
+}
+
+static void
+remmina_sftp_client_on_newtask(RemminaSFTPClient *client, gpointer data)
+{
+ TRACE_CALL(__func__);
+ if (client->thread) return;
+
+ if (pthread_create(&client->thread, NULL, remmina_sftp_client_thread_main, client))
+ client->thread = 0;
+}
+
+static gboolean
+remmina_sftp_client_on_canceltask(RemminaSFTPClient *client, gint taskid, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *dialog;
+ gint ret;
+
+ if (client->taskid != taskid) return TRUE;
+
+ dialog = gtk_message_dialog_new(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(client))),
+ GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
+ _("Are you sure you want to cancel the file transfer in progress?"));
+ ret = gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ if (ret == GTK_RESPONSE_YES) {
+ /* Make sure we are still handling the same task before we clear the flag */
+ if (client->taskid == taskid) client->taskid = 0;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gboolean
+remmina_sftp_client_on_deletefile(RemminaSFTPClient *client, gint type, gchar *name, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *dialog;
+ gint ret = 0;
+ gchar *tmp;
+
+ tmp = remmina_ssh_unconvert(REMMINA_SSH(client->sftp), name);
+ switch (type) {
+ case REMMINA_FTP_FILE_TYPE_DIR:
+ ret = sftp_rmdir(client->sftp->sftp_sess, tmp);
+ break;
+
+ case REMMINA_FTP_FILE_TYPE_FILE:
+ ret = sftp_unlink(client->sftp->sftp_sess, tmp);
+ break;
+ }
+ g_free(tmp);
+
+ if (ret != 0) {
+ dialog = gtk_message_dialog_new(GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(client))),
+ GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
+ _("Could not delete “%s”. %s"),
+ name, ssh_get_error(REMMINA_SSH(client->sftp)->session));
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+remmina_sftp_client_init(RemminaSFTPClient *client)
+{
+ TRACE_CALL(__func__);
+ client->sftp = NULL;
+ client->thread = 0;
+ client->taskid = 0;
+ client->thread_abort = FALSE;
+
+ /* Setup the internal signals */
+ g_signal_connect(G_OBJECT(client), "destroy",
+ G_CALLBACK(remmina_sftp_client_destroy), NULL);
+ g_signal_connect(G_OBJECT(client), "open-dir",
+ G_CALLBACK(remmina_sftp_client_on_opendir), NULL);
+ g_signal_connect(G_OBJECT(client), "new-task",
+ G_CALLBACK(remmina_sftp_client_on_newtask), NULL);
+ g_signal_connect(G_OBJECT(client), "cancel-task",
+ G_CALLBACK(remmina_sftp_client_on_canceltask), NULL);
+ g_signal_connect(G_OBJECT(client), "delete-file",
+ G_CALLBACK(remmina_sftp_client_on_deletefile), NULL);
+}
+
+static gboolean
+remmina_sftp_client_refresh(RemminaSFTPClient *client)
+{
+ TRACE_CALL(__func__);
+
+ GdkDisplay *display = gdk_display_get_default();
+
+ SET_CURSOR(gdk_cursor_new_for_display(display, GDK_WATCH));
+ gdk_display_flush(display);
+
+ remmina_sftp_client_on_opendir(client, ".", NULL);
+
+ SET_CURSOR(NULL);
+
+ return FALSE;
+}
+
+gint
+remmina_sftp_client_confirm_resume(RemminaSFTPClient *client, const gchar *path)
+{
+ TRACE_CALL(__func__);
+
+ GtkWidget *dialog;
+ gint response;
+ GtkWidget *hbox;
+ GtkWidget *vbox;
+ GtkWidget *widget;
+ const gchar *filename;
+
+ /* Always reply ACCEPT if overwrite_all was already set */
+ if (remmina_ftp_client_get_overwrite_status(REMMINA_FTP_CLIENT(client)))
+ return GTK_RESPONSE_ACCEPT;
+
+ /* Always reply APPLY if resume was already set */
+ if (remmina_ftp_client_get_resume_status(REMMINA_FTP_CLIENT(client)))
+ return GTK_RESPONSE_APPLY;
+
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ gint retval;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_SFTP_CLIENT_CONFIRM_RESUME;
+ d->p.sftp_client_confirm_resume.client = client;
+ d->p.sftp_client_confirm_resume.path = path;
+ remmina_masterthread_exec_and_wait(d);
+ retval = d->p.sftp_client_confirm_resume.retval;
+ g_free(d);
+ return retval;
+ }
+
+ filename = strrchr(path, '/');
+ filename = filename ? filename + 1 : path;
+
+ dialog = gtk_dialog_new_with_buttons(_("The file exists already"),
+ GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(client))),
+ GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
+ _("Resume"), GTK_RESPONSE_APPLY,
+ _("Overwrite"), GTK_RESPONSE_ACCEPT,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ NULL);
+ gtk_container_set_border_width(GTK_CONTAINER(dialog), 4);
+
+ hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
+ gtk_widget_show(hbox);
+ gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
+ hbox, TRUE, TRUE, 4);
+
+ widget = gtk_image_new_from_icon_name("dialog-question", GTK_ICON_SIZE_DIALOG);
+ gtk_widget_show(widget);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 4);
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
+ gtk_widget_show(vbox);
+ gtk_box_pack_start(GTK_BOX(hbox), vbox, TRUE, TRUE, 4);
+
+ widget = gtk_label_new(_("The following file already exists in the target folder:"));
+ 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_box_pack_start(GTK_BOX(vbox), widget, TRUE, TRUE, 4);
+
+ widget = gtk_label_new(filename);
+ 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_box_pack_start(GTK_BOX(vbox), widget, TRUE, TRUE, 4);
+
+ response = gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ return response;
+}
+
+RemminaSFTPClient *
+remmina_sftp_client_new(void)
+{
+ TRACE_CALL(__func__);
+ return REMMINA_SFTP_CLIENT(g_object_new(REMMINA_TYPE_SFTP_CLIENT, NULL));
+}
+
+void
+remmina_sftp_client_open(RemminaSFTPClient *client, RemminaSFTP *sftp)
+{
+ TRACE_CALL(__func__);
+ client->sftp = sftp;
+
+ g_idle_add((GSourceFunc)remmina_sftp_client_refresh, client);
+}
+
+/*
+ * GtkWidget *
+ * remmina_sftp_client_new_init(RemminaSFTP *sftp)
+ * {
+ * TRACE_CALL(__func__);
+ * GdkDisplay *display;
+ * GtkWidget *client;
+ * GtkWidget *dialog;
+ *
+ * display = gdk_display_get_default();
+ * client = remmina_sftp_client_new();
+ *
+ *
+ * SET_CURSOR(gdk_cursor_new_for_display(display, GDK_WATCH));
+ * gdk_display_flush(display);
+ *
+ * if (!remmina_ssh_init_session(REMMINA_SSH(sftp)) ||
+ * remmina_ssh_auth(REMMINA_SSH(sftp), NULL, NULL, NULL) != REMMINA_SSH_AUTH_SUCCESS ||
+ * !remmina_sftp_open(sftp)) {
+ * dialog = gtk_message_dialog_new(GTK_WINDOW(gtk_widget_get_toplevel(client)),
+ * GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
+ * (REMMINA_SSH(sftp))->error, NULL);
+ * gtk_dialog_run(GTK_DIALOG(dialog));
+ * gtk_widget_destroy(dialog);
+ * gtk_widget_destroy(client);
+ * return NULL;
+ * }
+ *
+ * SET_CURSOR(NULL);
+ *
+ * g_idle_add((GSourceFunc)remmina_sftp_client_refresh, client);
+ * return client;
+ * }
+ */
+
+#endif
diff --git a/src/remmina_sftp_client.h b/src/remmina_sftp_client.h
new file mode 100644
index 0000000..13da079
--- /dev/null
+++ b/src/remmina_sftp_client.h
@@ -0,0 +1,80 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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"
+
+#ifdef HAVE_LIBSSH
+
+#include "remmina_file.h"
+#include "remmina_ftp_client.h"
+#include "remmina_ssh.h"
+
+G_BEGIN_DECLS
+
+#define REMMINA_TYPE_SFTP_CLIENT (remmina_sftp_client_get_type())
+#define REMMINA_SFTP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), REMMINA_TYPE_SFTP_CLIENT, RemminaSFTPClient))
+#define REMMINA_SFTP_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), REMMINA_TYPE_SFTP_CLIENT, RemminaSFTPClientClass))
+#define REMMINA_IS_SFTP_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), REMMINA_TYPE_SFTP_CLIENT))
+#define REMMINA_IS_SFTP_CLIENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), REMMINA_TYPE_SFTP_CLIENT))
+#define REMMINA_SFTP_CLIENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), REMMINA_TYPE_SFTP_CLIENT, RemminaSFTPClientClass))
+
+typedef struct _RemminaSFTPClient {
+ RemminaFTPClient client;
+
+ RemminaSFTP * sftp;
+
+ pthread_t thread;
+ gint taskid;
+ gboolean thread_abort;
+ RemminaProtocolWidget * gp;
+} RemminaSFTPClient;
+
+typedef struct _RemminaSFTPClientClass {
+ RemminaFTPClientClass parent_class;
+} RemminaSFTPClientClass;
+
+GType remmina_sftp_client_get_type(void) G_GNUC_CONST;
+
+RemminaSFTPClient *remmina_sftp_client_new();
+
+void remmina_sftp_client_open(RemminaSFTPClient *client, RemminaSFTP *sftp);
+gint remmina_sftp_client_confirm_resume(RemminaSFTPClient *client, const gchar *path);
+
+G_END_DECLS
+
+#endif /* HAVE_LIBSSH */
diff --git a/src/remmina_sftp_plugin.c b/src/remmina_sftp_plugin.c
new file mode 100644
index 0000000..8132649
--- /dev/null
+++ b/src/remmina_sftp_plugin.c
@@ -0,0 +1,401 @@
+/*
+ * 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 "config.h"
+#include "remmina/remmina_trace_calls.h"
+
+#ifdef HAVE_LIBSSH
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include "remmina_public.h"
+#include "remmina_sftp_client.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_ssh.h"
+#include "remmina_sftp_plugin.h"
+
+#define REMMINA_PLUGIN_SFTP_FEATURE_PREF_SHOW_HIDDEN 1
+#define REMMINA_PLUGIN_SFTP_FEATURE_PREF_OVERWRITE_ALL 2
+#define REMMINA_PLUGIN_SFTP_FEATURE_PREF_RESUME_ALL 3
+
+#define REMMINA_PLUGIN_SFTP_FEATURE_PREF_OVERWRITE_ALL_KEY "overwrite_all"
+#define REMMINA_PLUGIN_SFTP_FEATURE_PREF_RESUME_ALL_KEY "resume_all"
+
+#define GET_PLUGIN_DATA(gp) (RemminaPluginSftpData *)g_object_get_data(G_OBJECT(gp), "plugin-data");
+
+typedef struct _RemminaPluginSftpData {
+ RemminaSFTPClient * client;
+ pthread_t thread;
+ RemminaSFTP * sftp;
+} RemminaPluginSftpData;
+
+static RemminaPluginService *remmina_plugin_service = NULL;
+
+gboolean remmina_plugin_sftp_start_direct_tunnel(RemminaProtocolWidget *gp, char **phost, int *pport)
+{
+ gchar *hostport;
+
+ hostport = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, 22, FALSE);
+ if (hostport == NULL) {
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ return FALSE;
+ }
+
+ remmina_plugin_service->get_server_port(hostport, 22, phost, pport);
+
+ return TRUE;
+}
+
+static gpointer
+remmina_plugin_sftp_main_thread(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)data;
+ RemminaPluginSftpData *gpdata;
+ RemminaFile *remminafile;
+ RemminaSSH *ssh;
+ RemminaSFTP *sftp = NULL;
+ gboolean cont = FALSE;
+ gint ret;
+ const gchar *cs;
+ gchar *host;
+ int port;
+
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+ CANCEL_ASYNC
+
+ gpdata = GET_PLUGIN_DATA(gp);
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ /* we may need to open a new tunnel too */
+ host = NULL;
+ port = 0;
+ if (!remmina_plugin_sftp_start_direct_tunnel(gp, &host, &port))
+ return NULL;
+
+ ssh = g_object_get_data(G_OBJECT(gp), "user-data");
+ if (ssh) {
+ /* Create SFTP connection based on existing SSH session */
+ sftp = remmina_sftp_new_from_ssh(ssh);
+ ssh->tunnel_entrance_host = host;
+ ssh->tunnel_entrance_port = port;
+ if (remmina_ssh_init_session(REMMINA_SSH(sftp)) &&
+ remmina_ssh_auth(REMMINA_SSH(sftp), NULL, gp, remminafile) == REMMINA_SSH_AUTH_SUCCESS &&
+ remmina_sftp_open(sftp))
+ cont = TRUE;
+ } else {
+ /* New SFTP connection */
+ sftp = remmina_sftp_new_from_file(remminafile);
+ ssh = REMMINA_SSH(sftp);
+ ssh->tunnel_entrance_host = host;
+ ssh->tunnel_entrance_port = port;
+ while (1) {
+ if (!remmina_ssh_init_session(ssh)) {
+ remmina_plugin_service->protocol_plugin_set_error(gp, "%s", ssh->error);
+ break;
+ }
+
+ ret = remmina_ssh_auth_gui(ssh, gp, remminafile);
+ if (ret != REMMINA_SSH_AUTH_SUCCESS) {
+ if (ret == REMMINA_SSH_AUTH_RECONNECT) {
+ if (ssh->session) {
+ ssh_disconnect(ssh->session);
+ ssh_free(ssh->session);
+ ssh->session = NULL;
+ }
+ g_free(ssh->callback);
+ continue;
+ }
+ if (ret != REMMINA_SSH_AUTH_USERCANCEL)
+ remmina_plugin_service->protocol_plugin_set_error(gp, "%s", ssh->error);
+ break;
+ }
+
+ if (!remmina_sftp_open(sftp)) {
+ remmina_plugin_service->protocol_plugin_set_error(gp, "%s", ssh->error);
+ break;
+ }
+
+ cs = remmina_plugin_service->file_get_string(remminafile, "execpath");
+ if (cs && cs[0])
+ remmina_ftp_client_set_dir(REMMINA_FTP_CLIENT(gpdata->client), cs);
+
+ cont = TRUE;
+ break;
+ }
+ }
+
+ if (!cont) {
+ if (sftp) remmina_sftp_free(sftp);
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ return NULL;
+ }
+
+ remmina_sftp_client_open(REMMINA_SFTP_CLIENT(gpdata->client), sftp);
+ /* RemminaSFTPClient owns the object, we just take the reference */
+ gpdata->sftp = sftp;
+
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+
+ gpdata->thread = 0;
+ return NULL;
+}
+
+static void
+remmina_plugin_sftp_client_on_realize(GtkWidget *widget, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaFile *remminafile;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ remmina_ftp_client_load_state(REMMINA_FTP_CLIENT(widget), remminafile);
+}
+
+static void
+remmina_plugin_sftp_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSftpData *gpdata;
+ RemminaFile *remminafile;
+
+ gpdata = g_new0(RemminaPluginSftpData, 1);
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ gpdata->client = remmina_sftp_client_new();
+ gpdata->client->gp = gp;
+ gtk_widget_show(GTK_WIDGET(gpdata->client));
+ gtk_container_add(GTK_CONTAINER(gp), GTK_WIDGET(gpdata->client));
+
+ remmina_ftp_client_set_show_hidden(REMMINA_FTP_CLIENT(gpdata->client),
+ remmina_plugin_service->file_get_int(remminafile, "showhidden", FALSE));
+
+ remmina_ftp_client_set_overwrite_status(REMMINA_FTP_CLIENT(gpdata->client),
+ remmina_plugin_service->file_get_int(remminafile,
+ REMMINA_PLUGIN_SFTP_FEATURE_PREF_OVERWRITE_ALL_KEY, FALSE));
+ remmina_ftp_client_set_resume_status(REMMINA_FTP_CLIENT(gpdata->client),
+ remmina_plugin_service->file_get_int(remminafile,
+ REMMINA_PLUGIN_SFTP_FEATURE_PREF_RESUME_ALL_KEY, FALSE));
+
+ remmina_plugin_service->protocol_plugin_register_hostkey(gp, GTK_WIDGET(gpdata->client));
+
+ g_signal_connect(G_OBJECT(gpdata->client),
+ "realize", G_CALLBACK(remmina_plugin_sftp_client_on_realize), gp);
+}
+
+static gboolean
+remmina_plugin_sftp_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSftpData *gpdata = GET_PLUGIN_DATA(gp);
+
+ remmina_plugin_service->protocol_plugin_set_expand(gp, TRUE);
+ remmina_plugin_service->protocol_plugin_set_width(gp, 640);
+ remmina_plugin_service->protocol_plugin_set_height(gp, 480);
+
+ if (pthread_create(&gpdata->thread, NULL, remmina_plugin_sftp_main_thread, gp)) {
+ remmina_plugin_service->protocol_plugin_set_error(gp,
+ "Could not initialize pthread. Falling back to non-thread mode…");
+ gpdata->thread = 0;
+ return FALSE;
+ } else {
+ return TRUE;
+ }
+ return TRUE;
+}
+
+static gboolean
+remmina_plugin_sftp_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSftpData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ if (gpdata->thread) {
+ pthread_cancel(gpdata->thread);
+ if (gpdata->thread) pthread_join(gpdata->thread, NULL);
+ }
+
+ remmina_ftp_client_save_state(REMMINA_FTP_CLIENT(gpdata->client), remminafile);
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ /* The session preference overwrite_all is always saved to FALSE in order
+ * to avoid unwanted overwriting.
+ * If we'd change idea just remove the next line to save the preference. */
+ remmina_file_set_int(remminafile,
+ REMMINA_PLUGIN_SFTP_FEATURE_PREF_OVERWRITE_ALL_KEY, FALSE);
+ return FALSE;
+}
+
+static gboolean
+remmina_plugin_sftp_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ return TRUE;
+}
+
+static void
+remmina_plugin_sftp_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSftpData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ switch (feature->id) {
+ case REMMINA_PROTOCOL_FEATURE_TOOL_SSH:
+ remmina_plugin_service->open_connection(
+ remmina_file_dup_temp_protocol(remmina_plugin_service->protocol_plugin_get_file(gp), "SSH"),
+ NULL, gpdata->sftp, NULL);
+ return;
+ case REMMINA_PROTOCOL_FEATURE_TOOL_SFTP:
+ remmina_plugin_service->open_connection(
+ remmina_file_dup_temp_protocol(remmina_plugin_service->protocol_plugin_get_file(gp), "SFTP"),
+ NULL, gpdata->sftp, NULL);
+ return;
+ case REMMINA_PLUGIN_SFTP_FEATURE_PREF_SHOW_HIDDEN:
+ remmina_ftp_client_set_show_hidden(REMMINA_FTP_CLIENT(gpdata->client),
+ remmina_plugin_service->file_get_int(remminafile, "showhidden", FALSE));
+ return;
+ case REMMINA_PLUGIN_SFTP_FEATURE_PREF_OVERWRITE_ALL:
+ remmina_ftp_client_set_overwrite_status(REMMINA_FTP_CLIENT(gpdata->client),
+ remmina_plugin_service->file_get_int(remminafile,
+ REMMINA_PLUGIN_SFTP_FEATURE_PREF_OVERWRITE_ALL_KEY, FALSE));
+ case REMMINA_PLUGIN_SFTP_FEATURE_PREF_RESUME_ALL:
+ remmina_ftp_client_set_resume_status(REMMINA_FTP_CLIENT(gpdata->client),
+ remmina_plugin_service->file_get_int(remminafile,
+ REMMINA_PLUGIN_SFTP_FEATURE_PREF_RESUME_ALL_KEY, FALSE));
+ return;
+ }
+}
+
+static gpointer ssh_auth[] =
+{
+ "0", N_("Password"),
+ "1", N_("SSH identity file"),
+ "2", N_("SSH agent"),
+ "3", N_("Public key (automatic)"),
+ "4", N_("Kerberos (GSSAPI)"),
+ NULL
+};
+
+/* Array for available features.
+ * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END. */
+static const RemminaProtocolFeature remmina_plugin_sftp_features[] =
+{
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_SFTP_FEATURE_PREF_SHOW_HIDDEN, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK), "showhidden",
+ N_("Show Hidden Files") },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_SFTP_FEATURE_PREF_OVERWRITE_ALL, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK),
+ REMMINA_PLUGIN_SFTP_FEATURE_PREF_OVERWRITE_ALL_KEY, N_("Overwrite all files") },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_PREF, REMMINA_PLUGIN_SFTP_FEATURE_PREF_RESUME_ALL, GINT_TO_POINTER(REMMINA_PROTOCOL_FEATURE_PREF_CHECK),
+ REMMINA_PLUGIN_SFTP_FEATURE_PREF_RESUME_ALL_KEY, N_("Resume all file transfers") },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PROTOCOL_FEATURE_TOOL_SSH, N_("Connect via SSH from a new terminal"), "utilities-terminal",NULL},
+ { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL,
+ 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 remmina_sftp_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, "_sftp-ssh._tcp", 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_SELECT, "ssh_auth", N_("Authentication type"), FALSE, ssh_auth, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_FILE, "ssh_privatekey", N_("SSH identity file"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "ssh_passphrase", N_("Password to unlock private key"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "ssh_proxycommand", N_("SSH Proxy Command"), 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_sftp =
+{
+ REMMINA_PLUGIN_TYPE_PROTOCOL, // Type
+ "SFTP", // Name
+ N_("SFTP - Secure File Transfer"), // Description
+ GETTEXT_PACKAGE, // Translation domain
+ VERSION, // Version number
+ "org.remmina.Remmina-sftp-symbolic", // Icon for normal connection
+ "org.remmina.Remmina-sftp-symbolic", // Icon for SSH connection
+ remmina_sftp_basic_settings, // Array for basic settings
+ NULL, // Array for advanced settings
+ REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, // SSH settings type
+ remmina_plugin_sftp_features, // Array for available features
+ remmina_plugin_sftp_init, // Plugin initialization
+ remmina_plugin_sftp_open_connection, // Plugin open connection
+ remmina_plugin_sftp_close_connection, // Plugin close connection
+ remmina_plugin_sftp_query_feature, // Query for available features
+ remmina_plugin_sftp_call_feature, // Call a feature
+ NULL, // Send a keystroke
+ NULL, // Screenshot support unavailable
+ NULL, // RCW map event
+ NULL // RCW unmap event
+};
+
+void
+remmina_sftp_plugin_register(void)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_service = &remmina_plugin_manager_service;
+ remmina_plugin_service->register_plugin((RemminaPlugin *)&remmina_plugin_sftp);
+}
+
+#else
+
+void remmina_sftp_plugin_register(void)
+{
+ TRACE_CALL(__func__);
+}
+
+#endif
diff --git a/src/remmina_sftp_plugin.h b/src/remmina_sftp_plugin.h
new file mode 100644
index 0000000..06d95c3
--- /dev/null
+++ b/src/remmina_sftp_plugin.h
@@ -0,0 +1,47 @@
+/*
+ * 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 <glib.h>
+#include "remmina_protocol_widget.h"
+
+G_BEGIN_DECLS
+
+void remmina_sftp_plugin_register(void);
+gboolean remmina_plugin_sftp_start_direct_tunnel(RemminaProtocolWidget *gp, char **phost, int *pport);
+
+G_END_DECLS
diff --git a/src/remmina_sodium.c b/src/remmina_sodium.c
new file mode 100644
index 0000000..fabc050
--- /dev/null
+++ b/src/remmina_sodium.c
@@ -0,0 +1,192 @@
+/*
+ * 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 remmina_sodium.c
+ * @brief Remmina encryption functions,
+ * @author Antenore Gatta
+ * @date 31 Mar 2019
+ *
+ * These functions are used to:
+ * - hash password using the Argon2 hashing algorithm.
+ * - Encrypt and decrypt data streams (files for examples).
+ *
+ * @code
+ *
+ * gchar *test = remmina_sodium_pwhash("Password test");
+ * g_free(test);
+ * test = remmina_sodium_pwhash_str("Password Test");
+ * g_free(test);
+ * gint rc = remmina_sodium_pwhash_str_verify("$argon2id$v=19$m=65536,t=2,p=1$6o+kpazlHSaevezH2J9qUA$4pN75oHgyh1BLc/b+ybLYHjZbatG4ZSCSlxLI32YPY4", "Password Test");
+ *
+ * @endcode
+ *
+ */
+
+#include <string.h>
+
+#if defined(__linux__)
+# include <fcntl.h>
+# include <unistd.h>
+# include <sys/ioctl.h>
+# include <linux/random.h>
+#endif
+
+#include "config.h"
+#include <glib.h>
+#include "remmina_pref.h"
+#include "remmina/remmina_trace_calls.h"
+
+#include "remmina_sodium.h"
+#if SODIUM_VERSION_INT >= 90200
+
+gchar *remmina_sodium_pwhash(const gchar *pass)
+{
+ TRACE_CALL(__func__);
+ g_info("Generating passphrase (may take a while)...");
+ /* Create a random salt for the key derivation function */
+ unsigned char salt[crypto_pwhash_SALTBYTES] = { 0 };
+ randombytes_buf(salt, sizeof salt);
+
+ unsigned long long opslimit;
+ size_t memlimit;
+
+ switch (remmina_pref.enc_mode) {
+ case RM_ENC_MODE_SODIUM_MODERATE:
+ opslimit = crypto_pwhash_OPSLIMIT_MODERATE;
+ memlimit = crypto_pwhash_MEMLIMIT_MODERATE;
+ break;
+ case RM_ENC_MODE_SODIUM_SENSITIVE:
+ opslimit = crypto_pwhash_OPSLIMIT_SENSITIVE;
+ memlimit = crypto_pwhash_MEMLIMIT_SENSITIVE;
+ break;
+ case RM_ENC_MODE_GCRYPT:
+ case RM_ENC_MODE_SECRET:
+ case RM_ENC_MODE_SODIUM_INTERACTIVE:
+ default:
+ opslimit = crypto_pwhash_OPSLIMIT_INTERACTIVE;
+ memlimit = crypto_pwhash_MEMLIMIT_INTERACTIVE;
+ break;
+ }
+
+ /* Use argon2 to convert password to a full size key */
+ unsigned char key[crypto_secretbox_KEYBYTES];
+ if (crypto_pwhash(key, sizeof key, pass, strlen(pass), salt,
+ opslimit,
+ memlimit,
+ crypto_pwhash_ALG_DEFAULT) != 0) {
+ g_error("%s - Out of memory!", __func__);
+ exit(1);
+ }
+
+ g_info("%s - Password hashed", __func__);
+ return g_strdup((const char *)key);
+}
+
+gchar *remmina_sodium_pwhash_str(const gchar *pass)
+{
+ TRACE_CALL(__func__);
+ g_info("Generating passphrase (may take a while)...");
+ /* Create a random salt for the key derivation function */
+ unsigned char salt[crypto_pwhash_SALTBYTES] = { 0 };
+ randombytes_buf(salt, sizeof salt);
+
+ unsigned long long opslimit;
+ size_t memlimit;
+
+ switch (remmina_pref.enc_mode) {
+ case RM_ENC_MODE_SODIUM_MODERATE:
+ opslimit = crypto_pwhash_OPSLIMIT_MODERATE;
+ memlimit = crypto_pwhash_MEMLIMIT_MODERATE;
+ break;
+ case RM_ENC_MODE_SODIUM_SENSITIVE:
+ opslimit = crypto_pwhash_OPSLIMIT_SENSITIVE;
+ memlimit = crypto_pwhash_MEMLIMIT_SENSITIVE;
+ break;
+ case RM_ENC_MODE_GCRYPT:
+ case RM_ENC_MODE_SECRET:
+ case RM_ENC_MODE_SODIUM_INTERACTIVE:
+ default:
+ opslimit = crypto_pwhash_OPSLIMIT_INTERACTIVE;
+ memlimit = crypto_pwhash_MEMLIMIT_INTERACTIVE;
+ break;
+ }
+
+ /* Use argon2 to convert password to a full size key */
+ char key[crypto_pwhash_STRBYTES];
+ if (crypto_pwhash_str(key, pass, strlen(pass),
+ opslimit,
+ memlimit) != 0) {
+ g_error("%s - Out of memory!", __func__);
+ exit(1);
+ }
+
+ g_info("%s - Password hashed", __func__);
+ return g_strdup((const char *)key);
+}
+
+gint remmina_sodium_pwhash_str_verify(const char *key, const char *pass)
+{
+ TRACE_CALL(__func__);
+
+ gint rc;
+
+ rc = crypto_pwhash_str_verify(key, pass, strlen(pass));
+
+ return rc;
+}
+
+void remmina_sodium_init(void)
+{
+ TRACE_CALL(__func__);
+#if defined(__linux__) && defined(RNDGETENTCNT)
+ int fd;
+ int c;
+
+ if ((fd = open("/dev/random", O_RDONLY)) != -1) {
+ if (ioctl(fd, RNDGETENTCNT, &c) == 0 && c < 160) {
+ g_printerr("This system doesn't provide enough entropy to quickly generate high-quality random numbers.\n"
+ "Installing the rng-utils/rng-tools, jitterentropy or haveged packages may help.\n"
+ "On virtualized Linux environments, also consider using virtio-rng.\n"
+ "The service will not start until enough entropy has been collected.\n");
+ }
+ (void)close(fd);
+ }
+#endif
+
+ if (sodium_init() < 0)
+ g_critical("%s - Failed to initialize sodium, it is not safe to use", __func__);
+}
+
+#endif
diff --git a/src/remmina_sodium.h b/src/remmina_sodium.h
new file mode 100644
index 0000000..cd151e5
--- /dev/null
+++ b/src/remmina_sodium.h
@@ -0,0 +1,52 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * 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>
+
+G_BEGIN_DECLS
+
+#include <sodium.h>
+#include <glib.h>
+
+#define SODIUM_VERSION_INT (SODIUM_LIBRARY_VERSION_MAJOR * 10000 + SODIUM_LIBRARY_VERSION_MINOR * 100)
+#if SODIUM_VERSION_INT >= 90200
+void remmina_sodium_init(void);
+gchar *remmina_sodium_pwhash(const gchar *pass);
+gchar *remmina_sodium_pwhash_str(const gchar *pass);
+gint remmina_sodium_pwhash_str_verify(const char *key, const char *pass);
+#endif
+
+G_END_DECLS
diff --git a/src/remmina_ssh.c b/src/remmina_ssh.c
new file mode 100644
index 0000000..890046a
--- /dev/null
+++ b/src/remmina_ssh.c
@@ -0,0 +1,3238 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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
+ * 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.
+ *
+ */
+
+#include "config.h"
+
+#ifdef HAVE_LIBSSH
+
+/* To get definitions of NI_MAXHOST and NI_MAXSERV from <netdb.h> */
+#define _DEFAULT_SOURCE
+#define _DARWIN_C_SOURCE
+
+/* Define this before stdlib.h to have posix_openpt */
+#define _XOPEN_SOURCE 600
+
+#include <errno.h>
+#define LIBSSH_STATIC 1
+#include <libssh/libssh.h>
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <time.h>
+#include <sys/types.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
+#ifdef HAVE_TERMIOS_H
+#include <termios.h>
+#endif
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#ifdef HAVE_PTY_H
+#include <pty.h>
+#endif
+#ifdef HAVE_SYS_UN_H
+#include <sys/un.h>
+#endif
+#include "remmina_public.h"
+#include "remmina/types.h"
+#include "remmina_file.h"
+#include "remmina_log.h"
+#include "remmina_pref.h"
+#include "remmina_ssh.h"
+#include "remmina_masterthread_exec.h"
+#include "remmina/remmina_trace_calls.h"
+
+
+#ifdef HAVE_NETINET_TCP_H
+#include <netinet/tcp.h>
+
+#if defined(__FreeBSD__) || defined(__OpenBSD__)
+#ifndef SOL_TCP
+#define SOL_TCP IPPROTO_TCP
+#endif
+#endif
+
+#endif
+
+/*-----------------------------------------------------------------------------*
+* SSH Base *
+*-----------------------------------------------------------------------------*/
+
+#define LOCK_SSH(ssh) pthread_mutex_lock(&REMMINA_SSH(ssh)->ssh_mutex);
+#define UNLOCK_SSH(ssh) pthread_mutex_unlock(&REMMINA_SSH(ssh)->ssh_mutex);
+
+static const gchar *common_identities[] =
+{
+ ".ssh/id_ed25519",
+ ".ssh/id_rsa",
+ ".ssh/id_dsa",
+ ".ssh/identity",
+ NULL
+};
+
+/*-----------------------------------------------------------------------------*
+* X11 Channels *
+*-----------------------------------------------------------------------------*/
+#define _PATH_UNIX_X "/tmp/.X11-unix/X%d"
+#define _XAUTH_CMD "/usr/bin/xauth list %s 2>/dev/null"
+
+typedef struct item {
+ ssh_channel channel;
+ gint fd_in;
+ gint fd_out;
+ gboolean protected;
+ pthread_t thread;
+ struct item *next;
+} node_t;
+
+node_t *node = NULL;
+
+// Mutex
+pthread_mutex_t mutex;
+
+// Linked nodes to manage channel/fd tuples
+static void remmina_ssh_insert_item(ssh_channel channel, gint fd_in, gint fd_out, gboolean protected, pthread_t thread);
+static void remmina_ssh_delete_item(ssh_channel channel);
+static node_t * remmina_ssh_search_item(ssh_channel channel);
+
+// X11 Display
+const char * remmina_ssh_ssh_gai_strerror(int gaierr);
+static int remmina_ssh_x11_get_proto(const char *display, char **_proto, char **_data);
+static void remmina_ssh_set_nodelay(int fd);
+static int remmina_ssh_connect_local_xsocket_path(const char *pathname);
+static int remmina_ssh_connect_local_xsocket(int display_number);
+static int remmina_ssh_x11_connect_display();
+
+// Send data to channel
+static int remmina_ssh_cp_to_ch_cb(int fd, int revents, void *userdata);
+
+// Read data from channel
+static int remmina_ssh_cp_to_fd_cb(ssh_session session, ssh_channel channel, void *data, uint32_t len, int is_stderr, void *userdata);
+
+// EOF&Close channel
+static void remmina_ssh_ch_close_cb(ssh_session session, ssh_channel channel, void *userdata);
+
+// Close all X11 channel
+static void remmina_ssh_close_all_x11_ch(pthread_t thread);
+
+// X11 Request
+static ssh_channel remmina_ssh_x11_open_request_cb(ssh_session session, const char *shost, int sport, void *userdata);
+
+// SSH Channel Callbacks
+struct ssh_channel_callbacks_struct channel_cb =
+{
+ .channel_data_function = remmina_ssh_cp_to_fd_cb,
+ .channel_eof_function = remmina_ssh_ch_close_cb,
+ .channel_close_function = remmina_ssh_ch_close_cb,
+ .userdata = NULL
+};
+
+// SSH Event Context
+short events = POLLIN | POLLPRI | POLLERR | POLLHUP | POLLNVAL;
+
+// Functions
+static void
+remmina_ssh_insert_item(ssh_channel channel, gint fd_in, gint fd_out, gboolean protected, pthread_t thread)
+{
+ TRACE_CALL(__func__);
+
+ pthread_mutex_lock(&mutex);
+
+ REMMINA_DEBUG("insert node - fd_in: %d - fd_out: %d - protected %d", fd_in, fd_out, protected);
+
+ node_t *node_iterator, *new;
+ if (node == NULL) {
+ /* Calloc ensure that node is full of 0 */
+ node = (node_t *) calloc(1, sizeof(node_t));
+ node->channel = channel;
+ node->fd_in = fd_in;
+ node->fd_out = fd_out;
+ node->protected = protected;
+ node->thread = thread;
+ node->next = NULL;
+ } else {
+ node_iterator = node;
+ while (node_iterator->next != NULL)
+ node_iterator = node_iterator->next;
+ /* Create the new node */
+ new = (node_t *) malloc(sizeof(node_t));
+ new->channel = channel;
+ new->fd_in = fd_in;
+ new->fd_out = fd_out;
+ new->protected = protected;
+ new->thread = thread;
+ new->next = NULL;
+ node_iterator->next = new;
+ }
+
+ pthread_mutex_unlock(&mutex);
+}
+
+static void
+remmina_ssh_delete_item(ssh_channel channel)
+{
+ TRACE_CALL(__func__);
+
+ REMMINA_DEBUG("delete node");
+
+ pthread_mutex_lock(&mutex);
+
+ node_t *current, *previous = NULL;
+ for (current = node; current; previous = current, current = current->next) {
+ if (current->channel != channel)
+ continue;
+
+ if (previous == NULL)
+ node = current->next;
+ else
+ previous->next = current->next;
+
+ free(current);
+ pthread_mutex_unlock(&mutex);
+ return;
+ }
+
+ pthread_mutex_unlock(&mutex);
+}
+
+static node_t *
+remmina_ssh_search_item(ssh_channel channel)
+{
+ TRACE_CALL(__func__);
+
+ // TODO: too verbose REMMINA_DEBUG("search node");
+
+ pthread_mutex_lock(&mutex);
+
+ node_t *current = node;
+ while (current != NULL) {
+ if (current->channel == channel) {
+ pthread_mutex_unlock(&mutex);
+ // TODO: too verbose REMMINA_DEBUG("found node - fd_in: %d - fd_out: %d - protected: %d", current->fd_in, current->fd_out, current->protected);
+ return current;
+ } else {
+ current = current->next;
+ }
+ }
+
+ pthread_mutex_unlock(&mutex);
+
+ return NULL;
+}
+
+static void
+remmina_ssh_set_nodelay(int fd)
+{
+ TRACE_CALL(__func__);
+ int opt;
+ socklen_t optlen;
+
+ optlen = sizeof(opt);
+ if (getsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, &optlen) == -1) {
+ REMMINA_WARNING("getsockopt TCP_NODELAY: %.100s", strerror(errno));
+ return;
+ }
+ if (opt == 1) {
+ REMMINA_DEBUG("fd %d is TCP_NODELAY", fd);
+ return;
+ }
+ opt = 1;
+ REMMINA_DEBUG("fd %d setting TCP_NODELAY", fd);
+ if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)) == -1)
+ REMMINA_WARNING("setsockopt TCP_NODELAY: %.100s", strerror(errno));
+}
+
+const char *
+remmina_ssh_ssh_gai_strerror(int gaierr)
+{
+ TRACE_CALL(__func__);
+
+ if (gaierr == EAI_SYSTEM && errno != 0)
+ return strerror(errno);
+ return gai_strerror(gaierr);
+}
+
+static int
+remmina_ssh_x11_get_proto(const char *display, char **_proto, char **_cookie)
+{
+ TRACE_CALL(__func__);
+
+ char cmd[1024], line[512], xdisplay[512];
+ static char proto[512], cookie[512];
+ FILE *f;
+ int ret = 0, r;
+
+ *_proto = proto;
+ *_cookie = cookie;
+
+ proto[0] = cookie[0] = '\0';
+
+ if (strncmp(display, "localhost:", 10) == 0) {
+ if ((r = snprintf(xdisplay, sizeof(xdisplay), "unix:%s", display + 10)) < 0 || (size_t)r >= sizeof(xdisplay)) {
+ REMMINA_WARNING("display name too long. display: %s", display);
+ return -1;
+ }
+ display = xdisplay;
+ }
+
+ snprintf(cmd, sizeof(cmd), _XAUTH_CMD, display);
+ REMMINA_DEBUG("xauth cmd: %s", cmd);
+
+ f = popen(cmd, "r");
+ if (f && fgets(line, sizeof(line), f) && sscanf(line, "%*s %511s %511s", proto, cookie) == 2) {
+ ret = 0;
+ } else {
+ ret = 1;
+ }
+
+ if (f) pclose(f);
+
+ REMMINA_DEBUG("proto: %s - cookie: %s - ret: %d", proto, cookie, ret);
+
+ return ret;
+}
+
+static int
+remmina_ssh_connect_local_xsocket_path(const char *pathname)
+{
+ TRACE_CALL(__func__);
+
+ int sock;
+ struct sockaddr_un addr;
+
+ sock = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (sock == -1)
+ REMMINA_WARNING("socket: %.100s", strerror(errno));
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ addr.sun_path[0] = '\0';
+ memcpy(addr.sun_path + 1, pathname, strlen(pathname));
+ if (connect(sock, (struct sockaddr *)&addr, offsetof(struct sockaddr_un, sun_path) + 1 + strlen(pathname)) == 0) {
+ REMMINA_DEBUG("sock: %d", sock);
+ return sock;
+ }
+
+ REMMINA_WARNING("connect %.100s: %.100s", addr.sun_path, strerror(errno));
+ close(sock);
+
+ return -1;
+}
+
+static int
+remmina_ssh_connect_local_xsocket(int display_number)
+{
+ TRACE_CALL(__func__);
+
+ char buf[1024];
+ snprintf(buf, sizeof(buf), _PATH_UNIX_X, display_number);
+ return remmina_ssh_connect_local_xsocket_path(buf);
+}
+
+static int
+remmina_ssh_x11_connect_display()
+{
+ TRACE_CALL(__func__);
+
+ unsigned int display_number;
+ const char *display;
+ char buf[1024], *cp;
+ struct addrinfo hints, *ai, *aitop;
+ char strport[NI_MAXSERV];
+ int gaierr, sock = 0;
+
+ /* Try to open a socket for the local X server. */
+ display = getenv("DISPLAY");
+ if (!display) {
+ return -1;
+ }
+
+ REMMINA_DEBUG("display: %s", display);
+
+ /* Check if it is a unix domain socket. */
+ if (strncmp(display, "unix:", 5) == 0 || display[0] == ':') {
+ /* Connect to the unix domain socket. */
+ if (sscanf(strrchr(display, ':') + 1, "%u", &display_number) != 1) {
+ REMMINA_WARNING("Could not parse display number from DISPLAY: %.100s", display);
+ return -1;
+ }
+
+ REMMINA_DEBUG("display_number: %d", display_number);
+
+ /* Create a socket. */
+ sock = remmina_ssh_connect_local_xsocket(display_number);
+
+ REMMINA_DEBUG("socket: %d", sock);
+
+ if (sock < 0)
+ return -1;
+
+ /* OK, we now have a connection to the display. */
+ return sock;
+ }
+
+ /* Connect to an inet socket. */
+ strncpy(buf, display, sizeof(buf) - 1);
+ cp = strchr(buf, ':');
+ if (!cp) {
+ REMMINA_WARNING("Could not find ':' in DISPLAY: %.100s", display);
+ return -1;
+ }
+ *cp = 0;
+ if (sscanf(cp + 1, "%u", &display_number) != 1) {
+ REMMINA_WARNING("Could not parse display number from DISPLAY: %.100s", display);
+ return -1;
+ }
+
+ /* Look up the host address */
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_INET;
+ hints.ai_socktype = SOCK_STREAM;
+ snprintf(strport, sizeof(strport), "%u", 6000 + display_number);
+ if ((gaierr = getaddrinfo(buf, strport, &hints, &aitop)) != 0) {
+ REMMINA_WARNING("%.100s: unknown host. (%s)", buf, remmina_ssh_ssh_gai_strerror(gaierr));
+ return -1;
+ }
+ for (ai = aitop; ai; ai = ai->ai_next) {
+ /* Create a socket. */
+ sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+ if (sock == -1) {
+ REMMINA_WARNING("socket: %.100s", strerror(errno));
+ continue;
+ }
+ /* Connect it to the display. */
+ if (connect(sock, ai->ai_addr, ai->ai_addrlen) == -1) {
+ REMMINA_WARNING("connect %.100s port %u: %.100s", buf, 6000 + display_number, strerror(errno));
+ close(sock);
+ continue;
+ }
+ /* Success */
+ break;
+ }
+ freeaddrinfo(aitop);
+ if (!ai) {
+ REMMINA_WARNING("connect %.100s port %u: %.100s", buf, 6000 + display_number, strerror(errno));
+ return -1;
+ }
+ remmina_ssh_set_nodelay(sock);
+
+ REMMINA_DEBUG("sock: %d", sock);
+
+ return sock;
+}
+
+static int
+remmina_ssh_cp_to_ch_cb(int fd, int revents, void *userdata)
+{
+ TRACE_CALL(__func__);
+ ssh_channel channel = (ssh_channel)userdata;
+ gchar *buf = (gchar *) g_malloc ( sizeof(gchar) * 0x200000 );
+ if (buf ==NULL){
+ return -1;
+ }
+ gint sz = 0, ret = 0;
+
+ node_t *temp_node = remmina_ssh_search_item(channel);
+
+ if (!channel) {
+ if (!temp_node->protected) {
+ shutdown(fd, SHUT_RDWR);
+ close(fd);
+ REMMINA_DEBUG("fd %d closed.", fd);
+ }
+ REMMINA_WARNING("channel does not exist.");
+ return -1;
+ }
+
+ if ((revents & POLLIN) || (revents & POLLPRI)) {
+ sz = read(fd, buf, sizeof(buf));
+ if (sz > 0) {
+ ret = ssh_channel_write(channel, buf, sz);
+ if (ret != sz){
+ g_free(buf);
+ return -1;
+ }
+
+ //TODO: too verbose REMMINA_DEBUG("ssh_channel_write ret: %d sz: %d", ret, sz);
+ } else if (sz < 0) {
+ // TODO: too verbose REMMINA_WARNING("fd bytes read: %d", sz);
+ g_free(buf);
+ return -1;
+ } else {
+ REMMINA_WARNING("Why the hell am I here?");
+ if (!temp_node->protected) {
+ shutdown(fd, SHUT_RDWR);
+ close(fd);
+ REMMINA_DEBUG("fd %d closed.", fd);
+ }
+ g_free(buf);
+ return -1;
+ }
+ }
+
+ if ((revents & POLLHUP) || (revents & POLLNVAL) || (revents & POLLERR)) {
+ REMMINA_DEBUG("Closing channel.");
+ ssh_channel_close(channel);
+ ret = -1;
+ }
+ g_free(buf);
+ return ret;
+}
+
+static int
+remmina_ssh_cp_to_fd_cb(ssh_session session, ssh_channel channel, void *data, uint32_t len, int is_stderr, void *userdata)
+{
+ TRACE_CALL(__func__);
+ (void)session;
+ (void)is_stderr;
+ // Expecting userdata to be type RemminaSSHShell *, but it is unused
+ // in this function.
+ (void)userdata;
+
+ node_t *temp_node = remmina_ssh_search_item(channel);
+ gint fd = temp_node->fd_out;
+ gint sz = 0;
+
+ sz = write(fd, data, len);
+ // TODO: too verbose REMMINA_DEBUG("fd bytes written: %d", sz);
+
+ return sz;
+}
+
+static void
+remmina_ssh_ch_close_cb(ssh_session session, ssh_channel channel, void *userdata)
+{
+ TRACE_CALL(__func__);
+ (void)session;
+
+ RemminaSSHShell *shell = (RemminaSSHShell *)userdata;
+
+ node_t *temp_node = remmina_ssh_search_item(channel);
+
+ if (temp_node != NULL) {
+ int fd = temp_node->fd_in;
+
+ if (!temp_node->protected) {
+ remmina_ssh_delete_item(channel);
+ ssh_event_remove_fd(shell->event, fd);
+ shutdown(fd, SHUT_RDWR);
+ close(fd);
+ REMMINA_DEBUG("fd %d closed.", fd);
+ }
+ }
+ REMMINA_DEBUG("Channel closed.");
+}
+
+static void
+remmina_ssh_close_all_x11_ch(pthread_t thread)
+{
+ TRACE_CALL(__func__);
+
+ REMMINA_DEBUG("Close all X11 channels");
+
+ node_t *current = node;
+ while (current != NULL) {
+ if (current->thread == thread && !current->protected) {
+ shutdown(current->fd_in, SHUT_RDWR);
+ close(current->fd_in);
+ REMMINA_DEBUG("thread: %d - fd %d closed.", thread, current->fd_in);
+ if (current->fd_in != current->fd_out) {
+ shutdown(current->fd_out, SHUT_RDWR);
+ close(current->fd_out);
+ REMMINA_DEBUG("thread: %d - fd %d closed.", thread, current->fd_out);
+ }
+ }
+ current = current->next;
+ }
+}
+
+static ssh_channel
+remmina_ssh_x11_open_request_cb(ssh_session session, const char *shost, int sport, void *userdata)
+{
+ TRACE_CALL(__func__);
+
+ (void)shost;
+ (void)sport;
+
+ RemminaSSHShell *shell = (RemminaSSHShell *)userdata;
+
+ ssh_channel channel = ssh_channel_new(session);
+
+ int sock = remmina_ssh_x11_connect_display();
+
+ remmina_ssh_insert_item(channel, sock, sock, FALSE, shell->thread);
+
+ ssh_event_add_fd(shell->event, sock, events, remmina_ssh_cp_to_ch_cb, channel);
+ ssh_event_add_session(shell->event, session);
+
+ ssh_add_channel_callbacks(channel, &channel_cb);
+
+ return channel;
+}
+
+gchar *
+remmina_ssh_identity_path(const gchar *id)
+{
+ TRACE_CALL(__func__);
+ if (id == NULL) return NULL;
+ if (id[0] == '/') return g_strdup(id);
+ return g_strdup_printf("%s/%s", g_get_home_dir(), id);
+}
+
+gchar *
+remmina_ssh_find_identity(void)
+{
+ TRACE_CALL(__func__);
+ gchar *path;
+ gint i;
+
+ for (i = 0; common_identities[i]; i++) {
+ path = remmina_ssh_identity_path(common_identities[i]);
+ if (g_file_test(path, G_FILE_TEST_IS_REGULAR | G_FILE_TEST_EXISTS))
+ return path;
+ g_free(path);
+ }
+ return NULL;
+}
+
+void
+remmina_ssh_set_error(RemminaSSH *ssh, const gchar *fmt)
+{
+ TRACE_CALL(__func__);
+ const gchar *err;
+
+ err = ssh_get_error(ssh->session);
+ ssh->error = g_strdup_printf(fmt, err);
+}
+
+void
+remmina_ssh_set_application_error(RemminaSSH *ssh, const gchar *fmt, ...)
+{
+ TRACE_CALL(__func__);
+ va_list args;
+
+ va_start(args, fmt);
+ ssh->error = g_strdup_vprintf(fmt, args);
+ va_end(args);
+}
+
+static enum remmina_ssh_auth_result
+remmina_ssh_auth_interactive(RemminaSSH *ssh)
+{
+ TRACE_CALL(__func__);
+ gint ret;
+ gint n;
+ gint i;
+ const gchar *name, *instruction = NULL;
+ //gchar *prompt,*ptr;
+
+ ret = SSH_AUTH_ERROR;
+ if (ssh->authenticated) return REMMINA_SSH_AUTH_SUCCESS;
+ /* TODO: What if I have an empty password? */
+ if (ssh->password == NULL) {
+ remmina_ssh_set_error(ssh, "OTP code is empty");
+ REMMINA_DEBUG("OTP code is empty, returning");
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+ REMMINA_DEBUG("OTP code has been set to: %s", ssh->password);
+
+ ret = ssh_userauth_kbdint(ssh->session, NULL, NULL);
+ while (ret == SSH_AUTH_INFO) {
+ name = ssh_userauth_kbdint_getname(ssh->session);
+ if (strlen(name) > 0)
+ REMMINA_DEBUG("SSH kbd-interactive name: %s", name);
+ else
+ REMMINA_DEBUG("SSH kbd-interactive name is empty");
+ instruction = ssh_userauth_kbdint_getinstruction(ssh->session);
+ if (strlen(instruction) > 0)
+ REMMINA_DEBUG("SSH kbd-interactive instruction: %s", instruction);
+ else
+ REMMINA_DEBUG("SSH kbd-interactive instruction is empty");
+ n = ssh_userauth_kbdint_getnprompts(ssh->session);
+ for (i = 0; i < n; i++)
+ ssh_userauth_kbdint_setanswer(ssh->session, i, ssh->password);
+ ret = ssh_userauth_kbdint(ssh->session, NULL, NULL);
+ }
+
+
+ REMMINA_DEBUG("ssh_userauth_kbdint returned %d", ret);
+ switch (ret) {
+ case SSH_AUTH_PARTIAL:
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ //You've been partially authenticated, you still have to use another method
+ REMMINA_DEBUG("Authenticated with SSH keyboard interactive. Another method is required. %d", ret);
+ ssh->is_multiauth = TRUE;
+ return REMMINA_SSH_AUTH_PARTIAL;
+ break;
+ case SSH_AUTH_SUCCESS:
+ //Authentication success
+ ssh->authenticated = TRUE;
+ REMMINA_DEBUG("Authenticated with SSH keyboard interactive. %s", ssh->error);
+ return REMMINA_SSH_AUTH_SUCCESS;
+ break;
+ case SSH_AUTH_INFO:
+ //The server asked some questions. Use ssh_userauth_kbdint_getnprompts() and such.
+ REMMINA_DEBUG("Authenticating aagin with SSH keyboard interactive??? %s", ssh->error);
+ break;
+ case SSH_AUTH_AGAIN:
+ //In nonblocking mode, you've got to call this again later.
+ REMMINA_DEBUG("Authenticated with keyboard interactive, Requested to authenticate again. %s", ssh->error);
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ break;
+ case SSH_AUTH_DENIED:
+ case SSH_AUTH_ERROR:
+ default:
+ //A serious error happened
+ ssh->authenticated = FALSE;
+ remmina_ssh_set_error(ssh, _("Could not authenticate with TOTP/OTP/2FA. %s"));
+ REMMINA_DEBUG("Cannot authenticate with TOTP/OTP/2FA. Error is %s", ssh->error);
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+ ssh->authenticated = FALSE;
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+}
+
+static enum remmina_ssh_auth_result
+remmina_ssh_auth_password(RemminaSSH *ssh)
+{
+ TRACE_CALL(__func__);
+ gint ret;
+
+ REMMINA_DEBUG("Password authentication");
+
+ ret = SSH_AUTH_ERROR;
+ if (ssh->authenticated) {
+ REMMINA_DEBUG("Already authenticated");
+ return REMMINA_SSH_AUTH_SUCCESS;
+ }
+ if (ssh->password == NULL) {
+ remmina_ssh_set_error(ssh, "Password is null");
+ REMMINA_DEBUG("Password is null, returning");
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+
+ ret = ssh_userauth_password(ssh->session, NULL, ssh->password);
+ REMMINA_DEBUG("Authentication with SSH password returned: %d", ret);
+
+ switch (ret) {
+ case SSH_AUTH_PARTIAL:
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ //You've been partially authenticated, you still have to use another method.
+ REMMINA_DEBUG("Authenticated with SSH password, Another method is required. %d", ret);
+ ssh->is_multiauth = TRUE;
+ return REMMINA_SSH_AUTH_PARTIAL;
+ break;
+ case SSH_AUTH_SUCCESS:
+ //The public key is accepted.
+ ssh->authenticated = TRUE;
+ REMMINA_DEBUG("Authenticated with SSH password. %s", ssh->error);
+ return REMMINA_SSH_AUTH_SUCCESS;
+ break;
+ case SSH_AUTH_AGAIN:
+ //In nonblocking mode, you've got to call this again later.
+ REMMINA_DEBUG("Authenticated with SSH password, Requested to authenticate again. %s", ssh->error);
+ ssh->authenticated = FALSE;
+ return REMMINA_SSH_AUTH_AGAIN;
+ break;
+ case SSH_AUTH_DENIED:
+ case SSH_AUTH_ERROR:
+ default:
+ //A serious error happened.
+ ssh->authenticated = FALSE;
+ REMMINA_DEBUG("Cannot authenticate with password. Error is %s", ssh->error);
+ remmina_ssh_set_error(ssh, _("Could not authenticate with SSH password. %s"));
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+ ssh->authenticated = FALSE;
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+}
+
+static enum remmina_ssh_auth_result
+remmina_ssh_auth_pubkey(RemminaSSH *ssh, RemminaProtocolWidget *gp, RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+
+ ssh_key key = NULL;
+ ssh_key cert = NULL;
+ gchar pubkey[132] = { 0 }; // +".pub"
+ gint ret;
+
+ if (ssh->authenticated) return REMMINA_SSH_AUTH_SUCCESS;
+
+ REMMINA_DEBUG("SSH certificate file: %s", ssh->certfile);
+ REMMINA_DEBUG("File for private SSH key: %s", ssh->privkeyfile);
+ if (ssh->certfile != NULL) {
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 9, 0)
+ /* First we import the private key */
+ if (ssh_pki_import_privkey_file(ssh->privkeyfile, (ssh->passphrase ? ssh->passphrase : ""),
+ NULL, NULL, &key) != SSH_OK) {
+ if (ssh->passphrase == NULL || ssh->passphrase[0] == '\0') {
+ remmina_ssh_set_error(ssh, _("No saved SSH password supplied. Asking user to enter it."));
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(ssh, _("Could not authenticate with public SSH key. %s"));
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+ REMMINA_DEBUG ("Imported private SSH key file");
+ /* First we import the certificate */
+ ret = ssh_pki_import_cert_file(ssh->certfile, &cert );
+ if (ret != SSH_OK) {
+ REMMINA_DEBUG ("Certificate import returned: %d", ret);
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(ssh, _("SSH certificate cannot be imported. %s"));
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ REMMINA_DEBUG ("certificate imported correctly");
+ /* We copy th certificate in the private key */
+ ret = ssh_pki_copy_cert_to_privkey(cert, key);
+ if (ret != SSH_OK) {
+ REMMINA_DEBUG ("Copying the certificate into a key returned: %d", ret);
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(ssh, _("SSH certificate cannot be copied into the private SSH key. %s"));
+ ssh_key_free(cert);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ REMMINA_DEBUG ("%s certificate copied into the private SSH key", ssh->certfile);
+ /* We try to authenticate */
+ ret = ssh_userauth_try_publickey(ssh->session, NULL, cert);
+ if (ret != SSH_AUTH_SUCCESS && ret != SSH_AUTH_AGAIN ) {
+ REMMINA_DEBUG ("Trying to authenticate with the new key returned: %d", ret);
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(ssh, _("Could not authenticate using SSH certificate. %s"));
+ ssh_key_free(key);
+ ssh_key_free(cert);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ REMMINA_DEBUG ("Authentication with a certificate file works, we can authenticate");
+#else
+ REMMINA_DEBUG ("lbssh >= 0.9.0 is required to authenticate with certificate file");
+#endif
+ /* if it goes well we authenticate (later on) with the key, not the cert*/
+ } else {
+ if (ssh->privkeyfile == NULL) {
+ // TRANSLATORS: The placeholder %s is an error message
+ ssh->error = g_strdup_printf(_("Could not authenticate with public SSH key. %s"),
+ _("SSH identity file not selected."));
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+
+ // Check for empty username
+ if (ssh->user == NULL) {
+ remmina_ssh_set_error(ssh, _("No username found. Asking user to enter it."));
+ return REMMINA_SSH_AUTH_AUTHFAILED_EMPTY_USERNAME;
+ }
+
+ g_snprintf(pubkey, sizeof(pubkey), "%s.pub", ssh->privkeyfile);
+
+ /*G_FILE_TEST_EXISTS*/
+ if (g_file_test(pubkey, G_FILE_TEST_EXISTS)) {
+ ret = ssh_pki_import_pubkey_file(pubkey, &key);
+ if (ret != SSH_OK) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(ssh, _("Public SSH key cannot be imported. %s"));
+ ssh_key_free(key);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ }
+
+ if (ssh_pki_import_privkey_file(ssh->privkeyfile, (ssh->passphrase ? ssh->passphrase : ""),
+ NULL, NULL, &key) != SSH_OK) {
+ if (ssh->passphrase == NULL || ssh->passphrase[0] == '\0') {
+ remmina_ssh_set_error(ssh, _("No saved SSH password supplied. Asking user to enter it."));
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(ssh, _("Could not authenticate with public SSH key. %s"));
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+ }
+
+ ret = ssh_userauth_publickey(ssh->session, NULL, key);
+ ssh_key_free(key);
+ ssh_key_free(cert);
+ REMMINA_DEBUG("Authentication with public SSH key returned: %d", ret);
+
+ switch (ret) {
+ case SSH_AUTH_PARTIAL:
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ //You've been partially authenticated, you still have to use another method.
+ REMMINA_DEBUG("Authenticated with public SSH key, Another method is required. %d", ret);
+ ssh->is_multiauth = TRUE;
+ return REMMINA_SSH_AUTH_PARTIAL;
+ break;
+ case SSH_AUTH_SUCCESS:
+ //The public key is accepted.
+ ssh->authenticated = TRUE;
+ REMMINA_DEBUG("Authenticated with public SSH key. %s", ssh->error);
+ return REMMINA_SSH_AUTH_SUCCESS;
+ break;
+ case SSH_AUTH_AGAIN:
+ //In nonblocking mode, you've got to call this again later.
+ REMMINA_DEBUG("Authenticated with public SSH key, Requested to authenticate again. %s", ssh->error);
+ ssh->authenticated = FALSE;
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ break;
+ case SSH_AUTH_DENIED:
+ case SSH_AUTH_ERROR:
+ default:
+ //A serious error happened.
+ ssh->authenticated = FALSE;
+ REMMINA_DEBUG("Could not authenticate with public SSH key. %s", ssh->error);
+ remmina_ssh_set_error(ssh, _("Could not authenticate with public SSH key. %s"));
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+}
+
+static enum remmina_ssh_auth_result
+remmina_ssh_auth_auto_pubkey(RemminaSSH *ssh, RemminaProtocolWidget *gp, RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+
+ gint ret;
+ ret = ssh_userauth_publickey_auto(ssh->session, NULL, (ssh->passphrase ? ssh->passphrase : NULL));
+
+ REMMINA_DEBUG("Authentication with public SSH key returned: %d", ret);
+
+ switch (ret) {
+ case SSH_AUTH_PARTIAL:
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ //You've been partially authenticated, you still have to use another method.
+ REMMINA_DEBUG("Authenticated with public SSH key, Another method is required. %d", ret);
+ ssh->is_multiauth = TRUE;
+ return REMMINA_SSH_AUTH_PARTIAL;
+ break;
+ case SSH_AUTH_SUCCESS:
+ //The public key is accepted.
+ ssh->authenticated = TRUE;
+ REMMINA_DEBUG("Authenticated with public SSH key. %s", ssh->error);
+ return REMMINA_SSH_AUTH_SUCCESS;
+ break;
+ case SSH_AUTH_AGAIN:
+ //In nonblocking mode, you've got to call this again later.
+ REMMINA_DEBUG("Authenticated with public SSH key, Requested to authenticate again. %s", ssh->error);
+ ssh->authenticated = FALSE;
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ break;
+ case SSH_AUTH_DENIED:
+ case SSH_AUTH_ERROR:
+ default:
+ //A serious error happened.
+ ssh->authenticated = FALSE;
+ REMMINA_DEBUG("Cannot authenticate automatically with public SSH key. %s", ssh->error);
+ remmina_ssh_set_error(ssh, _("Could not authenticate automatically with public SSH key. %s"));
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+ ssh->authenticated = FALSE;
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+}
+
+static enum remmina_ssh_auth_result
+remmina_ssh_auth_agent(RemminaSSH *ssh)
+{
+ TRACE_CALL(__func__);
+ gint ret;
+ ret = ssh_userauth_agent(ssh->session, NULL);
+
+ REMMINA_DEBUG("Authentication with SSH agent returned: %d", ret);
+
+ switch (ret) {
+ case SSH_AUTH_PARTIAL:
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ //You've been partially authenticated, you still have to use another method.
+ REMMINA_DEBUG("Authenticated with public SSH key, Another method is required. %d", ret);
+ ssh->is_multiauth = TRUE;
+ return REMMINA_SSH_AUTH_PARTIAL;
+ break;
+ case SSH_AUTH_SUCCESS:
+ //The public key is accepted.
+ ssh->authenticated = TRUE;
+ REMMINA_DEBUG("Authenticated with public SSH key. %s", ssh->error);
+ return REMMINA_SSH_AUTH_SUCCESS;
+ break;
+ case SSH_AUTH_AGAIN:
+ //In nonblocking mode, you've got to call this again later.
+ REMMINA_DEBUG("Authenticated with public SSH key, Requested to authenticate again. %s", ssh->error);
+ ssh->authenticated = FALSE;
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ break;
+ case SSH_AUTH_DENIED:
+ case SSH_AUTH_ERROR:
+ default:
+ //A serious error happened.
+ ssh->authenticated = FALSE;
+ REMMINA_DEBUG("Cannot authenticate automatically with SSH agent. %s", ssh->error);
+ remmina_ssh_set_error(ssh, _("Could not authenticate automatically with SSH agent. %s"));
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+ ssh->authenticated = FALSE;
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+
+}
+
+static enum remmina_ssh_auth_result
+remmina_ssh_auth_gssapi(RemminaSSH *ssh)
+{
+ TRACE_CALL(__func__);
+ gint ret;
+
+ ret = ssh_userauth_gssapi(ssh->session);
+ REMMINA_DEBUG("Authentication with SSH GSSAPI/Kerberos: %d", ret);
+
+ switch (ret) {
+ case SSH_AUTH_PARTIAL:
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ //You've been partially authenticated, you still have to use another method.
+ REMMINA_DEBUG("Authenticated with public SSH key, Another method is required. %d", ret);
+ ssh->is_multiauth = TRUE;
+ return REMMINA_SSH_AUTH_PARTIAL;
+ break;
+ case SSH_AUTH_SUCCESS:
+ //The public key is accepted.
+ ssh->authenticated = TRUE;
+ REMMINA_DEBUG("Authenticated with public SSH key. %s", ssh->error);
+ return REMMINA_SSH_AUTH_SUCCESS;
+ break;
+ case SSH_AUTH_AGAIN:
+ //In nonblocking mode, you've got to call this again later.
+ REMMINA_DEBUG("Authenticated with public SSH key, Requested to authenticate again. %s", ssh->error);
+ ssh->authenticated = FALSE;
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ break;
+ case SSH_AUTH_DENIED:
+ case SSH_AUTH_ERROR:
+ default:
+ //A serious error happened.
+ ssh->authenticated = FALSE;
+ REMMINA_DEBUG("Cannot authenticate with SSH GSSAPI/Kerberos. %s", ssh->error);
+ remmina_ssh_set_error(ssh, _("Could not authenticate with SSH GSSAPI/Kerberos. %s"));
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+ ssh->authenticated = FALSE;
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+}
+
+enum remmina_ssh_auth_result
+remmina_ssh_auth(RemminaSSH *ssh, const gchar *password, RemminaProtocolWidget *gp, RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ gint method;
+ enum remmina_ssh_auth_result rv = REMMINA_SSH_AUTH_NULL;
+
+ /* Check known host again to ensure it’s still the original server when user forks
+ * a new session from existing one */
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 9, 0)
+ /* TODO: Add error checking
+ * SSH_KNOWN_HOSTS_OK: The server is known and has not changed.
+ * SSH_KNOWN_HOSTS_CHANGED: The server key has changed. Either you are under attack or the administrator changed the key. You HAVE to warn the user about a possible attack.
+ * SSH_KNOWN_HOSTS_OTHER: The server gave use a key of a type while we had an other type recorded. It is a possible attack.
+ * SSH_KNOWN_HOSTS_UNKNOWN: The server is unknown. User should confirm the public key hash is correct.
+ * SSH_KNOWN_HOSTS_NOT_FOUND: The known host file does not exist. The host is thus unknown. File will be created if host key is accepted.
+ * SSH_KNOWN_HOSTS_ERROR: There had been an error checking the host.
+ */
+ if (ssh_session_is_known_server(ssh->session) != SSH_KNOWN_HOSTS_OK) {
+#else
+ if (ssh_is_server_known(ssh->session) != SSH_SERVER_KNOWN_OK) {
+#endif
+ remmina_ssh_set_application_error(ssh, _("The public SSH key changed!"));
+ return REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT;
+ }
+
+ if (password) {
+ if (password != ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ if (password != ssh->passphrase) g_free(ssh->passphrase);
+ ssh->password = g_strdup(password);
+ ssh->passphrase = g_strdup(password);
+ }
+
+ /** @todo Here we should call
+ * gint method;
+ * method = ssh_userauth_list(ssh->session, NULL);
+ *
+ * #define SSH_AUTH_METHOD_UNKNOWN 0x0000u
+ * #define SSH_AUTH_METHOD_NONE 0x0001u
+ * #define SSH_AUTH_METHOD_PASSWORD 0x0002u
+ * #define SSH_AUTH_METHOD_PUBLICKEY 0x0004u
+ * #define SSH_AUTH_METHOD_HOSTBASED 0x0008u
+ * #define SSH_AUTH_METHOD_INTERACTIVE 0x0010u
+ * #define SSH_AUTH_METHOD_GSSAPI_MIC 0x0020u
+ *
+ * And than test both the method and the option selected by the user
+ */
+ method = ssh_userauth_list(ssh->session, NULL);
+ REMMINA_DEBUG("Methods supported by server: %s%s%s%s%s%s%s",
+ (method & SSH_AUTH_METHOD_NONE) ? "SSH_AUTH_METHOD_NONE " : "",
+ (method & SSH_AUTH_METHOD_UNKNOWN) ? "SSH_AUTH_METHOD_UNKNOWN " : "",
+ (method & SSH_AUTH_METHOD_PASSWORD) ? "SSH_AUTH_METHOD_PASSWORD " : "",
+ (method & SSH_AUTH_METHOD_PUBLICKEY) ? "SSH_AUTH_METHOD_PUBLICKEY " : "",
+ (method & SSH_AUTH_METHOD_HOSTBASED) ? "SSH_AUTH_METHOD_HOSTBASED " : "",
+ (method & SSH_AUTH_METHOD_INTERACTIVE) ? "SSH_AUTH_METHOD_INTERACTIVE " : "",
+ (method & SSH_AUTH_METHOD_GSSAPI_MIC) ? "SSH_AUTH_METHOD_GSSAPI_MIC " : ""
+ );
+ switch (ssh->auth) {
+ case SSH_AUTH_PASSWORD:
+ /* This authentication method is normally disabled on SSHv2 server. You should use keyboard-interactive mode. */
+ REMMINA_DEBUG("SSH_AUTH_PASSWORD (%d)", ssh->auth);
+ if (ssh->authenticated)
+ return REMMINA_SSH_AUTH_SUCCESS;
+ if (method & SSH_AUTH_METHOD_PASSWORD) {
+ REMMINA_DEBUG("SSH using remmina_ssh_auth_password");
+ rv = remmina_ssh_auth_password(ssh);
+ }
+ if (!ssh->authenticated && (method & SSH_AUTH_METHOD_INTERACTIVE)) {
+ /* SSH server is requesting us to do interactive auth. */
+ REMMINA_DEBUG("SSH using remmina_ssh_auth_interactive after password has failed");
+ rv = remmina_ssh_auth_interactive(ssh);
+ }
+ if (rv == REMMINA_SSH_AUTH_PARTIAL) {
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ switch (ssh_userauth_list(ssh->session, NULL)) {
+ case SSH_AUTH_METHOD_PASSWORD:
+ ssh->auth = SSH_AUTH_PASSWORD;
+ break;
+ case SSH_AUTH_METHOD_PUBLICKEY:
+ ssh->auth = SSH_AUTH_PUBLICKEY;
+ break;
+ case SSH_AUTH_METHOD_HOSTBASED:
+ REMMINA_DEBUG("Host-based authentication method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_INTERACTIVE:
+ ssh->auth = SSH_AUTH_KBDINTERACTIVE;
+ //REMMINA_DEBUG("Interactive auth method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_UNKNOWN:
+ default:
+ REMMINA_DEBUG("User-based authentication method not supported: %d", ssh->auth);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ }
+ ssh->error = g_strdup_printf(_("Could not authenticate with SSH password. %s"), "");
+ return rv;
+ break;
+
+ case SSH_AUTH_KBDINTERACTIVE:
+ REMMINA_DEBUG("SSH using remmina_ssh_auth_interactive");
+ if (method & SSH_AUTH_METHOD_INTERACTIVE) {
+ rv = remmina_ssh_auth_interactive(ssh);
+ if (rv == REMMINA_SSH_AUTH_PARTIAL) {
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ switch (ssh_userauth_list(ssh->session, NULL)) {
+ case SSH_AUTH_METHOD_PASSWORD:
+ ssh->auth = SSH_AUTH_PASSWORD;
+ break;
+ case SSH_AUTH_METHOD_PUBLICKEY:
+ ssh->auth = SSH_AUTH_PUBLICKEY;
+ break;
+ case SSH_AUTH_METHOD_HOSTBASED:
+ REMMINA_DEBUG("Host-based authentication method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_INTERACTIVE:
+ ssh->auth = SSH_AUTH_KBDINTERACTIVE;
+ //REMMINA_DEBUG("Interactive auth method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_UNKNOWN:
+ default:
+ REMMINA_DEBUG("User-based authentication method not supported: %d", ssh->auth);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ }
+ return rv;
+ }
+ ssh->error = g_strdup_printf(_("Could not authenticate with keyboard-interactive. %s"), "");
+ break;
+
+ case SSH_AUTH_PUBLICKEY:
+ REMMINA_DEBUG("SSH_AUTH_PUBLICKEY (%d)", ssh->auth);
+ if (method & SSH_AUTH_METHOD_PUBLICKEY) {
+ rv = remmina_ssh_auth_pubkey(ssh, gp, remminafile);
+ if (rv == REMMINA_SSH_AUTH_PARTIAL) {
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ switch (ssh_userauth_list(ssh->session, NULL)) {
+ case SSH_AUTH_METHOD_PASSWORD:
+ ssh->auth = SSH_AUTH_PASSWORD;
+ break;
+ case SSH_AUTH_METHOD_PUBLICKEY:
+ ssh->auth = SSH_AUTH_PUBLICKEY;
+ break;
+ case SSH_AUTH_METHOD_HOSTBASED:
+ REMMINA_DEBUG("Host based auth method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_INTERACTIVE:
+ ssh->auth = SSH_AUTH_KBDINTERACTIVE;
+ //REMMINA_DEBUG("Interactive auth method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_UNKNOWN:
+ default:
+ REMMINA_DEBUG("User auth method not supported: %d", ssh->auth);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ }
+ return rv;
+ }
+ // The real error here should be: "The SSH server %s:%d does not support public key authentication"
+ ssh->error = g_strdup_printf(_("Could not authenticate with public SSH key. %s"), "");
+ break;
+
+ case SSH_AUTH_AGENT:
+ REMMINA_DEBUG("SSH_AUTH_AGENT (%d)", ssh->auth);
+ rv = remmina_ssh_auth_agent(ssh);
+ if (rv == REMMINA_SSH_AUTH_PARTIAL) {
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ switch (ssh_userauth_list(ssh->session, NULL)) {
+ case SSH_AUTH_METHOD_PASSWORD:
+ ssh->auth = SSH_AUTH_PASSWORD;
+ break;
+ case SSH_AUTH_METHOD_PUBLICKEY:
+ ssh->auth = SSH_AUTH_PUBLICKEY;
+ break;
+ case SSH_AUTH_METHOD_HOSTBASED:
+ REMMINA_DEBUG("Host based auth method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_INTERACTIVE:
+ ssh->auth = SSH_AUTH_KBDINTERACTIVE;
+ //REMMINA_DEBUG("Interactive auth method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_UNKNOWN:
+ default:
+ REMMINA_DEBUG("User auth method not supported: %d", ssh->auth);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ }
+ return rv;
+ break;
+
+ case SSH_AUTH_AUTO_PUBLICKEY:
+ REMMINA_DEBUG("SSH_AUTH_AUTO_PUBLICKEY (%d)", ssh->auth);
+ rv = remmina_ssh_auth_auto_pubkey(ssh, gp, remminafile);
+ /* ssh_agent or none */
+ if (method & SSH_AUTH_METHOD_PUBLICKEY) {
+ if (rv == REMMINA_SSH_AUTH_PARTIAL) {
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ switch (ssh_userauth_list(ssh->session, NULL)) {
+ case SSH_AUTH_METHOD_PASSWORD:
+ ssh->auth = SSH_AUTH_PASSWORD;
+ break;
+ case SSH_AUTH_METHOD_PUBLICKEY:
+ ssh->auth = SSH_AUTH_PUBLICKEY;
+ break;
+ case SSH_AUTH_METHOD_HOSTBASED:
+ REMMINA_DEBUG("Host based auth method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_INTERACTIVE:
+ ssh->auth = SSH_AUTH_KBDINTERACTIVE;
+ //REMMINA_DEBUG("Interactive auth method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_UNKNOWN:
+ default:
+ REMMINA_DEBUG("User auth method not supported: %d", ssh->auth);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ }
+ return rv;
+ }
+ // The real error here should be: "The SSH server %s:%d does not support public key authentication"
+ ssh->error = g_strdup_printf(_("Could not authenticate with automatic public SSH key. %s"), "");
+ break;
+
+#if 0
+ /* Not yet supported by libssh */
+ case SSH_AUTH_HOSTBASED:
+ if (method & SSH_AUTH_METHOD_HOSTBASED)
+ //return remmina_ssh_auth_hostbased;
+ return 0;
+#endif
+
+ case SSH_AUTH_GSSAPI:
+ REMMINA_DEBUG("SSH_AUTH_GSSAPI (%d)", ssh->auth);
+ if (method & SSH_AUTH_METHOD_GSSAPI_MIC) {
+ rv = remmina_ssh_auth_gssapi(ssh);
+ if (rv == REMMINA_SSH_AUTH_PARTIAL) {
+ if (ssh->password) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ switch (ssh_userauth_list(ssh->session, NULL)) {
+ case SSH_AUTH_METHOD_PASSWORD:
+ ssh->auth = SSH_AUTH_PASSWORD;
+ break;
+ case SSH_AUTH_METHOD_PUBLICKEY:
+ ssh->auth = SSH_AUTH_PUBLICKEY;
+ break;
+ case SSH_AUTH_METHOD_HOSTBASED:
+ REMMINA_DEBUG("Host based auth method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_INTERACTIVE:
+ ssh->auth = SSH_AUTH_KBDINTERACTIVE;
+ //REMMINA_DEBUG("Interactive auth method not implemented: %d", ssh->auth);
+ break;
+ case SSH_AUTH_METHOD_UNKNOWN:
+ default:
+ REMMINA_DEBUG("User auth method not supported: %d", ssh->auth);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ }
+ return rv;
+ }
+ // The real error here should be: "The SSH server %s:%d does not support SSH GSSAPI/Kerberos authentication"
+ ssh->error = g_strdup_printf(_("Could not authenticate with SSH GSSAPI/Kerberos. %s"), "");
+ break;
+
+ default:
+ REMMINA_DEBUG("User auth method not supported: %d", ssh->auth);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+
+ // We come here after a "break". ssh->error should be already set
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+}
+
+enum remmina_ssh_auth_result
+remmina_ssh_auth_gui(RemminaSSH *ssh, RemminaProtocolWidget *gp, RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ gchar *keyname;
+ gchar *pwdfkey = NULL;
+ gchar *message;
+ gchar *current_pwd;
+ gchar *current_user;
+ const gchar *instruction = NULL;
+ gint ret;
+ size_t len;
+ guchar *pubkey;
+ ssh_key server_pubkey;
+ gboolean disablepasswordstoring;
+ gboolean save_password;
+ gint attempt;
+
+ /* Check if the server’s public key is known */
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 9, 0)
+ /* TODO: Add error checking
+ * SSH_KNOWN_HOSTS_OK: The server is known and has not changed.
+ * SSH_KNOWN_HOSTS_CHANGED: The server key has changed. Either you are under attack or the administrator changed the key. You HAVE to warn the user about a possible attack.
+ * SSH_KNOWN_HOSTS_OTHER: The server gave use a key of a type while we had an other type recorded. It is a possible attack.
+ * SSH_KNOWN_HOSTS_UNKNOWN: The server is unknown. User should confirm the public key hash is correct.
+ * SSH_KNOWN_HOSTS_NOT_FOUND: The known host file does not exist. The host is thus unknown. File will be created if host key is accepted.
+ * SSH_KNOWN_HOSTS_ERROR: There had been an error checking the host.
+ */
+ ret = ssh_session_is_known_server(ssh->session);
+ switch (ret) {
+ case SSH_KNOWN_HOSTS_OK:
+ break; /* ok */
+
+ /* TODO: These are all wrong, we should deal with each of them */
+ case SSH_KNOWN_HOSTS_CHANGED:
+ case SSH_KNOWN_HOSTS_OTHER:
+ case SSH_KNOWN_HOSTS_UNKNOWN:
+ case SSH_KNOWN_HOSTS_NOT_FOUND:
+#else
+ ret = ssh_is_server_known(ssh->session);
+ switch (ret) {
+ case SSH_SERVER_KNOWN_OK:
+ break; /* ok */
+
+ /* fallback to SSH_SERVER_NOT_KNOWN behavior */
+ case SSH_SERVER_KNOWN_CHANGED:
+ case SSH_SERVER_FOUND_OTHER:
+ case SSH_SERVER_NOT_KNOWN:
+ case SSH_SERVER_FILE_NOT_FOUND:
+#endif
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 8, 6)
+ if (ssh_get_server_publickey(ssh->session, &server_pubkey) != SSH_OK) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(ssh, _("Could not fetch the server\'s public SSH key. %s"));
+ REMMINA_DEBUG("ssh_get_server_publickey() has failed");
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+#else
+ if (ssh_get_publickey(ssh->session, &server_pubkey) != SSH_OK) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(ssh, _("Could not fetch public SSH key. %s"));
+ REMMINA_DEBUG("ssh_get_publickey() has failed");
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+#endif
+ if (ssh_get_publickey_hash(server_pubkey, SSH_PUBLICKEY_HASH_MD5, &pubkey, &len) != 0) {
+ ssh_key_free(server_pubkey);
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(ssh, _("Could not fetch checksum of the public SSH key. %s"));
+ REMMINA_DEBUG("ssh_get_publickey_hash() has failed");
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ ssh_key_free(server_pubkey);
+ keyname = ssh_get_hexa(pubkey, len);
+
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 9, 0)
+ if (ret == SSH_KNOWN_HOSTS_UNKNOWN || ret == SSH_KNOWN_HOSTS_NOT_FOUND) {
+#else
+ if (ret == SSH_SERVER_NOT_KNOWN || ret == SSH_SERVER_FILE_NOT_FOUND) {
+#endif
+ message = g_strdup_printf("%s\n%s\n\n%s",
+ _("The server is unknown. The public key fingerprint is:"),
+ keyname,
+ _("Do you trust the new public key?"));
+ } else {
+ message = g_strdup_printf("%s\n%s\n\n%s",
+ _("Warning: The server has changed its public key. This means you are either under attack,\n"
+ "or the administrator has changed the key. The new public key fingerprint is:"),
+ keyname,
+ _("Do you trust the new public key?"));
+ }
+
+ ret = remmina_protocol_widget_panel_question_yesno(gp, message);
+ g_free(message);
+
+ ssh_string_free_char(keyname);
+ ssh_clean_pubkey_hash(&pubkey);
+ if (ret != GTK_RESPONSE_YES) return REMMINA_SSH_AUTH_USERCANCEL;
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 9, 0)
+ ssh_session_update_known_hosts(ssh->session);
+#else
+ ssh_write_knownhost(ssh->session);
+#endif
+ break;
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 9, 0)
+ case SSH_KNOWN_HOSTS_ERROR:
+#else
+ case SSH_SERVER_ERROR:
+#endif
+ default:
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(ssh, _("Could not check list of known SSH hosts. %s"));
+ REMMINA_DEBUG("Could not check list of known SSH hosts");
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+
+ enum { REMMINA_SSH_AUTH_PASSWORD, REMMINA_SSH_AUTH_PKPASSPHRASE, REMMINA_SSH_AUTH_KRBTOKEN, REMMINA_SSH_AUTH_KBDINTERACTIVE } remmina_ssh_auth_type;
+
+ switch (ssh->auth) {
+ case SSH_AUTH_PASSWORD:
+ keyname = _("SSH password");
+ pwdfkey = ssh->is_tunnel ? "ssh_tunnel_password" : "password";
+ remmina_ssh_auth_type = REMMINA_SSH_AUTH_PASSWORD;
+ break;
+ case SSH_AUTH_PUBLICKEY:
+ case SSH_AUTH_AGENT:
+ case SSH_AUTH_AUTO_PUBLICKEY:
+ keyname = _("Password for private SSH key");
+ pwdfkey = ssh->is_tunnel ? "ssh_tunnel_passphrase" : "ssh_passphrase";
+ remmina_ssh_auth_type = REMMINA_SSH_AUTH_PKPASSPHRASE;
+ break;
+ case SSH_AUTH_GSSAPI:
+ keyname = _("SSH Kerberos/GSSAPI");
+ pwdfkey = ssh->is_tunnel ? "ssh_tunnel_kerberos_token" : "ssh_kerberos_token";
+ remmina_ssh_auth_type = REMMINA_SSH_AUTH_KRBTOKEN;
+ break;
+ case SSH_AUTH_KBDINTERACTIVE:
+ instruction = _("Enter TOTP/OTP/2FA code");
+ remmina_ssh_auth_type = REMMINA_SSH_AUTH_KBDINTERACTIVE;
+ break;
+ default:
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+
+ disablepasswordstoring = remmina_file_get_int(remminafile, "disablepasswordstoring", FALSE);
+
+
+ current_pwd = g_strdup(remmina_file_get_string(remminafile, pwdfkey));
+
+ /* Try existing password/passphrase first */
+ ret = remmina_ssh_auth(ssh, current_pwd, gp, remminafile);
+ REMMINA_DEBUG("Returned %d at 1st attempt with the following message:", ret);
+ REMMINA_DEBUG("%s", ssh->error);
+
+ /* It seems that functions like ssh_userauth_password() can only be called 3 times
+ * on a ssh connection. And the 3rd failed attempt will block the calling thread forever.
+ * So we retry only 2 extra time authentication. */
+ for (attempt = 0;
+ attempt < 2 && (ret == REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT || ret == REMMINA_SSH_AUTH_AUTHFAILED_EMPTY_USERNAME);
+ attempt++) {
+ if (ssh->error)
+ REMMINA_DEBUG("Retrying auth because %s", ssh->error);
+
+ if (remmina_ssh_auth_type == REMMINA_SSH_AUTH_PKPASSPHRASE) {
+ // If username is empty, prompt user to enter it and attempt reconnect
+ if ( ret == REMMINA_SSH_AUTH_AUTHFAILED_EMPTY_USERNAME ) {
+ current_user = g_strdup(remmina_file_get_string(remminafile, ssh->is_tunnel ? "ssh_tunnel_username" : "username"));
+ ret = remmina_protocol_widget_panel_auth(gp,
+ (REMMINA_MESSAGE_PANEL_FLAG_USERNAME | REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD),
+ (ssh->is_tunnel ? _("SSH tunnel private key credentials") : _("SSH private key credentials")),
+ current_user,
+ remmina_file_get_string(remminafile, pwdfkey),
+ NULL,
+ _("Password for private SSH key"));
+
+ if (ret == GTK_RESPONSE_OK) {
+ // Save username to remmina file and reset ssh error for reconnect attempt
+ // If password is empty or changed, save the new password
+ remmina_file_set_string(remminafile, ssh->is_tunnel ? "ssh_tunnel_username" : "username", remmina_protocol_widget_get_username(gp));
+ ssh->user = remmina_protocol_widget_get_username(gp);
+
+ g_free(current_pwd);
+ current_pwd = remmina_protocol_widget_get_password(gp);
+ save_password = remmina_protocol_widget_get_savepassword(gp);
+ if (save_password) {
+ remmina_file_set_string(remminafile, pwdfkey, current_pwd);
+ }
+ else {
+ remmina_file_set_string(remminafile, pwdfkey, NULL);
+ }
+
+ ssh->passphrase = remmina_protocol_widget_get_password(gp);
+ ssh->error = NULL;
+ return REMMINA_SSH_AUTH_RECONNECT;
+ }
+ else {
+ return REMMINA_SSH_AUTH_USERCANCEL;
+ }
+ }
+
+ ret = remmina_protocol_widget_panel_auth(gp,
+ (disablepasswordstoring ? 0 :
+ REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD),
+ ssh->is_tunnel ? _("SSH tunnel credentials") : _("SSH credentials"),
+ NULL,
+ remmina_file_get_string(remminafile, pwdfkey),
+ NULL,
+ _("Password for private SSH key"));
+ if (ret == GTK_RESPONSE_OK) {
+ g_free(current_pwd);
+ current_pwd = remmina_protocol_widget_get_password(gp);
+ save_password = remmina_protocol_widget_get_savepassword(gp);
+ if (save_password)
+ remmina_file_set_string(remminafile, pwdfkey, current_pwd);
+ else
+ remmina_file_set_string(remminafile, pwdfkey, NULL);
+ } else {
+ g_free(current_pwd);
+ return REMMINA_SSH_AUTH_USERCANCEL;
+ }
+ } else if (remmina_ssh_auth_type == REMMINA_SSH_AUTH_PASSWORD) {
+ /* Ask for user credentials. Username cannot be changed here,
+ * because we already sent it when opening the connection */
+ REMMINA_DEBUG("Showing panel for password\n");
+ current_user = g_strdup(remmina_file_get_string(remminafile, ssh->is_tunnel ? "ssh_tunnel_username" : "username"));
+ ret = remmina_protocol_widget_panel_auth(gp,
+ (disablepasswordstoring ? 0 : REMMINA_MESSAGE_PANEL_FLAG_SAVEPASSWORD)
+ | REMMINA_MESSAGE_PANEL_FLAG_USERNAME
+ | (!ssh->is_tunnel ? 0 : REMMINA_MESSAGE_PANEL_FLAG_USERNAME_READONLY),
+ ssh->is_tunnel ? _("SSH tunnel credentials") : _("SSH credentials"),
+ current_user,
+ current_pwd,
+ NULL,
+ NULL);
+ if (ret == GTK_RESPONSE_OK) {
+ g_free(current_pwd);
+ current_pwd = remmina_protocol_widget_get_password(gp);
+ save_password = remmina_protocol_widget_get_savepassword(gp);
+ if (save_password)
+ remmina_file_set_string(remminafile, pwdfkey, current_pwd);
+ else
+ remmina_file_set_string(remminafile, pwdfkey, NULL);
+
+ if (!ssh->is_tunnel && !ssh->is_multiauth) {
+ g_free(current_user);
+ current_user = remmina_protocol_widget_get_username(gp);
+ remmina_file_set_string(remminafile, "username", current_user);
+ if (ssh->user != NULL) {
+ g_free(ssh->user);
+ ssh->user = NULL;
+ }
+ ssh->user = g_strdup(current_user);
+ if (ssh->password != NULL) {
+ g_free(ssh->password);
+ ssh->password = NULL;
+ }
+ ssh->password = g_strdup(current_pwd);
+ g_free(current_user);
+ return REMMINA_SSH_AUTH_RECONNECT;
+ }
+ g_free(current_user);
+ } else {
+ g_free(current_pwd);
+ g_free(current_user);
+ return REMMINA_SSH_AUTH_USERCANCEL;
+ }
+ } else if (remmina_ssh_auth_type == REMMINA_SSH_AUTH_KBDINTERACTIVE) {
+ REMMINA_DEBUG("Showing panel for keyboard interactive login\n");
+ /**
+ * gp
+ * flags
+ * title
+ * default_username
+ * default_password
+ * default_domain
+ * password_prompt
+ */
+ ret = remmina_protocol_widget_panel_auth(
+ gp,
+ 0,
+ _("Keyboard interactive login, TOTP/OTP/2FA"),
+ NULL,
+ NULL,
+ NULL,
+ instruction);
+ if (ret == GTK_RESPONSE_OK) {
+ g_free(current_pwd);
+ current_pwd = remmina_protocol_widget_get_password(gp);
+ REMMINA_DEBUG("OTP code is: %s", current_pwd);
+ ssh->password = g_strdup(current_pwd);
+ } else {
+ g_free(current_pwd);
+ return REMMINA_SSH_AUTH_USERCANCEL;
+ }
+ } else {
+ g_print("Unimplemented.");
+ g_free(current_pwd);
+ return REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+ REMMINA_DEBUG("Retrying authentication");
+ ret = remmina_ssh_auth(ssh, current_pwd, gp, remminafile);
+ REMMINA_DEBUG("Authentication attempt n° %d returned %d with the following message:", attempt + 2, ret);
+ REMMINA_DEBUG("%s", ssh->error);
+ }
+
+ g_free(current_pwd); current_pwd = NULL;
+
+ /* After attempting the max number of times, REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT
+ * becomes REMMINA_SSH_AUTH_FATAL_ERROR */
+ if (ret == REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT || ret == REMMINA_SSH_AUTH_AGAIN) {
+ REMMINA_DEBUG("SSH Authentication failed");
+ ret = REMMINA_SSH_AUTH_FATAL_ERROR;
+ }
+
+ return ret;
+}
+
+void
+remmina_ssh_log_callback(ssh_session session, int priority, const char *message, void *userdata)
+{
+ TRACE_CALL(__func__);
+ REMMINA_DEBUG(message);
+}
+
+gboolean
+remmina_ssh_init_session(RemminaSSH *ssh)
+{
+ TRACE_CALL(__func__);
+ gint verbosity;
+ gint rc;
+ gchar *parsed_config;
+#ifdef HAVE_NETINET_TCP_H
+ socket_t sshsock;
+ gint optval;
+#endif
+ // Handle IPv4 / IPv6 dual stack
+ char *hostname;
+ struct addrinfo hints,*aitop,*ai;
+ char ipstr[INET6_ADDRSTRLEN];
+ void *addr4=NULL;
+ void *addr6=NULL;
+
+ ssh->callback = g_new0(struct ssh_callbacks_struct, 1);
+
+ /* Init & startup the SSH session */
+ REMMINA_DEBUG("server=%s port=%d is_tunnel=%s tunnel_entrance_host=%s tunnel_entrance_port=%d",
+ ssh->server,
+ ssh->port,
+ ssh->is_tunnel ? "Yes" : "No",
+ ssh->tunnel_entrance_host, ssh->tunnel_entrance_port);
+
+ ssh->session = ssh_new();
+
+ /* Tunnel sanity checks */
+ if (ssh->is_tunnel && ssh->tunnel_entrance_host != NULL) {
+ ssh->error = g_strdup_printf("Internal error in %s: is_tunnel and tunnel_entrance != NULL", __func__);
+ REMMINA_DEBUG(ssh->error);
+ return FALSE;
+ }
+ if (!ssh->is_tunnel && ssh->tunnel_entrance_host == NULL) {
+ ssh->error = g_strdup_printf("Internal error in %s: is_tunnel == false and tunnel_entrance == NULL", __func__);
+ REMMINA_DEBUG(ssh->error);
+ return FALSE;
+ }
+
+ /* Set connection host/port */
+ if (ssh->is_tunnel) {
+ ssh_options_set(ssh->session, SSH_OPTIONS_HOST, ssh->server);
+ ssh_options_set(ssh->session, SSH_OPTIONS_PORT, &ssh->port);
+ REMMINA_DEBUG("Setting SSH_OPTIONS_HOST to %s and SSH_OPTIONS_PORT to %d", ssh->server, ssh->port);
+ } else {
+ ssh_options_set(ssh->session, SSH_OPTIONS_HOST, ssh->tunnel_entrance_host);
+ ssh_options_set(ssh->session, SSH_OPTIONS_PORT, &ssh->tunnel_entrance_port);
+ REMMINA_DEBUG("Setting SSH_OPTIONS_HOST to %s and SSH_OPTIONS_PORT to %d", ssh->tunnel_entrance_host, ssh->tunnel_entrance_port);
+ }
+
+ if (ssh->privkeyfile && *ssh->privkeyfile != 0) {
+ rc = ssh_options_set(ssh->session, SSH_OPTIONS_IDENTITY, ssh->privkeyfile);
+ if (rc == 0)
+ REMMINA_DEBUG("SSH_OPTIONS_IDENTITY is now %s", ssh->privkeyfile);
+ else
+ REMMINA_DEBUG("SSH_OPTIONS_IDENTITY is not set, by default the files “identity”, “id_dsa” and “id_rsa” are used.");
+ }
+
+#ifdef SNAP_BUILD
+ ssh_options_set(ssh->session, SSH_OPTIONS_SSH_DIR, g_strdup_printf("%s/.ssh", g_getenv("SNAP_USER_COMMON")));
+#endif
+ ssh_callbacks_init(ssh->callback);
+ if (remmina_log_running()) {
+ verbosity = remmina_pref.ssh_loglevel;
+ ssh_options_set(ssh->session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
+ ssh->callback->log_function = remmina_ssh_log_callback;
+ /* Reset libssh legacy userdata. This is a workaround for a libssh bug */
+ ssh_set_log_userdata(ssh->session);
+ }
+ ssh->callback->userdata = ssh;
+ ssh_set_callbacks(ssh->session, ssh->callback);
+
+ /* As the latest parse the ~/.ssh/config file */
+ if (g_strcmp0(ssh->tunnel_entrance_host, "127.0.0.1") == 0) {
+ REMMINA_DEBUG("SSH_OPTIONS_HOST temporary set to the destination host as ssh->tunnel_entrance_host is 127.0.0.1,");
+ ssh_options_set(ssh->session, SSH_OPTIONS_HOST, ssh->server);
+ }
+ if (remmina_pref.ssh_parseconfig) {
+ if (ssh_options_parse_config(ssh->session, NULL) == 0)
+ REMMINA_DEBUG("ssh_config have been correctly parsed");
+ else
+ REMMINA_DEBUG("Cannot parse ssh_config: %s", ssh_get_error(ssh->session));
+ }
+ if (g_strcmp0(ssh->tunnel_entrance_host, "127.0.0.1") == 0) {
+ REMMINA_DEBUG("Setting SSH_OPTIONS_HOST to ssh->tunnel_entrance_host is 127.0.0.1,");
+ ssh_options_set(ssh->session, SSH_OPTIONS_HOST, ssh->tunnel_entrance_host);
+ }
+ if (!ssh->user || *ssh->user == 0) {
+ rc = ssh_options_get(ssh->session, SSH_OPTIONS_USER, &parsed_config);
+ if (rc == SSH_OK) {
+ if (ssh->user)
+ g_free(ssh->user);
+ ssh->user = g_strdup(parsed_config);
+ ssh_string_free_char(parsed_config);
+ } else {
+ REMMINA_DEBUG("Parsing ssh_config for SSH_OPTIONS_USER returned an error: %s", ssh_get_error(ssh->session));
+ }
+ }
+ ssh_options_set(ssh->session, SSH_OPTIONS_USER, ssh->user);
+ REMMINA_DEBUG("SSH_OPTIONS_USER is now %s", ssh->user);
+
+ /* SSH_OPTIONS_PROXYCOMMAND */
+ rc = ssh_options_get(ssh->session, SSH_OPTIONS_PROXYCOMMAND, &parsed_config);
+ if (rc == SSH_OK) {
+ ssh->proxycommand = g_strdup(parsed_config);
+ ssh_string_free_char(parsed_config);
+ } else {
+ REMMINA_DEBUG("Parsing ssh_config for SSH_OPTIONS_PROXYCOMMAND returned an error: %s", ssh_get_error(ssh->session));
+ }
+ rc = ssh_options_set(ssh->session, SSH_OPTIONS_PROXYCOMMAND, ssh->proxycommand);
+ if (rc == 0)
+ REMMINA_DEBUG("SSH_OPTIONS_PROXYCOMMAND is now %s", ssh->proxycommand);
+ else
+ REMMINA_DEBUG("SSH_OPTIONS_PROXYCOMMAND does not have a valid value. %s", ssh->proxycommand);
+
+ /* SSH_OPTIONS_HOSTKEYS */
+ rc = ssh_options_get(ssh->session, SSH_OPTIONS_HOSTKEYS, &parsed_config);
+ if (rc == SSH_OK) {
+ ssh->hostkeytypes = g_strdup(parsed_config);
+ ssh_string_free_char(parsed_config);
+ } else {
+ REMMINA_DEBUG("Parsing ssh_config for SSH_OPTIONS_HOSTKEYS returned an error: %s", ssh_get_error(ssh->session));
+ }
+ rc = ssh_options_set(ssh->session, SSH_OPTIONS_HOSTKEYS, ssh->hostkeytypes);
+ if (rc == 0)
+ REMMINA_DEBUG("SSH_OPTIONS_HOSTKEYS is now %s", ssh->hostkeytypes);
+ else
+ REMMINA_DEBUG("SSH_OPTIONS_HOSTKEYS does not have a valid value. %s", ssh->hostkeytypes);
+
+ /* SSH_OPTIONS_KEY_EXCHANGE */
+ rc = ssh_options_get(ssh->session, SSH_OPTIONS_KEY_EXCHANGE, &parsed_config);
+ if (rc == SSH_OK) {
+ ssh->kex_algorithms = g_strdup(parsed_config);
+ ssh_string_free_char(parsed_config);
+ } else {
+ REMMINA_DEBUG("Parsing ssh_config for SSH_OPTIONS_KEY_EXCHANGE returned an error: %s", ssh_get_error(ssh->session));
+ }
+ rc = ssh_options_set(ssh->session, SSH_OPTIONS_KEY_EXCHANGE, ssh->kex_algorithms);
+ if (rc == 0)
+ REMMINA_DEBUG("SSH_OPTIONS_KEY_EXCHANGE is now %s", ssh->kex_algorithms);
+ else
+ REMMINA_DEBUG("SSH_OPTIONS_KEY_EXCHANGE does not have a valid value. %s", ssh->kex_algorithms);
+
+ /* SSH_OPTIONS_CIPHERS_C_S */
+ rc = ssh_options_get(ssh->session, SSH_OPTIONS_CIPHERS_C_S, &parsed_config);
+ if (rc == SSH_OK) {
+ ssh->ciphers = g_strdup(parsed_config);
+ ssh_string_free_char(parsed_config);
+ } else {
+ REMMINA_DEBUG("Parsing ssh_config for SSH_OPTIONS_CIPHERS_C_S returned an error: %s", ssh_get_error(ssh->session));
+ }
+ rc = ssh_options_set(ssh->session, SSH_OPTIONS_CIPHERS_C_S, ssh->ciphers);
+ if (rc == 0)
+ REMMINA_DEBUG("SSH_OPTIONS_CIPHERS_C_S has been set to %s", ssh->ciphers);
+ else
+ REMMINA_DEBUG("SSH_OPTIONS_CIPHERS_C_S does not have a valid value. %s", ssh->ciphers);
+ /* SSH_OPTIONS_STRICTHOSTKEYCHECK */
+ rc = ssh_options_get(ssh->session, SSH_OPTIONS_STRICTHOSTKEYCHECK, &parsed_config);
+ if (rc == SSH_OK) {
+ ssh->stricthostkeycheck = atoi(parsed_config);
+ ssh_string_free_char(parsed_config);
+ } else {
+ REMMINA_DEBUG("Parsing ssh_config for SSH_OPTIONS_STRICTHOSTKEYCHECK returned an error: %s", ssh_get_error(ssh->session));
+ }
+ rc = ssh_options_set(ssh->session, SSH_OPTIONS_STRICTHOSTKEYCHECK, &ssh->stricthostkeycheck);
+ if (rc == 0)
+ REMMINA_DEBUG("SSH_OPTIONS_STRICTHOSTKEYCHECK is now %d", ssh->stricthostkeycheck);
+ else
+ REMMINA_DEBUG("SSH_OPTIONS_STRICTHOSTKEYCHECK does not have a valid value. %d", ssh->stricthostkeycheck);
+ /* SSH_OPTIONS_COMPRESSION */
+ rc = ssh_options_get(ssh->session, SSH_OPTIONS_COMPRESSION, &parsed_config);
+ if (rc == SSH_OK) {
+ ssh->compression = g_strdup(parsed_config);
+ ssh_string_free_char(parsed_config);
+ } else {
+ REMMINA_DEBUG("Parsing ssh_config for SSH_OPTIONS_COMPRESSION returned an error: %s", ssh_get_error(ssh->session));
+ }
+ rc = ssh_options_set(ssh->session, SSH_OPTIONS_COMPRESSION, ssh->compression);
+ if (rc == 0)
+ REMMINA_DEBUG("SSH_OPTIONS_COMPRESSION is now %s", ssh->compression);
+ else
+ REMMINA_DEBUG("SSH_OPTIONS_COMPRESSION does not have a valid value. %s", ssh->compression);
+
+ // Handle the dual IPv4 / IPv6 stack
+ // Prioritize IPv6 and fallback to IPv4
+ if (ssh_connect(ssh->session)) {
+ unsigned short int success = 0;
+
+ // Run the DNS resolution
+ // First retrieve host from the ssh->session structure
+ ssh_options_get(ssh->session, SSH_OPTIONS_HOST, &hostname);
+ // Call getaddrinfo
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_INET6;
+ hints.ai_socktype = SOCK_STREAM;
+ if ((getaddrinfo(hostname, NULL, &hints, &aitop)) != 0) {
+ ssh->error = g_strdup_printf("Could not resolve hostname %s to IPv6", hostname);
+ REMMINA_DEBUG(ssh->error);
+ }
+ else {
+ // We have one or more IPV6 addesses now, extract them
+ ai = aitop;
+ while (ai != NULL) {
+ struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)ai->ai_addr;
+ addr6 = &(ipv6->sin6_addr);
+ inet_ntop(AF_INET6, addr6, ipstr, sizeof ipstr);
+ ssh_options_set(ssh->session, SSH_OPTIONS_HOST, ipstr);
+ REMMINA_DEBUG("Setting SSH_OPTIONS_HOST to IPv6 %s", ipstr);
+ if (ssh_connect(ssh->session)) {
+ ssh_disconnect(ssh->session);
+ REMMINA_DEBUG("IPv6 session failed");
+ } else {
+ success = 1;
+ REMMINA_DEBUG("IPv6 session success !");
+ break;
+ }
+ ai = ai->ai_next;
+ }
+ freeaddrinfo(aitop);
+ }
+ if (success == 0) {
+ // Fallback to IPv4
+ // Call getaddrinfo
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_INET;
+ hints.ai_socktype = SOCK_STREAM;
+ if ((getaddrinfo(hostname, NULL, &hints, &aitop)) != 0) {
+ ssh->error = g_strdup_printf("Could not resolve hostname %s to IPv4", hostname);
+ REMMINA_DEBUG(ssh->error);
+ return FALSE;
+ }
+ else {
+ // We have one or more IPV4 addesses now, extract them
+ ai = aitop;
+ while (ai != NULL) {
+ struct sockaddr_in *ipv4 = (struct sockaddr_in *)ai->ai_addr;
+ addr4 = &(ipv4->sin_addr);
+ inet_ntop(AF_INET, addr4, ipstr, sizeof ipstr);
+ ssh_options_set(ssh->session, SSH_OPTIONS_HOST, ipstr);
+ REMMINA_DEBUG("Setting SSH_OPTIONS_HOST to IPv4 %s", ipstr);
+ if (ssh_connect(ssh->session)) {
+ ssh_disconnect(ssh->session);
+ REMMINA_DEBUG("IPv4 session failed");
+ } else {
+ success = 1;
+ REMMINA_DEBUG("IPv4 session success !");
+ break;
+ }
+ ai = ai->ai_next;
+ }
+ freeaddrinfo(aitop);
+ }
+ }
+ if (success == 0){
+ return FALSE;
+ }
+ }
+
+ #ifdef HAVE_NETINET_TCP_H
+
+ /* Set keepalive on SSH socket, so we can keep firewalls awaken and detect
+ * when we loss the tunnel */
+ sshsock = ssh_get_fd(ssh->session);
+ if (sshsock >= 0) {
+ optval = 1;
+ if (setsockopt(sshsock, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)) < 0)
+ REMMINA_DEBUG("TCP KeepAlive not set");
+ else
+ REMMINA_DEBUG("TCP KeepAlive enabled");
+
+#ifdef TCP_KEEPIDLE
+ optval = remmina_pref.ssh_tcp_keepidle;
+ if (setsockopt(sshsock, IPPROTO_TCP, TCP_KEEPIDLE, &optval, sizeof(optval)) < 0)
+ REMMINA_DEBUG("TCP_KEEPIDLE not set");
+ else
+ REMMINA_DEBUG("TCP_KEEPIDLE set to %i", optval);
+
+#endif
+#ifdef TCP_KEEPCNT
+ optval = remmina_pref.ssh_tcp_keepcnt;
+ if (setsockopt(sshsock, IPPROTO_TCP, TCP_KEEPCNT, &optval, sizeof(optval)) < 0)
+ REMMINA_DEBUG("TCP_KEEPCNT not set");
+ else
+ REMMINA_DEBUG("TCP_KEEPCNT set to %i", optval);
+
+#endif
+#ifdef TCP_KEEPINTVL
+ optval = remmina_pref.ssh_tcp_keepintvl;
+ if (setsockopt(sshsock, IPPROTO_TCP, TCP_KEEPINTVL, &optval, sizeof(optval)) < 0)
+ REMMINA_DEBUG("TCP_KEEPINTVL not set");
+ else
+ REMMINA_DEBUG("TCP_KEEPINTVL set to %i", optval);
+
+#endif
+#ifdef TCP_USER_TIMEOUT
+ optval = remmina_pref.ssh_tcp_usrtimeout;
+ if (setsockopt(sshsock, IPPROTO_TCP, TCP_USER_TIMEOUT, &optval, sizeof(optval)) < 0)
+ REMMINA_DEBUG("TCP_USER_TIMEOUT not set");
+ else
+ REMMINA_DEBUG("TCP_USER_TIMEOUT set to %i", optval);
+
+#endif
+ }
+#endif
+
+ /* Try the "none" authentication */
+ if (ssh_userauth_none(ssh->session, NULL) == SSH_AUTH_SUCCESS)
+ ssh->authenticated = TRUE;
+ return TRUE;
+}
+
+gboolean
+remmina_ssh_init_from_file(RemminaSSH *ssh, RemminaFile *remminafile, gboolean is_tunnel)
+{
+ TRACE_CALL(__func__);
+ const gchar *username;
+ const gchar *privatekey;
+ const gchar *server;
+ gchar *s;
+
+ ssh->session = NULL;
+ ssh->callback = NULL;
+ ssh->authenticated = FALSE;
+ ssh->error = NULL;
+ ssh->passphrase = NULL;
+ ssh->is_tunnel = is_tunnel;
+ pthread_mutex_init(&ssh->ssh_mutex, NULL);
+
+ ssh->tunnel_entrance_host = NULL;
+ ssh->tunnel_entrance_port = 0;
+
+ username = remmina_file_get_string(remminafile, is_tunnel ? "ssh_tunnel_username" : "username");
+ privatekey = remmina_file_get_string(remminafile, is_tunnel ? "ssh_tunnel_privatekey" : "ssh_privatekey");
+ ssh->certfile = g_strdup(remmina_file_get_string(remminafile, is_tunnel ? "ssh_tunnel_certfile" : "ssh_certfile"));
+
+ /* The ssh->server and ssh->port values */
+ if (is_tunnel) {
+ REMMINA_DEBUG("We are initializing an SSH tunnel session");
+ server = remmina_file_get_string(remminafile, "ssh_tunnel_server");
+ if (server == NULL || server[0] == 0) {
+ // ssh_tunnel_server empty or invalid, we are opening a tunnel, it means that "Same server at port 22" has been selected
+ server = remmina_file_get_string(remminafile, "server");
+ if (server == NULL || server[0] == 0)
+ server = "localhost";
+ REMMINA_DEBUG("Calling remmina_public_get_server_port");
+ remmina_public_get_server_port(server, 22, &ssh->server, &ssh->port);
+ ssh->port = 22;
+ } else {
+ REMMINA_DEBUG("Calling remmina_public_get_server_port");
+ remmina_public_get_server_port(server, 22, &ssh->server, &ssh->port);
+ }
+ REMMINA_DEBUG("server:port = %s, server = %s, port = %d", server, ssh->server, ssh->port);
+ } else {
+ REMMINA_DEBUG("We are initializing an SSH session");
+ server = remmina_file_get_string(remminafile, "server");
+ if (server == NULL || server[0] == 0)
+ server = "localhost";
+ REMMINA_DEBUG("Calling remmina_public_get_server_port");
+ remmina_public_get_server_port(server, 22, &ssh->server, &ssh->port);
+ REMMINA_DEBUG("server:port = %s, server = %s, port = %d", server, ssh->server, ssh->port);
+ }
+
+ if (ssh->server[0] == '\0') {
+ g_free(ssh->server);
+ // ???
+ REMMINA_DEBUG("Calling remmina_public_get_server_port");
+ remmina_public_get_server_port(server, 0, &ssh->server, NULL);
+ }
+
+ REMMINA_DEBUG("Initialized SSH struct from file with ssh->server = %s and SSH->port = %d", ssh->server, ssh->port);
+
+ ssh->user = g_strdup(username ? username : NULL);
+ ssh->password = NULL;
+ ssh->auth = remmina_file_get_int(remminafile, is_tunnel ? "ssh_tunnel_auth" : "ssh_auth", 0);
+ ssh->charset = g_strdup(remmina_file_get_string(remminafile, "ssh_charset"));
+ ssh->kex_algorithms = g_strdup(remmina_file_get_string(remminafile, is_tunnel ? "ssh_tunnel_kex_algorithms" : "ssh_kex_algorithms"));
+ ssh->ciphers = g_strdup(remmina_file_get_string(remminafile, is_tunnel ? "ssh_tunnel_ciphers" : "ssh_ciphers"));
+ ssh->hostkeytypes = g_strdup(remmina_file_get_string(remminafile, is_tunnel ? "ssh_tunnel_hostkeytypes" : "ssh_hostkeytypes"));
+ ssh->proxycommand = g_strdup(remmina_file_get_string(remminafile, is_tunnel ? "ssh_tunnel_proxycommand" : "ssh_proxycommand"));
+ ssh->stricthostkeycheck = remmina_file_get_int(remminafile, is_tunnel ? "ssh_tunnel_stricthostkeycheck" : "ssh_stricthostkeycheck", 0);
+ gint c = remmina_file_get_int(remminafile, is_tunnel ? "ssh_tunnel_compression" : "ssh_compression", 0);
+ ssh->compression = (c == 1) ? "yes" : "no";
+
+ REMMINA_DEBUG("ssh->user: %s", ssh->user);
+ REMMINA_DEBUG("ssh->password: %s", ssh->password);
+ REMMINA_DEBUG("ssh->auth: %d", ssh->auth);
+ REMMINA_DEBUG("ssh->charset: %s", ssh->charset);
+ REMMINA_DEBUG("ssh->kex_algorithms: %s", ssh->kex_algorithms);
+ REMMINA_DEBUG("ssh->ciphers: %s", ssh->ciphers);
+ REMMINA_DEBUG("ssh->hostkeytypes: %s", ssh->hostkeytypes);
+ REMMINA_DEBUG("ssh->proxycommand: %s", ssh->proxycommand);
+ REMMINA_DEBUG("ssh->stricthostkeycheck: %d", ssh->stricthostkeycheck);
+ REMMINA_DEBUG("ssh->compression: %s", ssh->compression);
+
+ /* Public/Private keys */
+ s = (privatekey ? g_strdup(privatekey) : remmina_ssh_find_identity());
+ if (s) {
+ ssh->privkeyfile = remmina_ssh_identity_path(s);
+ REMMINA_DEBUG("ssh->privkeyfile: %s", ssh->privkeyfile);
+ g_free(s);
+ } else {
+ ssh->privkeyfile = NULL;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+remmina_ssh_init_from_ssh(RemminaSSH *ssh, const RemminaSSH *ssh_src)
+{
+ TRACE_CALL(__func__);
+ ssh->session = NULL;
+ ssh->authenticated = FALSE;
+ ssh->error = NULL;
+ pthread_mutex_init(&ssh->ssh_mutex, NULL);
+
+ ssh->is_tunnel = ssh_src->is_tunnel;
+ ssh->server = g_strdup(ssh_src->server);
+ ssh->port = ssh_src->port;
+ ssh->user = g_strdup(ssh_src->user ? ssh_src->user : NULL);
+ ssh->auth = ssh_src->auth;
+ ssh->password = g_strdup(ssh_src->password);
+ ssh->passphrase = g_strdup(ssh_src->passphrase);
+ ssh->privkeyfile = g_strdup(ssh_src->privkeyfile);
+ ssh->certfile = g_strdup(ssh_src->certfile);
+ ssh->charset = g_strdup(ssh_src->charset);
+ ssh->proxycommand = g_strdup(ssh_src->proxycommand);
+ ssh->kex_algorithms = g_strdup(ssh_src->kex_algorithms);
+ ssh->ciphers = g_strdup(ssh_src->ciphers);
+ ssh->hostkeytypes = g_strdup(ssh_src->hostkeytypes);
+ ssh->stricthostkeycheck = ssh_src->stricthostkeycheck;
+ ssh->compression = ssh_src->compression;
+ ssh->tunnel_entrance_host = g_strdup(ssh_src->tunnel_entrance_host);
+ ssh->tunnel_entrance_port = ssh_src->tunnel_entrance_port;
+
+ return TRUE;
+}
+
+gchar *
+remmina_ssh_convert(RemminaSSH *ssh, const gchar *from)
+{
+ TRACE_CALL(__func__);
+ gchar *to = NULL;
+
+ if (ssh->charset && from)
+ to = g_convert(from, -1, "UTF-8", ssh->charset, NULL, NULL, NULL);
+ if (!to) to = g_strdup(from);
+ return to;
+}
+
+gchar *
+remmina_ssh_unconvert(RemminaSSH *ssh, const gchar *from)
+{
+ TRACE_CALL(__func__);
+ gchar *to = NULL;
+
+ if (ssh->charset && from)
+ to = g_convert(from, -1, ssh->charset, "UTF-8", NULL, NULL, NULL);
+ if (!to) to = g_strdup(from);
+ return to;
+}
+
+void
+remmina_ssh_free(RemminaSSH *ssh)
+{
+ TRACE_CALL(__func__);
+ if (ssh->session) {
+ REMMINA_DEBUG("Disconnecting SSH session");
+ ssh_disconnect(ssh->session);
+ ssh_free(ssh->session);
+ ssh->session = NULL;
+ }
+ g_free(ssh->callback);
+ g_free(ssh->server);
+ g_free(ssh->user);
+ g_free(ssh->password);
+ g_free(ssh->privkeyfile);
+ g_free(ssh->certfile);
+ g_free(ssh->charset);
+ g_free(ssh->error);
+ pthread_mutex_destroy(&ssh->ssh_mutex);
+ g_free(ssh);
+}
+
+/*-----------------------------------------------------------------------------*
+* SSH Tunnel *
+*-----------------------------------------------------------------------------*/
+struct _RemminaSSHTunnelBuffer {
+ gchar * data;
+ gchar * ptr;
+ ssize_t len;
+};
+
+static RemminaSSHTunnelBuffer *
+remmina_ssh_tunnel_buffer_new(ssize_t len)
+{
+ TRACE_CALL(__func__);
+ RemminaSSHTunnelBuffer *buffer;
+
+ buffer = g_new(RemminaSSHTunnelBuffer, 1);
+ buffer->data = (gchar *)g_malloc(len);
+ buffer->ptr = buffer->data;
+ buffer->len = len;
+ return buffer;
+}
+
+static void
+remmina_ssh_tunnel_buffer_free(RemminaSSHTunnelBuffer *buffer)
+{
+ TRACE_CALL(__func__);
+ if (buffer) {
+ g_free(buffer->data);
+ g_free(buffer);
+ }
+}
+
+RemminaSSHTunnel *
+remmina_ssh_tunnel_new_from_file(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ RemminaSSHTunnel *tunnel;
+
+ tunnel = g_new(RemminaSSHTunnel, 1);
+
+ remmina_ssh_init_from_file(REMMINA_SSH(tunnel), remminafile, TRUE);
+
+ tunnel->tunnel_type = -1;
+ tunnel->channels = NULL;
+ tunnel->sockets = NULL;
+ tunnel->socketbuffers = NULL;
+ tunnel->num_channels = 0;
+ tunnel->max_channels = 0;
+ tunnel->thread = 0;
+ tunnel->running = FALSE;
+ tunnel->server_sock = -1;
+ tunnel->dest = NULL;
+ tunnel->port = 0;
+ tunnel->buffer = NULL;
+ tunnel->buffer_len = 0;
+ tunnel->channels_out = NULL;
+ tunnel->remotedisplay = 0;
+ tunnel->localdisplay = NULL;
+ tunnel->init_func = NULL;
+ tunnel->connect_func = NULL;
+ tunnel->disconnect_func = NULL;
+ tunnel->callback_data = NULL;
+
+ return tunnel;
+}
+
+static void
+remmina_ssh_tunnel_close_all_channels(RemminaSSHTunnel *tunnel)
+{
+ TRACE_CALL(__func__);
+ int i;
+
+ for (i = 0; i < tunnel->num_channels; i++) {
+ close(tunnel->sockets[i]);
+ remmina_ssh_tunnel_buffer_free(tunnel->socketbuffers[i]);
+ ssh_channel_close(tunnel->channels[i]);
+ ssh_channel_send_eof(tunnel->channels[i]);
+ ssh_channel_free(tunnel->channels[i]);
+ }
+
+ g_free(tunnel->channels);
+ tunnel->channels = NULL;
+ g_free(tunnel->sockets);
+ tunnel->sockets = NULL;
+ g_free(tunnel->socketbuffers);
+ tunnel->socketbuffers = NULL;
+
+ tunnel->num_channels = 0;
+ tunnel->max_channels = 0;
+}
+
+static void
+remmina_ssh_tunnel_remove_channel(RemminaSSHTunnel *tunnel, gint n)
+{
+ TRACE_CALL(__func__);
+ ssh_channel_close(tunnel->channels[n]);
+ ssh_channel_send_eof(tunnel->channels[n]);
+ ssh_channel_free(tunnel->channels[n]);
+ close(tunnel->sockets[n]);
+ remmina_ssh_tunnel_buffer_free(tunnel->socketbuffers[n]);
+ tunnel->num_channels--;
+ tunnel->channels[n] = tunnel->channels[tunnel->num_channels];
+ tunnel->channels[tunnel->num_channels] = NULL;
+ tunnel->sockets[n] = tunnel->sockets[tunnel->num_channels];
+ tunnel->socketbuffers[n] = tunnel->socketbuffers[tunnel->num_channels];
+}
+
+/* Register the new channel/socket pair */
+static void
+remmina_ssh_tunnel_add_channel(RemminaSSHTunnel *tunnel, ssh_channel channel, gint sock)
+{
+ TRACE_CALL(__func__);
+ gint flags;
+ gint i;
+
+ i = tunnel->num_channels++;
+ if (tunnel->num_channels > tunnel->max_channels) {
+ /* Allocate an extra NULL pointer in channels for ssh_select */
+ tunnel->channels = (ssh_channel *)g_realloc(tunnel->channels,
+ sizeof(ssh_channel) * (tunnel->num_channels + 1));
+ tunnel->sockets = (gint *)g_realloc(tunnel->sockets,
+ sizeof(gint) * tunnel->num_channels);
+ tunnel->socketbuffers = (RemminaSSHTunnelBuffer **)g_realloc(tunnel->socketbuffers,
+ sizeof(RemminaSSHTunnelBuffer *) * tunnel->num_channels);
+ tunnel->max_channels = tunnel->num_channels;
+
+ tunnel->channels_out = (ssh_channel *)g_realloc(tunnel->channels_out,
+ sizeof(ssh_channel) * (tunnel->num_channels + 1));
+ }
+ tunnel->channels[i] = channel;
+ tunnel->channels[i + 1] = NULL;
+ tunnel->sockets[i] = sock;
+ tunnel->socketbuffers[i] = NULL;
+
+ flags = fcntl(sock, F_GETFL, 0);
+ fcntl(sock, F_SETFL, flags | O_NONBLOCK);
+}
+
+static int
+remmina_ssh_tunnel_accept_local_connection(RemminaSSHTunnel *tunnel, gboolean blocking)
+{
+ gint sock, sock_flags;
+
+ sock_flags = fcntl(tunnel->server_sock, F_GETFL, 0);
+ if (blocking)
+ sock_flags &= ~O_NONBLOCK;
+ else
+ sock_flags |= O_NONBLOCK;
+ fcntl(tunnel->server_sock, F_SETFL, sock_flags);
+
+ /* Accept a local connection */
+ sock = accept(tunnel->server_sock, NULL, NULL);
+ if (sock < 0) {
+ if (blocking) {
+ g_free(REMMINA_SSH(tunnel)->error);
+ REMMINA_SSH(tunnel)->error = g_strdup("Local socket not accepted");
+ }
+ }
+
+ return sock;
+}
+
+static ssh_channel
+remmina_ssh_tunnel_create_forward_channel(RemminaSSHTunnel *tunnel)
+{
+ ssh_channel channel = NULL;
+
+ channel = ssh_channel_new(tunnel->ssh.session);
+ if (!channel) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not create channel. %s"));
+ return NULL;
+ }
+
+ /* Request the SSH server to connect to the destination */
+ REMMINA_DEBUG("SSH tunnel destination is %s", tunnel->dest);
+ if (ssh_channel_open_forward(channel, tunnel->dest, tunnel->port, "127.0.0.1", 0) != SSH_OK) {
+ ssh_channel_close(channel);
+ ssh_channel_send_eof(channel);
+ ssh_channel_free(channel);
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not connect to SSH tunnel. %s"));
+ return NULL;
+ }
+
+ return channel;
+}
+
+static gpointer
+remmina_ssh_tunnel_main_thread_proc(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaSSHTunnel *tunnel = (RemminaSSHTunnel *)data;
+ gchar *ptr;
+ ssize_t len = 0, lenw = 0;
+ fd_set set;
+ struct timeval timeout;
+ g_autoptr(GDateTime) t1 = NULL;
+ g_autoptr(GDateTime) t2 = NULL;
+ GTimeSpan diff; // microseconds
+ ssh_channel channel = NULL;
+ gboolean first = TRUE;
+ gboolean disconnected;
+ gint sock;
+ gint maxfd;
+ gint i;
+ gint ret;
+ struct sockaddr_in sin;
+
+ t1 = g_date_time_new_now_local();
+ t2 = g_date_time_new_now_local();
+
+ switch (tunnel->tunnel_type) {
+ case REMMINA_SSH_TUNNEL_OPEN:
+ sock = remmina_ssh_tunnel_accept_local_connection(tunnel, TRUE);
+ if (sock < 0) {
+ if (tunnel)
+ tunnel->thread = 0;
+ return NULL;
+ }
+
+ channel = remmina_ssh_tunnel_create_forward_channel(tunnel);
+ if (!tunnel) {
+ close(sock);
+ tunnel->thread = 0;
+ return NULL;
+ }
+
+ remmina_ssh_tunnel_add_channel(tunnel, channel, sock);
+ break;
+
+ case REMMINA_SSH_TUNNEL_XPORT:
+ /* Detect the next available port starting from 6010 on the server */
+ for (i = 10; i <= MAX_X_DISPLAY_NUMBER; i++) {
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 7, 0)
+ if (ssh_channel_listen_forward(REMMINA_SSH(tunnel)->session, (tunnel->bindlocalhost ? "localhost" : NULL), 6000 + i, NULL)) {
+ continue;
+ } else {
+ tunnel->remotedisplay = i;
+ break;
+ }
+#else
+ if (ssh_forward_listen(REMMINA_SSH(tunnel)->session, (tunnel->bindlocalhost ? "localhost" : NULL), 6000 + i, NULL)) {
+ continue;
+ } else {
+ tunnel->remotedisplay = i;
+ break;
+ }
+#endif
+ }
+ if (tunnel->remotedisplay < 1) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not request port forwarding. %s"));
+ if (tunnel->disconnect_func)
+ (*tunnel->disconnect_func)(tunnel, tunnel->callback_data);
+ tunnel->thread = 0;
+ return NULL;
+ }
+
+ if (tunnel->init_func &&
+ !(*tunnel->init_func)(tunnel, tunnel->callback_data)) {
+ if (tunnel->disconnect_func)
+ (*tunnel->disconnect_func)(tunnel, tunnel->callback_data);
+ tunnel->thread = 0;
+ return NULL;
+ }
+
+ break;
+
+ case REMMINA_SSH_TUNNEL_REVERSE:
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 7, 0)
+ if (ssh_channel_listen_forward(REMMINA_SSH(tunnel)->session, NULL, tunnel->port, NULL)) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not request port forwarding. %s"));
+ if (tunnel->disconnect_func)
+ (*tunnel->disconnect_func)(tunnel, tunnel->callback_data);
+ tunnel->thread = 0;
+ return NULL;
+ }
+#else
+ if (ssh_forward_listen(REMMINA_SSH(tunnel)->session, NULL, tunnel->port, NULL)) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not request port forwarding. %s"));
+ if (tunnel->disconnect_func)
+ (*tunnel->disconnect_func)(tunnel, tunnel->callback_data);
+ tunnel->thread = 0;
+ return NULL;
+ }
+#endif
+
+ if (tunnel->init_func &&
+ !(*tunnel->init_func)(tunnel, tunnel->callback_data)) {
+ if (tunnel->disconnect_func)
+ (*tunnel->disconnect_func)(tunnel, tunnel->callback_data);
+ tunnel->thread = 0;
+ return NULL;
+ }
+
+ break;
+ }
+
+ tunnel->buffer_len = 10240;
+ tunnel->buffer = g_malloc(tunnel->buffer_len);
+
+ /* Start the tunnel data transmission */
+ while (tunnel->running) {
+ if (tunnel->tunnel_type == REMMINA_SSH_TUNNEL_XPORT ||
+ tunnel->tunnel_type == REMMINA_SSH_TUNNEL_REVERSE) {
+ if (first) {
+ first = FALSE;
+ channel = ssh_channel_accept_forward(REMMINA_SSH(tunnel)->session, 15000, &tunnel->port);
+ if (!channel) {
+ remmina_ssh_set_application_error(REMMINA_SSH(tunnel), _("The server did not respond."));
+ if (tunnel->disconnect_func)
+ (*tunnel->disconnect_func)(tunnel, tunnel->callback_data);
+ tunnel->thread = 0;
+ return NULL;
+ }
+ if (tunnel->connect_func)
+ (*tunnel->connect_func)(tunnel, tunnel->callback_data);
+ if (tunnel->tunnel_type == REMMINA_SSH_TUNNEL_REVERSE) {
+ /* For reverse tunnel, we only need one connection. */
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 7, 0)
+ ssh_channel_cancel_forward(REMMINA_SSH(tunnel)->session, NULL, tunnel->port);
+#else
+ ssh_forward_cancel(REMMINA_SSH(tunnel)->session, NULL, tunnel->port);
+#endif
+ }
+ } else if (tunnel->tunnel_type != REMMINA_SSH_TUNNEL_REVERSE) {
+ /* Poll once per some period of time if no incoming connections.
+ * Don’t try to poll continuously as it will significantly slow down the loop */
+ t1 = g_date_time_new_now_local();
+ diff = g_date_time_difference(t1, t2) * 10000000
+ + g_date_time_difference(t1, t2) / 100000;
+ if (diff > 1) {
+ REMMINA_DEBUG("Polling tunnel channels");
+ channel = ssh_channel_accept_forward(REMMINA_SSH(tunnel)->session, 0, &tunnel->port);
+ if (channel == NULL)
+ t2 = g_date_time_new_now_local();
+ }
+ }
+
+ if (channel) {
+ if (tunnel->tunnel_type == REMMINA_SSH_TUNNEL_REVERSE) {
+ sin.sin_family = AF_INET;
+ sin.sin_port = htons(tunnel->localport);
+ sin.sin_addr.s_addr = inet_addr("127.0.0.1");
+ sock = socket(AF_INET, SOCK_STREAM, 0);
+ if (connect(sock, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
+ remmina_ssh_set_application_error(REMMINA_SSH(tunnel),
+ _("Cannot connect to local port %i."), tunnel->localport);
+ close(sock);
+ sock = -1;
+ }
+ } else
+ sock = remmina_public_open_xdisplay(tunnel->localdisplay);
+ if (sock >= 0)
+ remmina_ssh_tunnel_add_channel(tunnel, channel, sock);
+ else {
+ /* Failed to create unix socket. Will this happen? */
+ ssh_channel_close(channel);
+ ssh_channel_send_eof(channel);
+ ssh_channel_free(channel);
+ }
+ channel = NULL;
+ }
+ }
+
+ if (tunnel->num_channels <= 0)
+ /* No more connections. We should quit */
+ break;
+
+ timeout.tv_sec = 0;
+ timeout.tv_usec = 200000;
+
+ FD_ZERO(&set);
+ maxfd = 0;
+ for (i = 0; i < tunnel->num_channels; i++) {
+ if (tunnel->sockets[i] > maxfd)
+ maxfd = tunnel->sockets[i];
+ FD_SET(tunnel->sockets[i], &set);
+ }
+
+ ret = ssh_select(tunnel->channels, tunnel->channels_out, maxfd + 1, &set, &timeout);
+ if (!tunnel->running) break;
+ if (ret == SSH_EINTR) continue;
+ if (ret == -1) break;
+
+ i = 0;
+ while (tunnel->running && i < tunnel->num_channels) {
+ disconnected = FALSE;
+ if (FD_ISSET(tunnel->sockets[i], &set)) {
+ while (!disconnected &&
+ (len = read(tunnel->sockets[i], tunnel->buffer, tunnel->buffer_len)) > 0) {
+ for (ptr = tunnel->buffer, lenw = 0; len > 0; len -= lenw, ptr += lenw) {
+ lenw = ssh_channel_write(tunnel->channels[i], (char *)ptr, len);
+ if (lenw <= 0) {
+ disconnected = TRUE;
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not write to SSH channel. %s"));
+ break;
+ }
+ }
+ }
+ if (len == 0) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not read from tunnel listening socket. %s"));
+ disconnected = TRUE;
+ }
+ }
+ if (disconnected) {
+ REMMINA_DEBUG("tunnel disconnected because %s", REMMINA_SSH(tunnel)->error);
+ remmina_ssh_tunnel_remove_channel(tunnel, i);
+ continue;
+ }
+ i++;
+ }
+ if (!tunnel->running) break;
+
+ i = 0;
+ while (tunnel->running && i < tunnel->num_channels) {
+ disconnected = FALSE;
+ if (!tunnel->socketbuffers[i]) {
+ len = ssh_channel_poll(tunnel->channels[i], 0);
+ if (len == SSH_ERROR || len == SSH_EOF) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not poll SSH channel. %s"));
+ disconnected = TRUE;
+ } else if (len > 0) {
+ tunnel->socketbuffers[i] = remmina_ssh_tunnel_buffer_new(len);
+ len = ssh_channel_read_nonblocking(tunnel->channels[i], tunnel->socketbuffers[i]->data, len, 0);
+ if (len <= 0) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not read SSH channel in a non-blocking way. %s"));
+ disconnected = TRUE;
+ } else {
+ tunnel->socketbuffers[i]->len = len;
+ }
+ }
+ }
+
+ if (!disconnected && tunnel->socketbuffers[i]) {
+ for (lenw = 0; tunnel->socketbuffers[i]->len > 0;
+ tunnel->socketbuffers[i]->len -= lenw, tunnel->socketbuffers[i]->ptr += lenw) {
+ lenw = write(tunnel->sockets[i], tunnel->socketbuffers[i]->ptr, tunnel->socketbuffers[i]->len);
+ if (lenw == -1 && errno == EAGAIN && tunnel->running)
+ /* Sometimes we cannot write to a socket (always EAGAIN), probably because it’s internal
+ * buffer is full. We need read the pending bytes from the socket first. so here we simply
+ * break, leave the buffer there, and continue with other data */
+ break;
+ if (lenw <= 0) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(tunnel), _("Could not send data to tunnel listening socket. %s"));
+ disconnected = TRUE;
+ break;
+ }
+ }
+ if (tunnel->socketbuffers[i]->len <= 0) {
+ remmina_ssh_tunnel_buffer_free(tunnel->socketbuffers[i]);
+ tunnel->socketbuffers[i] = NULL;
+ }
+ }
+
+ if (disconnected) {
+ REMMINA_DEBUG("Connection to SSH tunnel dropped. %s", REMMINA_SSH(tunnel)->error);
+ remmina_ssh_tunnel_remove_channel(tunnel, i);
+ continue;
+ }
+ i++;
+ }
+ /**
+ * Some protocols may open new connections during the session.
+ * e.g: SPICE opens a new connection for some channels.
+ */
+ sock = remmina_ssh_tunnel_accept_local_connection(tunnel, FALSE);
+ if (sock > 0) {
+ channel = remmina_ssh_tunnel_create_forward_channel(tunnel);
+ if (!channel) {
+ REMMINA_DEBUG("Could not open new SSH connection. %s", REMMINA_SSH(tunnel)->error);
+ close(sock);
+ /* Leave thread loop */
+ tunnel->running = FALSE;
+ } else {
+ remmina_ssh_tunnel_add_channel(tunnel, channel, sock);
+ }
+ }
+ }
+
+ remmina_ssh_tunnel_close_all_channels(tunnel);
+
+ tunnel->running = FALSE;
+
+ /* Notify tunnel owner of disconnection */
+ if (tunnel->disconnect_func)
+ (*tunnel->disconnect_func)(tunnel, tunnel->callback_data);
+
+ return NULL;
+}
+
+static gboolean remmina_ssh_notify_tunnel_main_thread_end(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaSSHTunnel *tunnel = (RemminaSSHTunnel *)data;
+
+ /* Ask tunnel owner to destroy tunnel object */
+ if (tunnel->destroy_func)
+ (*tunnel->destroy_func)(tunnel, tunnel->destroy_func_callback_data);
+
+ return FALSE;
+}
+
+static gpointer
+remmina_ssh_tunnel_main_thread(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaSSHTunnel *tunnel = (RemminaSSHTunnel *)data;
+
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+
+ while (TRUE) {
+ remmina_ssh_tunnel_main_thread_proc(data);
+ if (tunnel->server_sock < 0 || tunnel->thread == 0 || !tunnel->running) break;
+ }
+ tunnel->thread = 0;
+
+ /* Do after tunnel thread cleanup */
+ IDLE_ADD((GSourceFunc)remmina_ssh_notify_tunnel_main_thread_end, (gpointer)tunnel);
+
+ return NULL;
+}
+
+
+void
+remmina_ssh_tunnel_cancel_accept(RemminaSSHTunnel *tunnel)
+{
+ TRACE_CALL(__func__);
+ if (tunnel->server_sock >= 0) {
+ close(tunnel->server_sock);
+ tunnel->server_sock = -1;
+ }
+}
+
+gboolean
+remmina_ssh_tunnel_open(RemminaSSHTunnel *tunnel, const gchar *host, gint port, gint local_port)
+{
+ TRACE_CALL(__func__);
+ gint sock;
+ gint sockopt = 1;
+ struct sockaddr_in sin;
+
+ tunnel->tunnel_type = REMMINA_SSH_TUNNEL_OPEN;
+ tunnel->dest = g_strdup(host);
+ tunnel->port = port;
+ if (tunnel->port == 0) {
+ REMMINA_SSH(tunnel)->error = g_strdup(_("Assign a destination port."));
+ return FALSE;
+ }
+
+ /* Create the server socket that listens on the local port */
+ sock = socket(AF_INET, SOCK_STREAM, 0);
+ if (sock < 0) {
+ REMMINA_SSH(tunnel)->error = g_strdup(_("Could not create socket."));
+ return FALSE;
+ }
+ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &sockopt, sizeof(sockopt));
+
+ sin.sin_family = AF_INET;
+ sin.sin_port = htons(local_port);
+ sin.sin_addr.s_addr = inet_addr("127.0.0.1");
+
+ if (bind(sock, (struct sockaddr *)&sin, sizeof(sin))) {
+ REMMINA_SSH(tunnel)->error = g_strdup(_("Could not bind server socket to local port."));
+ close(sock);
+ return FALSE;
+ }
+
+ if (listen(sock, 1)) {
+ REMMINA_SSH(tunnel)->error = g_strdup(_("Could not listen to local port."));
+ close(sock);
+ return FALSE;
+ }
+
+ tunnel->server_sock = sock;
+ tunnel->running = TRUE;
+
+ if (pthread_create(&tunnel->thread, NULL, remmina_ssh_tunnel_main_thread, tunnel)) {
+ // TRANSLATORS: Do not translate pthread
+ remmina_ssh_set_application_error(REMMINA_SSH(tunnel), _("Could not start pthread."));
+ tunnel->thread = 0;
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+remmina_ssh_tunnel_xport(RemminaSSHTunnel *tunnel, gboolean bindlocalhost)
+{
+ TRACE_CALL(__func__);
+ tunnel->tunnel_type = REMMINA_SSH_TUNNEL_XPORT;
+ tunnel->bindlocalhost = bindlocalhost;
+ tunnel->running = TRUE;
+
+ if (pthread_create(&tunnel->thread, NULL, remmina_ssh_tunnel_main_thread, tunnel)) {
+ // TRANSLATORS: Do not translate pthread
+ remmina_ssh_set_application_error(REMMINA_SSH(tunnel), _("Could not start pthread."));
+ tunnel->thread = 0;
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+remmina_ssh_tunnel_reverse(RemminaSSHTunnel *tunnel, gint port, gint local_port)
+{
+ TRACE_CALL(__func__);
+ tunnel->tunnel_type = REMMINA_SSH_TUNNEL_REVERSE;
+ tunnel->port = port;
+ tunnel->localport = local_port;
+ tunnel->running = TRUE;
+
+ if (pthread_create(&tunnel->thread, NULL, remmina_ssh_tunnel_main_thread, tunnel)) {
+ // TRANSLATORS: Do not translate pthread
+ remmina_ssh_set_application_error(REMMINA_SSH(tunnel), _("Could not start pthread."));
+ tunnel->thread = 0;
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+remmina_ssh_tunnel_terminated(RemminaSSHTunnel *tunnel)
+{
+ TRACE_CALL(__func__);
+ return tunnel->thread == 0;
+}
+
+void
+remmina_ssh_tunnel_free(RemminaSSHTunnel *tunnel)
+{
+ TRACE_CALL(__func__);
+ pthread_t thread;
+
+ REMMINA_DEBUG("tunnel->thread = %lX\n", tunnel->thread);
+
+ thread = tunnel->thread;
+ if (thread != 0) {
+ tunnel->running = FALSE;
+ pthread_cancel(thread);
+ pthread_join(thread, NULL);
+ tunnel->thread = 0;
+ }
+
+ if (tunnel->tunnel_type == REMMINA_SSH_TUNNEL_XPORT && tunnel->remotedisplay > 0) {
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 7, 0)
+ ssh_channel_cancel_forward(REMMINA_SSH(tunnel)->session, NULL, 6000 + tunnel->remotedisplay);
+#else
+ ssh_forward_cancel(REMMINA_SSH(tunnel)->session, NULL, 6000 + tunnel->remotedisplay);
+#endif
+ }
+ if (tunnel->server_sock >= 0) {
+ close(tunnel->server_sock);
+ tunnel->server_sock = -1;
+ }
+
+ remmina_ssh_tunnel_close_all_channels(tunnel);
+
+ g_free(tunnel->buffer);
+ g_free(tunnel->channels_out);
+ g_free(tunnel->dest);
+ g_free(tunnel->localdisplay);
+
+ remmina_ssh_free((RemminaSSH *)tunnel);
+}
+
+/*-----------------------------------------------------------------------------*
+* SSH SFTP *
+*-----------------------------------------------------------------------------*/
+RemminaSFTP *
+remmina_sftp_new_from_file(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ RemminaSFTP *sftp;
+
+ sftp = g_new(RemminaSFTP, 1);
+
+ remmina_ssh_init_from_file(REMMINA_SSH(sftp), remminafile, FALSE);
+
+ sftp->sftp_sess = NULL;
+
+ return sftp;
+}
+
+RemminaSFTP *
+remmina_sftp_new_from_ssh(RemminaSSH *ssh)
+{
+ TRACE_CALL(__func__);
+ RemminaSFTP *sftp;
+
+ sftp = g_new(RemminaSFTP, 1);
+
+ remmina_ssh_init_from_ssh(REMMINA_SSH(sftp), ssh);
+
+ sftp->sftp_sess = NULL;
+
+ return sftp;
+}
+
+gboolean
+remmina_sftp_open(RemminaSFTP *sftp)
+{
+ TRACE_CALL(__func__);
+ sftp->sftp_sess = sftp_new(sftp->ssh.session);
+ if (!sftp->sftp_sess) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(sftp), _("Could not create SFTP session. %s"));
+ return FALSE;
+ }
+ if (sftp_init(sftp->sftp_sess)) {
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(sftp), _("Could not start SFTP session. %s"));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void
+remmina_sftp_free(RemminaSFTP *sftp)
+{
+ TRACE_CALL(__func__);
+ if (sftp->sftp_sess) {
+ sftp_free(sftp->sftp_sess);
+ sftp->sftp_sess = NULL;
+ }
+ remmina_ssh_free(REMMINA_SSH(sftp));
+}
+
+/*-----------------------------------------------------------------------------*
+* SSH Shell *
+*-----------------------------------------------------------------------------*/
+RemminaSSHShell *
+remmina_ssh_shell_new_from_file(RemminaFile *remminafile)
+{
+ TRACE_CALL(__func__);
+ RemminaSSHShell *shell;
+
+ shell = g_new0(RemminaSSHShell, 1);
+
+ remmina_ssh_init_from_file(REMMINA_SSH(shell), remminafile, FALSE);
+
+ shell->master = -1;
+ shell->slave = -1;
+ shell->exec = g_strdup(remmina_file_get_string(remminafile, "exec"));
+ shell->run_line = g_strdup(remmina_file_get_string(remminafile, "run_line"));
+
+ return shell;
+}
+
+RemminaSSHShell *
+remmina_ssh_shell_new_from_ssh(RemminaSSH *ssh)
+{
+ TRACE_CALL(__func__);
+ RemminaSSHShell *shell;
+
+ shell = g_new0(RemminaSSHShell, 1);
+
+ remmina_ssh_init_from_ssh(REMMINA_SSH(shell), ssh);
+
+ shell->master = -1;
+ shell->slave = -1;
+
+ return shell;
+}
+
+static gboolean
+remmina_ssh_call_exit_callback_on_main_thread(gpointer data)
+{
+ TRACE_CALL(__func__);
+
+ RemminaSSHShell *shell = (RemminaSSHShell *)data;
+ if (shell->exit_callback)
+ shell->exit_callback(shell->user_data);
+ if (shell) {
+ remmina_ssh_shell_free(shell);
+ shell = NULL;
+ }
+ return FALSE;
+}
+
+static gpointer
+remmina_ssh_shell_thread(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaSSHShell *shell = (RemminaSSHShell *)data;
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)shell->user_data;
+ RemminaFile *remminafile;
+ remminafile = remmina_protocol_widget_get_file(gp);
+ ssh_channel channel = NULL;
+ gint ret;
+ gchar *filename;
+ const gchar *dir;
+ const gchar *sshlogname;
+ FILE *fp = NULL;
+
+ //gint screen;
+
+ LOCK_SSH(shell)
+
+ if ((channel = ssh_channel_new(REMMINA_SSH(shell)->session)) == NULL ||
+ ssh_channel_open_session(channel)) {
+ UNLOCK_SSH(shell)
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(shell), _("Could not open channel. %s"));
+ if (channel) ssh_channel_free(channel);
+ shell->thread = 0;
+ return NULL;
+ }
+
+ ssh_channel_request_pty(channel);
+
+ // SSH Callbacks
+ struct ssh_callbacks_struct cb =
+ {
+ .channel_open_request_x11_function = remmina_ssh_x11_open_request_cb,
+ .userdata = shell,
+ };
+
+ if (remmina_file_get_int(remminafile, "ssh_forward_x11", FALSE)) {
+ ssh_callbacks_init(&cb);
+ ssh_set_callbacks(REMMINA_SSH(shell)->session, &cb);
+
+ const char *display = getenv("DISPLAY");
+ char *proto = NULL, *cookie = NULL;
+ if (remmina_ssh_x11_get_proto(display, &proto, &cookie) != 0) {
+ REMMINA_DEBUG("Using fake authentication data for X11 forwarding");
+ proto = NULL;
+ cookie = NULL;
+ }
+
+ REMMINA_DEBUG("proto: %s - cookie: %s", proto, cookie);
+ ret = ssh_channel_request_x11(channel, 0, proto, cookie, 0);
+ if (ret != SSH_OK) return NULL;
+ }
+
+ if (shell->exec && shell->exec[0]) {
+ REMMINA_DEBUG ("Requesting an SSH exec channel");
+ ret = ssh_channel_request_exec(channel, shell->exec);
+ } else {
+ REMMINA_DEBUG ("Requesting an SSH shell channel");
+ ret = ssh_channel_request_shell(channel);
+ }
+ if (ret != SSH_OK) {
+ UNLOCK_SSH(shell)
+ REMMINA_WARNING ("Could not request shell");
+ // TRANSLATORS: The placeholder %s is an error message
+ remmina_ssh_set_error(REMMINA_SSH(shell), _("Could not request shell. %s"));
+ ssh_channel_close(channel);
+ ssh_channel_send_eof(channel);
+ ssh_channel_free(channel);
+ shell->thread = 0;
+ return NULL;
+ }
+
+ shell->channel = channel;
+
+ UNLOCK_SSH(shell)
+
+ GFile *rf = g_file_new_for_path(remminafile->filename);
+
+ if (remmina_file_get_string(remminafile, "sshlogfolder") == NULL)
+ dir = g_build_path("/", g_get_user_cache_dir(), "remmina", NULL);
+ else
+ dir = remmina_file_get_string(remminafile, "sshlogfolder");
+
+ if (remmina_file_get_string(remminafile, "sshlogname") == NULL)
+ sshlogname = g_strconcat(g_file_get_basename(rf), ".", "log", NULL);
+ else
+ sshlogname = remmina_file_get_string(remminafile, "sshlogname");
+ sshlogname = remmina_file_format_properties(remminafile, sshlogname);
+ filename = g_strconcat(dir, "/", sshlogname, NULL);
+
+ if (remmina_file_get_int (remminafile, "sshsavesession", FALSE)) {
+ REMMINA_DEBUG("Saving session log to %s", filename);
+ fp = fopen(filename, "w");
+ }
+
+ g_free(filename);
+
+ REMMINA_DEBUG("Run_line: %s", shell->run_line);
+ if (!shell->closed && shell->run_line && shell->run_line[0]) {
+ LOCK_SSH(shell)
+ //TODO: Confirm assumption - assuming null terminated gchar string
+ ssh_channel_write(channel, shell->run_line, (gint)strlen(shell->run_line));
+ ssh_channel_write(channel, "\n", (gint)1); //TODO: Test this
+ UNLOCK_SSH(shell)
+ REMMINA_DEBUG("Run_line written to channel");
+ }
+
+ LOCK_SSH(shell)
+
+ // Create new event context.
+ shell->event = ssh_event_new();
+ if (shell->event == NULL) {
+ REMMINA_WARNING("Internal error in %s: Couldn't get a event.", __func__);
+ return NULL;
+ }
+
+ REMMINA_DEBUG("shell->slave: %d", shell->slave);
+
+ // Add the fd to the event and assign it the callback.
+ if (ssh_event_add_fd(shell->event, shell->slave, events, remmina_ssh_cp_to_ch_cb, channel) != SSH_OK) {
+ REMMINA_WARNING("Internal error in %s: Couldn't add an fd to the event.", __func__);
+ return NULL;
+ }
+
+ // Remove the poll handle from session and assign them to the event.
+ if (ssh_event_add_session(shell->event, REMMINA_SSH(shell)->session) != SSH_OK) {
+ REMMINA_WARNING("Internal error in %s: Couldn't add the session to the event.", __func__);
+ return NULL;
+ }
+
+ remmina_ssh_insert_item(shell->channel, shell->slave, shell->slave, TRUE, shell->thread);
+
+ // Initializes the ssh_callbacks_struct.
+ channel_cb.userdata = &shell;
+ ssh_callbacks_init(&channel_cb);
+ // Set the channel callback functions.
+ ssh_set_channel_callbacks(shell->channel, &channel_cb);
+ UNLOCK_SSH(shell)
+
+ do {
+ ssh_event_dopoll(shell->event, 1000);
+ } while(!ssh_channel_is_closed(shell->channel));
+
+ // Close all OPENED X11 channel
+ remmina_ssh_close_all_x11_ch(shell->thread);
+
+ shell->closed = TRUE;
+
+ LOCK_SSH(shell)
+
+ // Remove socket fd from event context.
+ ret = ssh_event_remove_fd(shell->event, shell->slave);
+ REMMINA_DEBUG("Remove socket fd from event context: %d", ret);
+
+ // Remove session object from event context.
+ ret = ssh_event_remove_session(shell->event, REMMINA_SSH(shell)->session);
+ REMMINA_DEBUG("Remove session object from event context: %d", ret);
+
+ // Free event context.
+ ssh_event_free(shell->event);
+ REMMINA_DEBUG("Free event context");
+
+ // Remove channel callback.
+ ret = ssh_remove_channel_callbacks(shell->channel, &channel_cb);
+ REMMINA_DEBUG("Remove channel callback: %d", ret);
+
+ if (remmina_file_get_int (remminafile, "sshsavesession", FALSE))
+ fclose(fp);
+ shell->channel = NULL;
+ ssh_channel_close(channel);
+ ssh_channel_send_eof(channel);
+ ssh_channel_free(channel);
+ UNLOCK_SSH(shell)
+
+ shell->thread = 0;
+
+ if (shell->exit_callback)
+ IDLE_ADD((GSourceFunc)remmina_ssh_call_exit_callback_on_main_thread, (gpointer)shell);
+ return NULL;
+}
+
+gboolean
+remmina_ssh_shell_open(RemminaSSHShell *shell, RemminaSSHExitFunc exit_callback, gpointer data)
+{
+ TRACE_CALL(__func__);
+ gchar *slavedevice;
+ struct termios stermios;
+
+ shell->master = posix_openpt(O_RDWR | O_NOCTTY);
+ if (shell->master == -1 ||
+ grantpt(shell->master) == -1 ||
+ unlockpt(shell->master) == -1 ||
+ (slavedevice = ptsname(shell->master)) == NULL ||
+ (shell->slave = open(slavedevice, O_RDWR | O_NOCTTY)) < 0) {
+ REMMINA_SSH(shell)->error = g_strdup(_("Could not create PTY device."));
+ return FALSE;
+ }
+
+ /* As per libssh documentation */
+ tcgetattr(shell->slave, &stermios);
+ stermios.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
+ stermios.c_oflag &= ~OPOST;
+ stermios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
+ stermios.c_cflag &= ~(CSIZE | PARENB);
+ stermios.c_cflag |= CS8;
+ tcsetattr(shell->slave, TCSANOW, &stermios);
+
+ shell->exit_callback = exit_callback;
+ shell->user_data = data;
+
+ /* Once the process started, we should always TRUE and assume the pthread will be created always */
+ pthread_create(&shell->thread, NULL, remmina_ssh_shell_thread, shell);
+
+ return TRUE;
+}
+
+void
+remmina_ssh_shell_set_size(RemminaSSHShell *shell, gint columns, gint rows)
+{
+ TRACE_CALL(__func__);
+ LOCK_SSH(shell)
+ if (shell->channel)
+ ssh_channel_change_pty_size(shell->channel, columns, rows);
+ UNLOCK_SSH(shell)
+}
+
+void
+remmina_ssh_shell_free(RemminaSSHShell *shell)
+{
+ TRACE_CALL(__func__);
+ //pthread_t thread = shell->thread;
+
+ // Close all OPENED X11 channel
+ remmina_ssh_close_all_x11_ch(shell->thread);
+
+ shell->exit_callback = NULL;
+ shell->closed = TRUE;
+ REMMINA_DEBUG("Cancelling the shell thread if needed");
+ if (shell->thread) {
+ pthread_cancel(shell->thread);
+ if (shell->thread) pthread_join(shell->thread, NULL);
+ }
+ close(shell->slave);
+ if (shell->exec) {
+ g_free(shell->exec);
+ shell->exec = NULL;
+ }
+ if (shell->run_line) {
+ g_free(shell->run_line);
+ shell->run_line = NULL;
+ }
+ /* It’s not necessary to close shell->slave since the other end (vte) will close it */;
+ remmina_ssh_free(REMMINA_SSH(shell));
+}
+
+#endif /* HAVE_LIBSSH */
diff --git a/src/remmina_ssh.h b/src/remmina_ssh.h
new file mode 100644
index 0000000..baad192
--- /dev/null
+++ b/src/remmina_ssh.h
@@ -0,0 +1,284 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009-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 "config.h"
+
+#ifdef HAVE_LIBSSH
+
+#define LIBSSH_STATIC 1
+#include <libssh/libssh.h>
+#include <libssh/callbacks.h>
+#include <libssh/sftp.h>
+#include <pthread.h>
+#include "remmina_file.h"
+#include "rcw.h"
+
+G_BEGIN_DECLS
+
+/*-----------------------------------------------------------------------------*
+* SSH Base *
+*-----------------------------------------------------------------------------*/
+
+#define REMMINA_SSH(a) ((RemminaSSH *)a)
+
+typedef struct _RemminaSSH {
+ ssh_session session;
+ ssh_callbacks callback;
+ gboolean authenticated;
+
+ gchar * server;
+ gint port;
+ gchar * user;
+ gint auth;
+ gchar * password;
+ gchar * privkeyfile;
+ gchar * certfile;
+
+ gchar * charset;
+ const gchar * kex_algorithms;
+ gchar * ciphers;
+ gchar * hostkeytypes;
+ gchar * proxycommand;
+ gint stricthostkeycheck;
+ const gchar * compression;
+
+ gchar * error;
+
+ pthread_mutex_t ssh_mutex;
+
+ gchar * passphrase;
+
+ gboolean is_tunnel;
+ gboolean is_multiauth;
+ gchar * tunnel_entrance_host;
+ gint tunnel_entrance_port;
+
+} RemminaSSH;
+
+gchar *remmina_ssh_identity_path(const gchar *id);
+
+/* Auto-detect commonly used private key identities */
+gchar *remmina_ssh_find_identity(void);
+
+/* Initialize the ssh object */
+gboolean remmina_ssh_init_from_file(RemminaSSH *ssh, RemminaFile *remminafile, gboolean is_tunnel);
+
+/* Initialize the SSH session */
+gboolean remmina_ssh_init_session(RemminaSSH *ssh);
+
+/* Authenticate SSH session */
+
+
+enum remmina_ssh_auth_result {
+ REMMINA_SSH_AUTH_NULL,
+ REMMINA_SSH_AUTH_SUCCESS,
+ REMMINA_SSH_AUTH_PARTIAL,
+ REMMINA_SSH_AUTH_AGAIN,
+ REMMINA_SSH_AUTH_AUTHFAILED_RETRY_AFTER_PROMPT,
+ REMMINA_SSH_AUTH_USERCANCEL,
+ REMMINA_SSH_AUTH_FATAL_ERROR,
+ REMMINA_SSH_AUTH_RECONNECT,
+ REMMINA_SSH_AUTH_AUTHFAILED_EMPTY_USERNAME
+};
+
+enum remmina_ssh_auth_result remmina_ssh_auth(RemminaSSH *ssh, const gchar *password, RemminaProtocolWidget *gp, RemminaFile *remminafile);
+
+enum remmina_ssh_auth_result remmina_ssh_auth_gui(RemminaSSH *ssh, RemminaProtocolWidget *gp, RemminaFile *remminafile);
+
+/* Error handling */
+#define remmina_ssh_has_error(ssh) (((RemminaSSH *)ssh)->error != NULL)
+void remmina_ssh_set_error(RemminaSSH *ssh, const gchar *fmt);
+void remmina_ssh_set_application_error(RemminaSSH *ssh, const gchar *fmt, ...);
+
+/* Converts a string to/from UTF-8, or simply duplicate it if no conversion */
+gchar *remmina_ssh_convert(RemminaSSH *ssh, const gchar *from);
+gchar *remmina_ssh_unconvert(RemminaSSH *ssh, const gchar *from);
+
+void remmina_ssh_free(RemminaSSH *ssh);
+
+/*-----------------------------------------------------------------------------*
+* SSH Tunnel *
+*-----------------------------------------------------------------------------*/
+typedef struct _RemminaSSHTunnel RemminaSSHTunnel;
+typedef struct _RemminaSSHTunnelBuffer RemminaSSHTunnelBuffer;
+
+typedef gboolean (*RemminaSSHTunnelCallback) (RemminaSSHTunnel *, gpointer);
+
+enum {
+ REMMINA_SSH_TUNNEL_OPEN,
+ REMMINA_SSH_TUNNEL_XPORT,
+ REMMINA_SSH_TUNNEL_REVERSE
+};
+
+
+struct _RemminaSSHTunnel {
+ RemminaSSH ssh;
+
+ gint tunnel_type;
+
+ ssh_channel * channels;
+ gint * sockets;
+ RemminaSSHTunnelBuffer ** socketbuffers;
+ gint num_channels;
+ gint max_channels;
+
+ pthread_t thread;
+ gboolean running;
+
+ gchar * buffer;
+ gint buffer_len;
+ ssh_channel * channels_out;
+
+ gint server_sock;
+ gchar * dest;
+ gint port;
+ gint localport;
+
+ gint remotedisplay;
+ gboolean bindlocalhost;
+ gchar * localdisplay;
+
+ RemminaSSHTunnelCallback init_func;
+ RemminaSSHTunnelCallback connect_func;
+ RemminaSSHTunnelCallback disconnect_func;
+ gpointer callback_data;
+
+ RemminaSSHTunnelCallback destroy_func;
+ gpointer destroy_func_callback_data;
+
+};
+
+/* Create a new SSH Tunnel session and connects to the SSH server */
+RemminaSSHTunnel *remmina_ssh_tunnel_new_from_file(RemminaFile *remminafile);
+
+/* Open the tunnel. A new thread will be started and listen on a local port.
+ * dest: The host:port of the remote destination
+ * local_port: The listening local port for the tunnel
+ */
+gboolean remmina_ssh_tunnel_open(RemminaSSHTunnel *tunnel, const gchar *host, gint port, gint local_port);
+
+/* Cancel accepting any incoming tunnel request.
+ * Typically called after the connection has already been establish.
+ */
+void remmina_ssh_tunnel_cancel_accept(RemminaSSHTunnel *tunnel);
+
+/* start X Port Forwarding */
+gboolean remmina_ssh_tunnel_xport(RemminaSSHTunnel *tunnel, gboolean bindlocalhost);
+
+/* start reverse tunnel. A new thread will be started and waiting for incoming connection.
+ * port: the port listening on the remote server side.
+ * local_port: the port listening on the local side. When connection on the server side comes
+ * in, it will connect to the local port and create the tunnel. The caller should
+ * start listening on the local port before calling it or in connect_func callback.
+ */
+gboolean remmina_ssh_tunnel_reverse(RemminaSSHTunnel *tunnel, gint port, gint local_port);
+
+/* Tells if the tunnel is terminated after start */
+gboolean remmina_ssh_tunnel_terminated(RemminaSSHTunnel *tunnel);
+
+/* Free the tunnel */
+void remmina_ssh_tunnel_free(RemminaSSHTunnel *tunnel);
+
+/*-----------------------------------------------------------------------------*
+* SSH sFTP *
+*-----------------------------------------------------------------------------*/
+
+typedef struct _RemminaSFTP {
+ RemminaSSH ssh;
+
+ sftp_session sftp_sess;
+} RemminaSFTP;
+
+/* Create a new SFTP session object from RemminaFile */
+RemminaSFTP *remmina_sftp_new_from_file(RemminaFile *remminafile);
+
+/* Create a new SFTP session object from existing SSH session */
+RemminaSFTP *remmina_sftp_new_from_ssh(RemminaSSH *ssh);
+
+/* open the SFTP session, assuming the session already authenticated */
+gboolean remmina_sftp_open(RemminaSFTP *sftp);
+
+/* Free the SFTP session */
+void remmina_sftp_free(RemminaSFTP *sftp);
+
+/*-----------------------------------------------------------------------------*
+* SSH Shell *
+*-----------------------------------------------------------------------------*/
+typedef void (*RemminaSSHExitFunc) (gpointer data);
+
+typedef struct _RemminaSSHShell {
+ RemminaSSH ssh;
+
+ gint master;
+ gint slave;
+ gchar * exec;
+ gchar * run_line;
+ pthread_t thread;
+ ssh_channel channel;
+ gboolean closed;
+ RemminaSSHExitFunc exit_callback;
+ gpointer user_data;
+ ssh_event event;
+} RemminaSSHShell;
+
+/* Create a new SSH Shell session object from RemminaFile */
+RemminaSSHShell *remmina_ssh_shell_new_from_file(RemminaFile *remminafile);
+
+/* Create a new SSH Shell session object from existing SSH session */
+RemminaSSHShell *remmina_ssh_shell_new_from_ssh(RemminaSSH *ssh);
+
+/* open the SSH Shell, assuming the session already authenticated */
+gboolean remmina_ssh_shell_open(RemminaSSHShell *shell, RemminaSSHExitFunc exit_callback, gpointer data);
+
+/* Change the SSH Shell terminal size */
+void remmina_ssh_shell_set_size(RemminaSSHShell *shell, gint columns, gint rows);
+
+/* Free the SFTP session */
+void remmina_ssh_shell_free(RemminaSSHShell *shell);
+
+G_END_DECLS
+
+#else
+
+#define RemminaSSH void
+#define RemminaSSHTunnel void
+#define RemminaSFTP void
+#define RemminaSSHShell void
+typedef void (*RemminaSSHTunnelCallback)(void);
+
+#endif /* HAVE_LIBSSH */
diff --git a/src/remmina_ssh_plugin.c b/src/remmina_ssh_plugin.c
new file mode 100644
index 0000000..93f692f
--- /dev/null
+++ b/src/remmina_ssh_plugin.c
@@ -0,0 +1,1738 @@
+/**
+ * Remmina - The GTK+ Remote Desktop Client - SSH plugin.
+ *
+ * @copyright Copyright (C) 2010-2011 Vic Lee.
+ * @copyright Copyright (C) 2014-2015 Antenore Gatta, Fabio Castelli, Giovanni Panozzo.
+ * @copyright Copyright (C) 2016-2022 Antenore Gatta, Giovanni Panozzo.
+ * @copyright 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 "config.h"
+#include "remmina/remmina_trace_calls.h"
+
+#if defined (HAVE_LIBSSH) && defined (HAVE_LIBVTE)
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+#include <gobject/gvaluecollector.h>
+#include <vte/vte.h>
+#include <locale.h>
+#include <langinfo.h>
+#include "remmina_log.h"
+#include "remmina_public.h"
+#include "remmina_plugin_manager.h"
+#include "remmina_ssh.h"
+#include "remmina_protocol_widget.h"
+#include "remmina_pref.h"
+#include "remmina_ssh_plugin.h"
+#include "remmina_masterthread_exec.h"
+
+#define PCRE2_CODE_UNIT_WIDTH 8
+#include <pcre2.h>
+
+
+#define REMMINA_PLUGIN_SSH_FEATURE_TOOL_COPY 1
+#define REMMINA_PLUGIN_SSH_FEATURE_TOOL_PASTE 2
+#define REMMINA_PLUGIN_SSH_FEATURE_TOOL_SELECT_ALL 3
+#define REMMINA_PLUGIN_SSH_FEATURE_TOOL_INCREASE_FONT 4
+#define REMMINA_PLUGIN_SSH_FEATURE_TOOL_DECREASE_FONT 5
+#define REMMINA_PLUGIN_SSH_FEATURE_TOOL_SEARCH 6
+
+#define GET_PLUGIN_DATA(gp) (RemminaPluginSshData *)g_object_get_data(G_OBJECT(gp), "plugin-data");
+
+/** Palette colors taken from sakura */
+#define PALETTE_SIZE 16
+/* Min fontsize and increase */
+#define FONT_SCALE 0.75
+#define SCALE_FACTOR 0.1
+#define FONT_MINIMAL_SIZE (PANGO_SCALE * 6)
+
+enum color_schemes { LINUX, TANGO, GRUVBOX, SOLARIZED_DARK, SOLARIZED_LIGHT, XTERM, CUSTOM };
+
+/** 16 colour palettes in GdkRGBA format (red, green, blue, alpha).
+ * Text displayed in the first 8 colours (0-7) is meek (uses thin strokes).
+ * Text displayed in the second 8 colours (8-15) is bold (uses thick strokes).
+ **/
+const GdkRGBA linux_palette[PALETTE_SIZE] = {
+ { 0, 0, 0, 1 },
+ { 0.666667, 0, 0, 1 },
+ { 0, 0.666667, 0, 1 },
+ { 0.666667, 0.333333, 0, 1 },
+ { 0, 0, 0.666667, 1 },
+ { 0.666667, 0, 0.666667, 1 },
+ { 0, 0.666667, 0.666667, 1 },
+ { 0.666667, 0.666667, 0.666667, 1 },
+ { 0.333333, 0.333333, 0.333333, 1 },
+ { 1, 0.333333, 0.333333, 1 },
+ { 0.333333, 1, 0.333333, 1 },
+ { 1, 1, 0.333333, 1 },
+ { 0.333333, 0.333333, 1, 1 },
+ { 1, 0.333333, 1, 1 },
+ { 0.333333, 1, 1, 1 },
+ { 1, 1, 1, 1 }
+};
+
+const GdkRGBA tango_palette[PALETTE_SIZE] = {
+ { 0, 0, 0, 1 },
+ { 0.8, 0, 0, 1 },
+ { 0.305882, 0.603922, 0.023529, 1 },
+ { 0.768627, 0.627451, 0, 1 },
+ { 0.203922, 0.396078, 0.643137, 1 },
+ { 0.458824, 0.313725, 0.482353, 1 },
+ { 0.0235294, 0.596078, 0.603922, 1 },
+ { 0.827451, 0.843137, 0.811765, 1 },
+ { 0.333333, 0.341176, 0.32549, 1 },
+ { 0.937255, 0.160784, 0.160784, 1 },
+ { 0.541176, 0.886275, 0.203922, 1 },
+ { 0.988235, 0.913725, 0.309804, 1 },
+ { 0.447059, 0.623529, 0.811765, 1 },
+ { 0.678431, 0.498039, 0.658824, 1 },
+ { 0.203922, 0.886275, 0.886275, 1 },
+ { 0.933333, 0.933333, 0.92549, 1 }
+};
+
+const GdkRGBA gruvbox_palette[PALETTE_SIZE] = {
+ { 0.156863, 0.156863, 0.156863, 1.000000 },
+ { 0.800000, 0.141176, 0.113725, 1.000000 },
+ { 0.596078, 0.592157, 0.101961, 1.000000 },
+ { 0.843137, 0.600000, 0.129412, 1.000000 },
+ { 0.270588, 0.521569, 0.533333, 1.000000 },
+ { 0.694118, 0.384314, 0.525490, 1.000000 },
+ { 0.407843, 0.615686, 0.415686, 1.000000 },
+ { 0.658824, 0.600000, 0.517647, 1.000000 },
+ { 0.572549, 0.513725, 0.454902, 1.000000 },
+ { 0.984314, 0.286275, 0.203922, 1.000000 },
+ { 0.721569, 0.733333, 0.149020, 1.000000 },
+ { 0.980392, 0.741176, 0.184314, 1.000000 },
+ { 0.513725, 0.647059, 0.596078, 1.000000 },
+ { 0.827451, 0.525490, 0.607843, 1.000000 },
+ { 0.556863, 0.752941, 0.486275, 1.000000 },
+ { 0.921569, 0.858824, 0.698039, 1.000000 },
+};
+
+const GdkRGBA solarized_dark_palette[PALETTE_SIZE] = {
+ { 0.027451, 0.211765, 0.258824, 1 },
+ { 0.862745, 0.196078, 0.184314, 1 },
+ { 0.521569, 0.600000, 0.000000, 1 },
+ { 0.709804, 0.537255, 0.000000, 1 },
+ { 0.149020, 0.545098, 0.823529, 1 },
+ { 0.827451, 0.211765, 0.509804, 1 },
+ { 0.164706, 0.631373, 0.596078, 1 },
+ { 0.933333, 0.909804, 0.835294, 1 },
+ { 0.000000, 0.168627, 0.211765, 1 },
+ { 0.796078, 0.294118, 0.086275, 1 },
+ { 0.345098, 0.431373, 0.458824, 1 },
+ { 0.396078, 0.482353, 0.513725, 1 },
+ { 0.513725, 0.580392, 0.588235, 1 },
+ { 0.423529, 0.443137, 0.768627, 1 },
+ { 0.576471, 0.631373, 0.631373, 1 },
+ { 0.992157, 0.964706, 0.890196, 1 }
+};
+
+const GdkRGBA solarized_light_palette[PALETTE_SIZE] = {
+ { 0.933333, 0.909804, 0.835294, 1 },
+ { 0.862745, 0.196078, 0.184314, 1 },
+ { 0.521569, 0.600000, 0.000000, 1 },
+ { 0.709804, 0.537255, 0.000000, 1 },
+ { 0.149020, 0.545098, 0.823529, 1 },
+ { 0.827451, 0.211765, 0.509804, 1 },
+ { 0.164706, 0.631373, 0.596078, 1 },
+ { 0.027451, 0.211765, 0.258824, 1 },
+ { 0.992157, 0.964706, 0.890196, 1 },
+ { 0.796078, 0.294118, 0.086275, 1 },
+ { 0.576471, 0.631373, 0.631373, 1 },
+ { 0.513725, 0.580392, 0.588235, 1 },
+ { 0.396078, 0.482353, 0.513725, 1 },
+ { 0.423529, 0.443137, 0.768627, 1 },
+ { 0.345098, 0.431373, 0.458824, 1 },
+ { 0.000000, 0.168627, 0.211765, 1 }
+};
+
+const GdkRGBA xterm_palette[PALETTE_SIZE] = {
+ { 0, 0, 0, 1 },
+ { 0.803922, 0, 0, 1 },
+ { 0, 0.803922, 0, 1 },
+ { 0.803922, 0.803922, 0, 1 },
+ { 0.117647, 0.564706, 1, 1 },
+ { 0.803922, 0, 0.803922, 1 },
+ { 0, 0.803922, 0.803922, 1 },
+ { 0.898039, 0.898039, 0.898039, 1 },
+ { 0.298039, 0.298039, 0.298039, 1 },
+ { 1, 0, 0, 1 },
+ { 0, 1, 0, 1 },
+ { 1, 1, 0, 1 },
+ { 0.27451, 0.509804, 0.705882, 1 },
+ { 1, 0, 1, 1 },
+ { 0, 1, 1, 1 },
+ { 1, 1, 1, 1 }
+};
+
+#define DEFAULT_PALETTE "linux_palette"
+
+
+/** The SSH plugin implementation */
+typedef struct _RemminaSshSearch {
+ GtkWidget * parent;
+
+ GtkBuilder * builder;
+ GtkWidget * window;
+ GtkWidget * search_entry;
+ GtkWidget * search_prev_button;
+ GtkWidget * search_next_button;
+ GtkWidget * close_button;
+ GtkToggleButton * match_case_checkbutton;
+ GtkToggleButton * entire_word_checkbutton;
+ GtkToggleButton * regex_checkbutton;
+ GtkToggleButton * wrap_around_checkbutton;
+ GtkWidget * reveal_button;
+ GtkWidget * revealer;
+
+ //VteTerminal *terminal;
+ gboolean regex_caseless;
+ gboolean has_regex;
+ gchar * regex_pattern;
+} RemminaSshSearch;
+
+typedef struct _RemminaPluginSshData {
+ RemminaSSHShell * shell;
+ GFile * vte_session_file;
+ GtkWidget * vte;
+
+ const GdkRGBA * palette;
+
+ pthread_t thread;
+
+ gboolean closed;
+
+ RemminaSshSearch * search_widget;
+} RemminaPluginSshData;
+
+
+#define GET_OBJECT(object_name) gtk_builder_get_object(search_widget->builder, object_name)
+
+static RemminaPluginService *remmina_plugin_service = NULL;
+
+static gboolean
+remmina_plugin_ssh_on_size_allocate(GtkWidget *widget, GtkAllocation *alloc, RemminaProtocolWidget *gp);
+
+static gboolean
+valid_color(GdkRGBA const *color)
+{
+ return color->red >= 0. && color->red <= 1. &&
+ color->green >= 0. && color->green <= 1. &&
+ color->blue >= 0. && color->blue <= 1. &&
+ color->alpha >= 0. && color->alpha <= 1.;
+}
+
+
+/**
+ * Remmina protocol plugin main function.
+ *
+ * First it starts the SSH tunnel if needed and then the SSH connection.
+ *
+ */
+static gpointer
+remmina_plugin_ssh_main_thread(gpointer data)
+{
+ TRACE_CALL(__func__);
+ RemminaProtocolWidget *gp = (RemminaProtocolWidget *)data;
+ RemminaPluginSshData *gpdata;
+ RemminaFile *remminafile;
+ RemminaSSH *ssh;
+ RemminaSSHShell *shell = NULL;
+ gboolean cont = FALSE;
+ gboolean partial = FALSE;
+ gchar *hostport;
+ gint ret = REMMINA_SSH_AUTH_NULL;
+
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+ CANCEL_ASYNC
+
+ gpdata = GET_PLUGIN_DATA(gp);
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ /* we may need to open a new tunnel */
+ REMMINA_DEBUG("Tentatively create an SSH tunnel");
+ hostport = remmina_plugin_service->protocol_plugin_start_direct_tunnel(gp, 22, FALSE);
+ if (hostport == NULL) {
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ return NULL;
+ }
+ REMMINA_DEBUG("protocol_plugin_start_direct_tunnel returned hostport: %s", hostport);
+
+ ssh = g_object_get_data(G_OBJECT(gp), "user-data");
+ if (ssh) {
+ REMMINA_DEBUG("Creating SSH shell based on existing SSH session");
+ shell = remmina_ssh_shell_new_from_ssh(ssh);
+ REMMINA_DEBUG("Calling remmina_public_get_server_port");
+ remmina_plugin_service->get_server_port(hostport, 22, &ssh->tunnel_entrance_host, &ssh->tunnel_entrance_port);
+ REMMINA_DEBUG("tunnel_entrance_host: %s, tunnel_entrance_port: %d", ssh->tunnel_entrance_host, ssh->tunnel_entrance_port);
+
+ if (remmina_ssh_init_session(REMMINA_SSH(shell)) &&
+ remmina_ssh_auth(REMMINA_SSH(shell), NULL, gp, remminafile) == REMMINA_SSH_AUTH_SUCCESS &&
+ remmina_ssh_shell_open(shell, (RemminaSSHExitFunc)
+ remmina_plugin_service->protocol_plugin_signal_connection_closed, gp))
+ cont = TRUE;
+ } else {
+ /* New SSH Shell connection */
+ REMMINA_DEBUG("Creating SSH shell based on a new SSH session");
+ shell = remmina_ssh_shell_new_from_file(remminafile);
+ ssh = REMMINA_SSH(shell);
+ REMMINA_DEBUG("Calling remmina_public_get_server_port");
+ remmina_plugin_service->get_server_port(hostport, 22, &ssh->tunnel_entrance_host, &ssh->tunnel_entrance_port);
+ REMMINA_DEBUG("tunnel_entrance_host: %s, tunnel_entrance_port: %d", ssh->tunnel_entrance_host, ssh->tunnel_entrance_port);
+
+ while (1) {
+ if (!partial) {
+ if (!remmina_ssh_init_session(ssh)) {
+ REMMINA_DEBUG("init session error: %s", ssh->error);
+ remmina_plugin_service->protocol_plugin_set_error(gp, "%s", ssh->error);
+ // exit the loop here: OK
+ break;
+ }
+ }
+
+ ret = remmina_ssh_auth_gui(ssh, gp, remminafile);
+ switch (ret) {
+ case REMMINA_SSH_AUTH_SUCCESS:
+ REMMINA_DEBUG("Authentication success");
+ if (!remmina_ssh_shell_open(shell, (RemminaSSHExitFunc)
+ remmina_plugin_service->protocol_plugin_signal_connection_closed, gp)) {
+ remmina_plugin_service->protocol_plugin_set_error(gp, "%s", ssh->error);
+ break;
+ }
+ gchar *server;
+ gint port;
+ remmina_plugin_service->get_server_port(remmina_plugin_service->file_get_string(remminafile, "server"),
+ 22,
+ &server,
+ &port);
+
+ REMMINA_AUDIT(_("Connected to %s:%d via SSH"), server, port);
+ g_free(server), server = NULL;
+ break;
+ case REMMINA_SSH_AUTH_PARTIAL:
+ REMMINA_DEBUG("Continue with the next auth method");
+ partial = TRUE;
+ // Continue the loop: OK
+ continue;
+ break;
+ case REMMINA_SSH_AUTH_RECONNECT:
+ REMMINA_DEBUG("Reconnecting…");
+ if (ssh->session) {
+ ssh_disconnect(ssh->session);
+ ssh_free(ssh->session);
+ ssh->session = NULL;
+ }
+ g_free(ssh->callback);
+ // Continue the loop: OK
+ continue;
+ break;
+ case REMMINA_SSH_AUTH_USERCANCEL:
+ REMMINA_DEBUG("Interrupted by the user");
+ // TODO: exit the loop here: OK
+ goto BREAK;
+ break;
+ default:
+ REMMINA_DEBUG("Error during the authentication: %s", ssh->error);
+ remmina_plugin_service->protocol_plugin_set_error(gp, "%s", ssh->error);
+ // TODO: exit the loop here: OK
+ goto BREAK;
+ }
+
+
+ cont = TRUE;
+ break;
+ }
+ }
+
+BREAK:
+ REMMINA_DEBUG("Authentication terminated with exit status %d", ret);
+
+ g_free(hostport);
+
+ if (!cont) {
+ if (shell) remmina_ssh_shell_free(shell);
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ return NULL;
+ }
+
+ gpdata->shell = shell;
+
+ gchar *charset = REMMINA_SSH(shell)->charset;
+
+ remmina_plugin_ssh_vte_terminal_set_encoding_and_pty(VTE_TERMINAL(gpdata->vte), charset, shell->master, shell->slave);
+
+ /* TODO: The following call should be moved on the main thread, or something weird could happen */
+ //remmina_plugin_ssh_on_size_allocate(GTK_WIDGET(gpdata->vte), NULL, gp);
+
+ remmina_plugin_service->protocol_plugin_signal_connection_opened(gp);
+
+ gpdata->thread = 0;
+ return NULL;
+}
+
+void remmina_plugin_ssh_vte_terminal_set_encoding_and_pty(VteTerminal *terminal, const char *codeset, int master, int slave)
+{
+ TRACE_CALL(__func__);
+ if (!remmina_masterthread_exec_is_main_thread()) {
+ /* Allow the execution of this function from a non main thread */
+ RemminaMTExecData *d;
+ d = (RemminaMTExecData *)g_malloc(sizeof(RemminaMTExecData));
+ d->func = FUNC_VTE_TERMINAL_SET_ENCODING_AND_PTY;
+ d->p.vte_terminal_set_encoding_and_pty.terminal = terminal;
+ d->p.vte_terminal_set_encoding_and_pty.codeset = codeset;
+ d->p.vte_terminal_set_encoding_and_pty.master = master;
+ remmina_masterthread_exec_and_wait(d);
+ g_free(d);
+ return;
+ }
+
+ setlocale(LC_ALL, "");
+ if (codeset && codeset[0] != '\0') {
+ }
+
+ vte_terminal_set_backspace_binding(terminal, VTE_ERASE_ASCII_DELETE);
+ vte_terminal_set_delete_binding(terminal, VTE_ERASE_DELETE_SEQUENCE);
+
+#if VTE_CHECK_VERSION(0, 38, 0)
+ /* vte_pty_new_foreign expect master FD, see https://bugzilla.gnome.org/show_bug.cgi?id=765382 */
+ vte_terminal_set_pty(terminal, vte_pty_new_foreign_sync(master, NULL, NULL));
+#else
+ vte_terminal_set_pty(terminal, master);
+#endif
+}
+
+static gboolean
+remmina_plugin_ssh_on_focus_in(GtkWidget *widget, GdkEventFocus *event, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+
+ gtk_widget_grab_focus(gpdata->vte);
+ return TRUE;
+}
+
+static gboolean
+remmina_plugin_ssh_on_size_allocate(GtkWidget *widget, GtkAllocation *alloc, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+ gint cols, rows;
+
+ if (!gtk_widget_get_mapped(widget)) return FALSE;
+
+ cols = vte_terminal_get_column_count(VTE_TERMINAL(widget));
+ rows = vte_terminal_get_row_count(VTE_TERMINAL(widget));
+
+ if (gpdata->shell)
+ remmina_ssh_shell_set_size(gpdata->shell, cols, rows);
+
+ return FALSE;
+}
+
+static void
+remmina_plugin_ssh_set_vte_pref(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+ RemminaFile *remminafile;
+
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ if (remmina_plugin_service->file_get_int(remminafile, "audiblebell", FALSE)) {
+ vte_terminal_set_audible_bell(VTE_TERMINAL(gpdata->vte), TRUE);
+ g_info("audible_bell set to %i", vte_terminal_get_audible_bell(VTE_TERMINAL(gpdata->vte)));
+ }
+ if (remmina_pref.vte_font && remmina_pref.vte_font[0]) {
+#if !VTE_CHECK_VERSION(0, 38, 0)
+ vte_terminal_set_font_from_string(VTE_TERMINAL(gpdata->vte), remmina_pref.vte_font);
+#else
+ vte_terminal_set_font(VTE_TERMINAL(gpdata->vte),
+ pango_font_description_from_string(remmina_pref.vte_font));
+#endif
+ }
+
+#if VTE_CHECK_VERSION(0, 51, 3)
+ REMMINA_DEBUG("Using vte_terminal_set_bold_is_bright instead of vte_terminal_set_allow_bold as deprecated");
+ vte_terminal_set_bold_is_bright(VTE_TERMINAL(gpdata->vte), remmina_pref.vte_allow_bold_text);
+#else
+ vte_terminal_set_allow_bold(VTE_TERMINAL(gpdata->vte), remmina_pref.vte_allow_bold_text);
+#endif
+ if (remmina_pref.vte_lines > 0)
+ vte_terminal_set_scrollback_lines(VTE_TERMINAL(gpdata->vte), remmina_pref.vte_lines);
+}
+
+void
+remmina_plugin_ssh_vte_select_all(GtkMenuItem *menuitem, gpointer vte)
+{
+ TRACE_CALL(__func__);
+ vte_terminal_select_all(VTE_TERMINAL(vte));
+ /** @todo we should add the vte_terminal_unselect_all as well */
+}
+
+void
+remmina_plugin_ssh_vte_decrease_font(GtkMenuItem *menuitem, gpointer vte)
+{
+ TRACE_CALL(__func__);
+ vte_terminal_set_font_scale(VTE_TERMINAL(vte), vte_terminal_get_font_scale(VTE_TERMINAL(vte)) - SCALE_FACTOR);
+}
+
+void
+remmina_plugin_ssh_vte_increase_font(GtkMenuItem *menuitem, gpointer vte)
+{
+ TRACE_CALL(__func__);
+ vte_terminal_set_font_scale(VTE_TERMINAL(vte), vte_terminal_get_font_scale(VTE_TERMINAL(vte)) + SCALE_FACTOR);
+}
+
+void
+remmina_plugin_ssh_vte_copy_clipboard(GtkMenuItem *menuitem, gpointer vte)
+{
+ TRACE_CALL(__func__);
+#if VTE_CHECK_VERSION(0, 50, 0)
+ vte_terminal_copy_clipboard_format(VTE_TERMINAL(vte), VTE_FORMAT_TEXT);
+#else
+ vte_terminal_copy_clipboard(VTE_TERMINAL(vte));
+#endif
+}
+
+void
+remmina_plugin_ssh_vte_paste_clipboard(GtkMenuItem *menuitem, gpointer vte)
+{
+ TRACE_CALL(__func__);
+ vte_terminal_paste_clipboard(VTE_TERMINAL(vte));
+}
+
+void
+remmina_plugin_ssh_vte_save_session(GtkMenuItem *menuitem, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+
+ GtkWidget *widget;
+ GError *err = NULL;
+
+ GFileOutputStream *stream = g_file_replace(gpdata->vte_session_file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &err);
+
+ if (err != NULL) {
+ // TRANSLATORS: %s is a placeholder for an error message
+ widget = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Error: %s"), err->message);
+ g_signal_connect(G_OBJECT(widget), "response", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_widget_show(widget);
+ return;
+ }
+
+ if (stream != NULL)
+#if VTE_CHECK_VERSION(0, 38, 0)
+ vte_terminal_write_contents_sync(VTE_TERMINAL(gpdata->vte), G_OUTPUT_STREAM(stream),
+ VTE_WRITE_DEFAULT, NULL, &err);
+#else
+ vte_terminal_write_contents(VTE_TERMINAL(gpdata->vte), G_OUTPUT_STREAM(stream),
+ VTE_TERMINAL_WRITE_DEFAULT, NULL, &err);
+#endif
+
+ if (err == NULL) {
+ remmina_public_send_notification("remmina-terminal-saved",
+ _("Terminal content saved in"),
+ g_file_get_path(gpdata->vte_session_file));
+ }
+
+ g_object_unref(stream);
+ g_free(err);
+}
+
+/** Send a keystroke to the plugin window */
+static void remmina_ssh_keystroke(RemminaProtocolWidget *gp, const guint keystrokes[], const gint keylen)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+
+ remmina_plugin_service->protocol_plugin_send_keys_signals(gpdata->vte,
+ keystrokes, keylen, GDK_KEY_PRESS | GDK_KEY_RELEASE);
+ return;
+}
+
+
+/* regex */
+
+static void jit_regex(VteRegex *regex, char const *pattern)
+{
+ TRACE_CALL(__func__);
+ GError *error;
+
+ if (!vte_regex_jit(regex, PCRE2_JIT_COMPLETE, &error) ||
+ !vte_regex_jit(regex, PCRE2_JIT_PARTIAL_SOFT, &error))
+ if (!g_error_matches(error, VTE_REGEX_ERROR, -45 /* PCRE2_ERROR_JIT_BADOPTION: JIT not supported */))
+ REMMINA_DEBUG("JITing regex “%s” failed: %s\n", pattern, error->message);
+}
+
+static VteRegex *compile_regex_for_search(char const *pattern, gboolean caseless, GError **error)
+{
+ TRACE_CALL(__func__);
+ uint32_t flags = PCRE2_UTF | PCRE2_NO_UTF_CHECK | PCRE2_MULTILINE;
+
+ if (caseless)
+ flags |= PCRE2_CASELESS;
+
+ VteRegex *regex = vte_regex_new_for_search(pattern, strlen(pattern), flags, error);
+
+ if (regex != NULL)
+ jit_regex(regex, pattern);
+
+ return regex;
+}
+
+static void
+remmina_search_widget_update_sensitivity(RemminaSshSearch *search_widget)
+{
+ TRACE_CALL(__func__);
+ gboolean can_search = search_widget->has_regex;
+
+ gtk_widget_set_sensitive(search_widget->search_next_button, can_search);
+ gtk_widget_set_sensitive(search_widget->search_prev_button, can_search);
+}
+
+static void
+remmina_search_widget_update_regex(RemminaPluginSshData *gpdata)
+{
+ TRACE_CALL(__func__);
+ GError *error = NULL;
+
+ RemminaSshSearch *search_widget = gpdata->search_widget;
+ char const *search_text = gtk_entry_get_text(GTK_ENTRY(search_widget->search_entry));
+ gboolean caseless = gtk_toggle_button_get_active(search_widget->match_case_checkbutton) == FALSE;
+
+ char *pattern;
+
+ if (gtk_toggle_button_get_active(search_widget->regex_checkbutton))
+ pattern = g_strdup(search_text);
+ else
+ pattern = g_regex_escape_string(search_text, -1);
+
+ if (gtk_toggle_button_get_active(search_widget->regex_checkbutton)) {
+ char *tmp = g_strdup_printf("\\b%s\\b", pattern);
+ g_free(pattern);
+ pattern = tmp;
+ }
+
+ if (caseless == search_widget->regex_caseless &&
+ g_strcmp0(pattern, search_widget->regex_pattern) == 0)
+ return;
+
+ search_widget->regex_caseless = caseless;
+ g_free(search_widget->regex_pattern);
+ search_widget->regex_pattern = NULL;
+
+ if (search_text[0] != '\0') {
+ REMMINA_DEBUG("Search text is: %s", search_text);
+ VteRegex *regex = compile_regex_for_search(pattern, caseless, &error);
+ vte_terminal_search_set_regex(VTE_TERMINAL(gpdata->vte), regex, 0);
+ if (regex != NULL)
+ vte_regex_unref(regex);
+
+ if (!error) {
+ search_widget->has_regex = TRUE;
+ search_widget->regex_pattern = pattern; /* adopt */
+ pattern = NULL; /* adopted */
+ gtk_widget_set_tooltip_text(search_widget->search_entry, NULL);
+ } else {
+ search_widget->has_regex = FALSE;
+ REMMINA_DEBUG("Regex not set, cannot search");
+ gtk_widget_set_tooltip_text(search_widget->search_entry, error->message);
+ }
+ }
+
+ g_free(pattern);
+ g_free(error);
+
+ remmina_search_widget_update_sensitivity(search_widget);
+}
+
+static void
+remmina_search_widget_wrap_around_toggled(GtkToggleButton *button, RemminaPluginSshData *gpdata)
+{
+ TRACE_CALL(__func__);
+
+ vte_terminal_search_set_wrap_around(VTE_TERMINAL(gpdata->vte), gtk_toggle_button_get_active(button));
+}
+
+static void
+remmina_search_widget_search_forward(RemminaPluginSshData *gpdata)
+{
+ TRACE_CALL(__func__);
+
+ RemminaSshSearch *search_sidget = gpdata->search_widget;
+
+ if (!search_sidget->has_regex)
+ return;
+ vte_terminal_search_find_next(VTE_TERMINAL(gpdata->vte));
+}
+
+static void
+remmina_search_widget_search_backward(RemminaPluginSshData *gpdata)
+{
+ TRACE_CALL(__func__);
+
+ RemminaSshSearch *search_sidget = gpdata->search_widget;
+
+ if (!search_sidget->has_regex)
+ return;
+ vte_terminal_search_find_previous(VTE_TERMINAL(gpdata->vte));
+}
+
+GtkWidget *remmina_plugin_pop_search_new(GtkWidget *relative_to, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+
+ gpdata->search_widget = g_new0(RemminaSshSearch, 1);
+ RemminaSshSearch *search_widget = gpdata->search_widget;
+
+ search_widget->regex_caseless = FALSE;
+ search_widget->has_regex = FALSE;
+ search_widget->regex_pattern = NULL;
+
+ search_widget->builder = remmina_public_gtk_builder_new_from_resource("/org/remmina/Remmina/src/../data/ui/remmina_search.glade");
+ search_widget->window = GTK_WIDGET(GET_OBJECT("RemminaSearchWidget"));
+ search_widget->search_entry = GTK_WIDGET(GET_OBJECT("search_entry"));
+ search_widget->search_prev_button = GTK_WIDGET(GET_OBJECT("search_prev_button"));
+ search_widget->search_next_button = GTK_WIDGET(GET_OBJECT("search_next_button"));
+ search_widget->close_button = GTK_WIDGET(GET_OBJECT("close_button"));
+ search_widget->match_case_checkbutton = GTK_TOGGLE_BUTTON(GET_OBJECT("match_case_checkbutton"));
+ search_widget->entire_word_checkbutton = GTK_TOGGLE_BUTTON(GET_OBJECT("entire_word_checkbutton"));
+ search_widget->regex_checkbutton = GTK_TOGGLE_BUTTON(GET_OBJECT("regex_checkbutton"));
+ search_widget->wrap_around_checkbutton = GTK_TOGGLE_BUTTON(GET_OBJECT("wrap_around_checkbutton"));
+ search_widget->reveal_button = GTK_WIDGET(GET_OBJECT("reveal_button"));
+ search_widget->revealer = GTK_WIDGET(GET_OBJECT("revealer"));
+
+ gtk_widget_set_can_default(search_widget->search_next_button, TRUE);
+ gtk_widget_grab_default(search_widget->search_next_button);
+
+ gtk_entry_set_activates_default(GTK_ENTRY(search_widget->search_entry), TRUE);
+
+ /* Connect signals */
+ gtk_builder_connect_signals(search_widget->builder, NULL);
+
+ g_signal_connect_swapped(search_widget->close_button, "clicked", G_CALLBACK(gtk_widget_destroy), GTK_WIDGET(search_widget->window));
+
+ g_object_bind_property(search_widget->reveal_button, "active",
+ search_widget->revealer, "reveal-child",
+ G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
+
+ g_signal_connect_swapped(search_widget->search_entry, "next-match", G_CALLBACK(remmina_search_widget_search_forward), gpdata);
+ g_signal_connect_swapped(search_widget->search_entry, "previous-match", G_CALLBACK(remmina_search_widget_search_backward), gpdata);
+ g_signal_connect_swapped(search_widget->search_entry, "search-changed", G_CALLBACK(remmina_search_widget_update_regex), gpdata);
+
+ g_signal_connect_swapped(search_widget->search_next_button, "clicked", G_CALLBACK(remmina_search_widget_search_forward), gpdata);
+ g_signal_connect_swapped(search_widget->search_prev_button, "clicked", G_CALLBACK(remmina_search_widget_search_backward), gpdata);
+
+ g_signal_connect_swapped(search_widget->match_case_checkbutton, "toggled", G_CALLBACK(remmina_search_widget_update_regex), gpdata);
+ g_signal_connect_swapped(search_widget->entire_word_checkbutton, "toggled", G_CALLBACK(remmina_search_widget_update_regex), gpdata);
+ g_signal_connect_swapped(search_widget->regex_checkbutton, "toggled", G_CALLBACK(remmina_search_widget_update_regex), gpdata);
+ g_signal_connect_swapped(search_widget->match_case_checkbutton, "toggled", G_CALLBACK(remmina_search_widget_update_regex), gpdata);
+
+ g_signal_connect(search_widget->wrap_around_checkbutton, "toggled", G_CALLBACK(remmina_search_widget_wrap_around_toggled), gpdata);
+
+ remmina_search_widget_update_sensitivity(search_widget);
+ return search_widget->window;
+}
+
+void remmina_plugin_pop_search(GtkMenuItem *menuitem, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+
+ GtkWindow *parent = NULL;
+
+ REMMINA_DEBUG("Before popover");
+ GtkWidget *window = remmina_plugin_pop_search_new(gpdata->vte, gp);
+ GtkWidget *toplevel = gtk_widget_get_toplevel(gpdata->vte);
+
+ if (GTK_IS_WINDOW(toplevel)) {
+ parent = GTK_WINDOW(toplevel);
+ gtk_window_set_transient_for(GTK_WINDOW(window), parent);
+ gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER_ON_PARENT);
+ }
+ gtk_widget_show(window);
+ REMMINA_DEBUG("After popover");
+}
+
+void remmina_plugin_ssh_call_sftp(GtkMenuItem *menuitem, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ remmina_protocol_widget_call_feature_by_type(gp, REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PROTOCOL_FEATURE_TOOL_SFTP);
+}
+
+gboolean
+remmina_ssh_plugin_popup_menu(GtkWidget *widget, GdkEvent *event, GtkWidget *menu)
+{
+ if ((event->type == GDK_BUTTON_PRESS) && (((GdkEventButton *)event)->button == 3)) {
+#if GTK_CHECK_VERSION(3, 22, 0)
+ gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL);
+#else
+ gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
+ ((GdkEventButton *)event)->button, gtk_get_current_event_time());
+#endif
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Remmina SSH plugin terminal popup menu.
+ *
+ * This is the context menu that popup when you right click in a terminal window.
+ * You can than select, copy, paste text and save the whole buffer to a file.
+ * Each menu entry call back the following functions:
+ * - remmina_plugin_ssh_vte_select_all()
+ * - remmina_plugin_ssh_vte_copy_clipboard()
+ * - remmina_plugin_ssh_vte_paste_clipboard()
+ * - remmina_plugin_ssh_vte_save_session()
+ * .
+ *
+ */
+void remmina_plugin_ssh_popup_ui(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+ /* Context menu for slection and clipboard */
+ GtkWidget *menu = gtk_menu_new();
+
+ GtkWidget *select_all = gtk_menu_item_new_with_label(_("Select All (host+A)"));
+ GtkWidget *copy = gtk_menu_item_new_with_label(_("Copy (host+C)"));
+ GtkWidget *paste = gtk_menu_item_new_with_label(_("Paste (host+V)"));
+ GtkWidget *save = gtk_menu_item_new_with_label(_("Save session to file"));
+ GtkWidget *font_incr = gtk_menu_item_new_with_label(_("Increase font size (host+Page Up)"));
+ GtkWidget *font_decr = gtk_menu_item_new_with_label(_("Decrease font size (host+Page Down)"));
+ GtkWidget *find_text = gtk_menu_item_new_with_label(_("Find text (host+G)"));
+ GtkWidget *sftp = gtk_menu_item_new_with_label(_("Open SFTP transfer…"));
+
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), select_all);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), copy);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), paste);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), save);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), font_incr);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), font_decr);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), find_text);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), sftp);
+
+ g_signal_connect(G_OBJECT(gpdata->vte), "button_press_event",
+ G_CALLBACK(remmina_ssh_plugin_popup_menu), menu);
+
+ g_signal_connect(G_OBJECT(select_all), "activate",
+ G_CALLBACK(remmina_plugin_ssh_vte_select_all), gpdata->vte);
+ g_signal_connect(G_OBJECT(copy), "activate",
+ G_CALLBACK(remmina_plugin_ssh_vte_copy_clipboard), gpdata->vte);
+ g_signal_connect(G_OBJECT(paste), "activate",
+ G_CALLBACK(remmina_plugin_ssh_vte_paste_clipboard), gpdata->vte);
+ g_signal_connect(G_OBJECT(save), "activate",
+ G_CALLBACK(remmina_plugin_ssh_vte_save_session), gp);
+ g_signal_connect(G_OBJECT(font_incr), "activate",
+ G_CALLBACK(remmina_plugin_ssh_vte_increase_font), gpdata->vte);
+ g_signal_connect(G_OBJECT(font_decr), "activate",
+ G_CALLBACK(remmina_plugin_ssh_vte_decrease_font), gpdata->vte);
+ g_signal_connect(G_OBJECT(find_text), "activate",
+ G_CALLBACK(remmina_plugin_pop_search), gp);
+ g_signal_connect(G_OBJECT(sftp), "activate",
+ G_CALLBACK(remmina_plugin_ssh_call_sftp), gp);
+
+ gtk_widget_show_all(menu);
+}
+
+static void
+remmina_plugin_ssh_eof(VteTerminal *vteterminal, RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+
+ RemminaFile *remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+
+ if (gpdata->closed)
+ return;
+
+ if (remmina_file_get_int(remminafile, "sshlogenabled", FALSE))
+ remmina_plugin_ssh_vte_save_session(NULL, gp);
+
+ gchar *server;
+ gint port;
+ remmina_plugin_service->get_server_port(remmina_plugin_service->file_get_string(remminafile, "server"),
+ 22,
+ &server,
+ &port);
+
+ REMMINA_AUDIT(_("Disconnected from %s:%d via SSH"), server, port);
+ g_free(server), server = NULL;
+ gpdata->closed = TRUE;
+}
+
+static gboolean
+remmina_plugin_ssh_close_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+
+ RemminaFile *remminafile;
+
+ REMMINA_DEBUG("Requesting to close the connection");
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+ if (remmina_file_get_int(remminafile, "sshlogenabled", FALSE))
+ remmina_plugin_ssh_vte_save_session(NULL, gp);
+ remmina_plugin_ssh_eof(VTE_TERMINAL(gpdata->vte), gp);
+
+ if (gpdata->thread) {
+ pthread_cancel(gpdata->thread);
+ if (gpdata->thread) pthread_join(gpdata->thread, NULL);
+ }
+ if (gpdata->shell) {
+ remmina_ssh_shell_free(gpdata->shell);
+ gpdata->shell = NULL;
+ }
+
+ remmina_plugin_service->protocol_plugin_signal_connection_closed(gp);
+ return FALSE;
+}
+
+/**
+ * Remmina SSH plugin initialization.
+ *
+ * This is the main function used to create the widget that will be embedded in the
+ * Remmina Connection Window.
+ * Initialize the terminal colours based on the user, everything is needed for the
+ * terminal window, the terminal session logging and the terminal popup menu.
+ *
+ * @see remmina_plugin_ssh_popup_ui
+ * @see RemminaProtocolWidget
+ * @see https://gitlab.com/Remmina/Remmina/wikis/Remmina-SSH-Terminal-colour-schemes
+ */
+static void
+remmina_plugin_ssh_init(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata;
+ RemminaFile *remminafile;
+ GtkWidget *hbox;
+ GtkAdjustment *vadjustment;
+ GtkWidget *vscrollbar;
+ GtkWidget *vte;
+ GdkRGBA foreground_color;
+ GdkRGBA background_color;
+ GdkRGBA palette[PALETTE_SIZE];
+
+
+#if !VTE_CHECK_VERSION(0, 38, 0)
+ GdkColor foreground_gdkcolor;
+ GdkColor background_gdkcolor;
+#endif /* VTE_CHECK_VERSION(0,38,0) */
+
+ gpdata = g_new0(RemminaPluginSshData, 1);
+ g_object_set_data_full(G_OBJECT(gp), "plugin-data", gpdata, g_free);
+
+ gpdata->closed = FALSE;
+
+ hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show(hbox);
+ gtk_container_add(GTK_CONTAINER(gp), hbox);
+ g_signal_connect(G_OBJECT(hbox), "focus-in-event", G_CALLBACK(remmina_plugin_ssh_on_focus_in), gp);
+
+ vte = vte_terminal_new();
+ //gtk_widget_show(vte);
+ vte_terminal_set_size(VTE_TERMINAL(vte), 80, 25);
+ vte_terminal_set_scroll_on_keystroke(VTE_TERMINAL(vte), TRUE);
+#if !VTE_CHECK_VERSION(0, 38, 0)
+ gdk_rgba_parse(&foreground_color, remmina_pref.color_pref.foreground);
+ gdk_rgba_parse(&background_color, remmina_pref.color_pref.background);
+#endif
+ remminafile = remmina_plugin_service->protocol_plugin_get_file(gp);
+
+
+#if VTE_CHECK_VERSION(0, 38, 0)
+ GdkRGBA cp[PALETTE_SIZE];
+ GdkRGBA cursor_color;
+ GdkRGBA cursor_foreground;
+ GdkRGBA highlight;
+ GdkRGBA highlight_foreground;
+ GdkRGBA colorBD;
+ unsigned int i = 0;
+
+ /*
+ * custom colors reside inside of the 'theme' subdir of the remmina config folder (.config/remmina/theme)
+ * with the file extension '.colors'. The name of the colorfile came from the menu (see below)
+ * sideeffect: It is possible to overwrite the standard colours with a dedicated colourfile like
+ * '0.colors' for GRUVBOX, '1.colors' for TANGO and so on
+ */
+ const gchar *color_name = remmina_plugin_service->file_get_string(remminafile, "ssh_color_scheme");
+
+ gchar *remmina_dir = g_build_path("/", g_get_user_config_dir(), "remmina", "theme", NULL);
+ gchar *remmina_colors_file = g_strdup_printf("%s/%s.colors", remmina_dir, color_name);
+ g_free(remmina_dir);
+
+ /*
+ * try to load theme from one of the system data dirs (based on XDG_DATA_DIRS environment var)
+ */
+ if (!g_file_test(remmina_colors_file, G_FILE_TEST_IS_REGULAR)) {
+ GError *error = NULL;
+ const gchar *const *dirs = g_get_system_data_dirs();
+
+ for (i = 0; dirs[i] != NULL; ++i) {
+ remmina_dir = g_build_path("/", dirs[i], "remmina", "theme", NULL);
+ GDir *system_data_dir = g_dir_open(remmina_dir, 0, &error);
+ // ignoring this error is OK, because the folder may not exist
+ if (error) {
+ g_error_free(error);
+ error = NULL;
+ } else {
+ if (system_data_dir) {
+ g_dir_close(system_data_dir);
+ g_free(remmina_colors_file);
+ remmina_colors_file = g_strdup_printf("%s/%s.colors", remmina_dir, color_name);
+ if (g_file_test(remmina_colors_file, G_FILE_TEST_IS_REGULAR))
+ break;
+ }
+ }
+ g_free(remmina_dir);
+ }
+ }
+
+ if (g_file_test(remmina_colors_file, G_FILE_TEST_IS_REGULAR)) {
+ GKeyFile *gkeyfile;
+ RemminaColorPref color_pref;
+
+ gkeyfile = g_key_file_new();
+ g_key_file_load_from_file(gkeyfile, remmina_colors_file, G_KEY_FILE_NONE, NULL);
+ remmina_pref_file_load_colors(gkeyfile, &color_pref);
+
+ REMMINA_DEBUG("Load custom theme for SSH teminal");
+ g_warn_if_fail(gdk_rgba_parse(&foreground_color, color_pref.foreground));
+ g_warn_if_fail(gdk_rgba_parse(&background_color, color_pref.background));
+ g_warn_if_fail(gdk_rgba_parse(&cursor_color, color_pref.cursor));
+ g_warn_if_fail(gdk_rgba_parse(&cursor_foreground, color_pref.cursor_foreground));
+ g_warn_if_fail(gdk_rgba_parse(&highlight, color_pref.highlight));
+ g_warn_if_fail(gdk_rgba_parse(&highlight_foreground, color_pref.highlight_foreground));
+ g_warn_if_fail(gdk_rgba_parse(&colorBD, color_pref.colorBD));
+
+ g_warn_if_fail(gdk_rgba_parse(&cp[0], color_pref.color0));
+ g_warn_if_fail(gdk_rgba_parse(&cp[1], color_pref.color1));
+ g_warn_if_fail(gdk_rgba_parse(&cp[2], color_pref.color2));
+ g_warn_if_fail(gdk_rgba_parse(&cp[3], color_pref.color3));
+ g_warn_if_fail(gdk_rgba_parse(&cp[4], color_pref.color4));
+ g_warn_if_fail(gdk_rgba_parse(&cp[5], color_pref.color5));
+ g_warn_if_fail(gdk_rgba_parse(&cp[6], color_pref.color6));
+ g_warn_if_fail(gdk_rgba_parse(&cp[7], color_pref.color7));
+ g_warn_if_fail(gdk_rgba_parse(&cp[8], color_pref.color8));
+ g_warn_if_fail(gdk_rgba_parse(&cp[9], color_pref.color9));
+ g_warn_if_fail(gdk_rgba_parse(&cp[10], color_pref.color10));
+ g_warn_if_fail(gdk_rgba_parse(&cp[11], color_pref.color11));
+ g_warn_if_fail(gdk_rgba_parse(&cp[12], color_pref.color12));
+ g_warn_if_fail(gdk_rgba_parse(&cp[13], color_pref.color13));
+ g_warn_if_fail(gdk_rgba_parse(&cp[14], color_pref.color14));
+ g_warn_if_fail(gdk_rgba_parse(&cp[15], color_pref.color15));
+
+ const GdkRGBA custom_palette[PALETTE_SIZE] = {
+ cp[0], cp[1], cp[2], cp[3],
+ cp[4], cp[5], cp[6], cp[7],
+ cp[8], cp[9], cp[10], cp[11],
+ cp[12], cp[13], cp[14], cp[15]
+ };
+
+ for (i = 0; i < PALETTE_SIZE; i++)
+ palette[i] = custom_palette[i];
+ } else {
+ /* Set colors to GdkRGBA */
+ switch (remmina_plugin_service->file_get_int(remminafile, "ssh_color_scheme", FALSE)) {
+ case LINUX:
+ gdk_rgba_parse(&foreground_color, "#ffffff");
+ gdk_rgba_parse(&background_color, "#000000");
+ gdk_rgba_parse(&cursor_color, "#ffffff");
+ gdk_rgba_parse(&cursor_foreground, "#00000");
+ gdk_rgba_parse(&highlight, "#ffffff");
+ gdk_rgba_parse(&highlight_foreground, "#00000");
+ gdk_rgba_parse(&colorBD, "#ffffff");
+ for (i = 0; i < PALETTE_SIZE; i++)
+ palette[i] = linux_palette[i];
+ break;
+ case TANGO:
+ gdk_rgba_parse(&foreground_color, "#ffffff");
+ gdk_rgba_parse(&background_color, "#000000");
+ gdk_rgba_parse(&cursor_color, "#000000");
+ gdk_rgba_parse(&cursor_foreground, "#ffffff");
+ gdk_rgba_parse(&highlight, "#ffffff");
+ gdk_rgba_parse(&highlight_foreground, "#00000");
+ gdk_rgba_parse(&colorBD, "#000000");
+ for (i = 0; i < PALETTE_SIZE; i++)
+ palette[i] = tango_palette[i];
+ break;
+ case GRUVBOX:
+ gdk_rgba_parse(&foreground_color, "#e6d4a3");
+ gdk_rgba_parse(&background_color, "#1e1e1e");
+ gdk_rgba_parse(&cursor_color, "#e6d4a3");
+ gdk_rgba_parse(&cursor_foreground, "#e6d4a3");
+ gdk_rgba_parse(&highlight, "#e6d4a3");
+ gdk_rgba_parse(&highlight_foreground, "#1e1e1e");
+ gdk_rgba_parse(&colorBD, "#ffffff");
+ for (i = 0; i < PALETTE_SIZE; i++)
+ palette[i] = gruvbox_palette[i];
+ break;
+ case SOLARIZED_DARK:
+ gdk_rgba_parse(&foreground_color, "#839496");
+ gdk_rgba_parse(&background_color, "#002b36");
+ gdk_rgba_parse(&cursor_color, "#93a1a1");
+ gdk_rgba_parse(&cursor_foreground, "#839496");
+ gdk_rgba_parse(&highlight, "#839496");
+ gdk_rgba_parse(&highlight_foreground, "#002b36");
+ gdk_rgba_parse(&colorBD, "#819090");
+ for (i = 0; i < PALETTE_SIZE; i++)
+ palette[i] = solarized_dark_palette[i];
+ break;
+ case SOLARIZED_LIGHT:
+ gdk_rgba_parse(&foreground_color, "#657b83");
+ gdk_rgba_parse(&background_color, "#fdf6e3");
+ gdk_rgba_parse(&cursor_color, "#586e75");
+ gdk_rgba_parse(&cursor_foreground, "#657b83");
+ gdk_rgba_parse(&highlight, "#657b83");
+ gdk_rgba_parse(&highlight_foreground, "#fdf6e3");
+ gdk_rgba_parse(&colorBD, "#475b62");
+ for (i = 0; i < PALETTE_SIZE; i++)
+ palette[i] = solarized_light_palette[i];
+ break;
+ case XTERM:
+ gdk_rgba_parse(&foreground_color, "#000000");
+ gdk_rgba_parse(&background_color, "#ffffff");
+ gdk_rgba_parse(&cursor_color, "#000000");
+ gdk_rgba_parse(&cursor_foreground, "#ffffff");
+ gdk_rgba_parse(&highlight, "#000000");
+ gdk_rgba_parse(&highlight_foreground, "#ffffff");
+ gdk_rgba_parse(&colorBD, "#000000");
+ for (i = 0; i < PALETTE_SIZE; i++)
+ palette[i] = xterm_palette[i];
+ break;
+ case CUSTOM:
+ REMMINA_DEBUG("Custom colors");
+ g_warn_if_fail(gdk_rgba_parse(&foreground_color, remmina_pref.color_pref.foreground));
+ g_warn_if_fail(gdk_rgba_parse(&background_color, remmina_pref.color_pref.background));
+ g_warn_if_fail(gdk_rgba_parse(&cursor_color, remmina_pref.color_pref.cursor));
+ g_warn_if_fail(gdk_rgba_parse(&cursor_foreground, remmina_pref.color_pref.cursor_foreground));
+ g_warn_if_fail(gdk_rgba_parse(&highlight, remmina_pref.color_pref.highlight));
+ g_warn_if_fail(gdk_rgba_parse(&highlight_foreground, remmina_pref.color_pref.highlight_foreground));
+ g_warn_if_fail(gdk_rgba_parse(&colorBD, remmina_pref.color_pref.colorBD));
+
+ g_warn_if_fail(gdk_rgba_parse(&cp[0], remmina_pref.color_pref.color0));
+ g_warn_if_fail(gdk_rgba_parse(&cp[1], remmina_pref.color_pref.color1));
+ g_warn_if_fail(gdk_rgba_parse(&cp[2], remmina_pref.color_pref.color2));
+ g_warn_if_fail(gdk_rgba_parse(&cp[3], remmina_pref.color_pref.color3));
+ g_warn_if_fail(gdk_rgba_parse(&cp[4], remmina_pref.color_pref.color4));
+ g_warn_if_fail(gdk_rgba_parse(&cp[5], remmina_pref.color_pref.color5));
+ g_warn_if_fail(gdk_rgba_parse(&cp[6], remmina_pref.color_pref.color6));
+ g_warn_if_fail(gdk_rgba_parse(&cp[7], remmina_pref.color_pref.color7));
+ g_warn_if_fail(gdk_rgba_parse(&cp[8], remmina_pref.color_pref.color8));
+ g_warn_if_fail(gdk_rgba_parse(&cp[9], remmina_pref.color_pref.color9));
+ g_warn_if_fail(gdk_rgba_parse(&cp[10], remmina_pref.color_pref.color10));
+ g_warn_if_fail(gdk_rgba_parse(&cp[11], remmina_pref.color_pref.color11));
+ g_warn_if_fail(gdk_rgba_parse(&cp[12], remmina_pref.color_pref.color12));
+ g_warn_if_fail(gdk_rgba_parse(&cp[13], remmina_pref.color_pref.color13));
+ g_warn_if_fail(gdk_rgba_parse(&cp[14], remmina_pref.color_pref.color14));
+ g_warn_if_fail(gdk_rgba_parse(&cp[15], remmina_pref.color_pref.color15));
+
+ const GdkRGBA custom_palette[PALETTE_SIZE] = {
+ cp[0], // remmina_pref.color_pref.color0
+ cp[1], // remmina_pref.color_pref.color1
+ cp[2], // remmina_pref.color_pref.color2
+ cp[3], // remmina_pref.color_pref.color3
+ cp[4], // remmina_pref.color_pref.color4
+ cp[5], // remmina_pref.color_pref.color5
+ cp[6], // remmina_pref.color_pref.color6
+ cp[7], // remmina_pref.color_pref.color7
+ cp[8], // remmina_pref.color_pref.color8
+ cp[9], // remmina_pref.color_pref.color9
+ cp[10], // remmina_pref.color_pref.color10
+ cp[11], // remmina_pref.color_pref.color11
+ cp[12], // remmina_pref.color_pref.color12
+ cp[13], // remmina_pref.color_pref.color13
+ cp[14], // remmina_pref.color_pref.color14
+ cp[15] // remmina_pref.color_pref.color15
+ };
+
+ for (i = 0; i < PALETTE_SIZE; i++)
+ palette[i] = custom_palette[i];
+ break;
+ default:
+ REMMINA_DEBUG("Linux paelette colors");
+ for (i = 0; i < PALETTE_SIZE; i++)
+ palette[i] = linux_palette[i];
+ break;
+ }
+ }
+ g_free(remmina_colors_file);
+ REMMINA_DEBUG("foreground_color.red %f, background_color.red %f", foreground_color.red, background_color.red);
+ REMMINA_DEBUG("foreground_color.blue %f, background_color.blue %f", foreground_color.blue, background_color.blue);
+ REMMINA_DEBUG("foreground_color.green %f, background_color.green %f", foreground_color.green, background_color.green);
+ REMMINA_DEBUG("foreground_color.alpha %f, background_color.alpha %f", foreground_color.alpha, background_color.alpha);
+ for (i = 0; i < PALETTE_SIZE; i++) {
+ REMMINA_DEBUG("index: %d, palette validation for red: %f", i, palette[i].red);
+ REMMINA_DEBUG("index: %d, palette validation for green: %f", i, palette[i].green);
+ REMMINA_DEBUG("index: %d, palette validation for blue: %f", i, palette[i].blue);
+ REMMINA_DEBUG("index: %d, palette validation for alpha: %f", i, palette[i].alpha);
+ g_warn_if_fail(valid_color(&palette[i]));
+ }
+ vte_terminal_set_colors(VTE_TERMINAL(vte), &foreground_color, &background_color, palette, PALETTE_SIZE);
+ vte_terminal_set_color_foreground(VTE_TERMINAL(vte), &foreground_color);
+ vte_terminal_set_color_background(VTE_TERMINAL(vte), &background_color);
+ vte_terminal_set_color_cursor(VTE_TERMINAL(vte), &cursor_color);
+ vte_terminal_set_color_cursor_foreground(VTE_TERMINAL(vte), &cursor_foreground);
+ vte_terminal_set_color_highlight(VTE_TERMINAL(vte), &highlight);
+ vte_terminal_set_color_highlight_foreground(VTE_TERMINAL(vte), &highlight_foreground);
+ vte_terminal_set_color_bold(VTE_TERMINAL(vte), &colorBD);
+
+#else
+ /* VTE <= 2.90 doesn’t support GdkRGBA so we must convert GdkRGBA to GdkColor */
+ foreground_gdkcolor.red = (guint16)(foreground_color.red * 0xFFFF);
+ foreground_gdkcolor.green = (guint16)(foreground_color.green * 0xFFFF);
+ foreground_gdkcolor.blue = (guint16)(foreground_color.blue * 0xFFFF);
+ background_gdkcolor.red = (guint16)(background_color.red * 0xFFFF);
+ background_gdkcolor.green = (guint16)(background_color.green * 0xFFFF);
+ background_gdkcolor.blue = (guint16)(background_color.blue * 0xFFFF);
+ /* Set colors to GdkColor */
+ vte_terminal_set_colors(VTE_TERMINAL(vte), &foreground_gdkcolor, &background_gdkcolor, NULL, 0);
+#endif
+
+ gtk_box_pack_start(GTK_BOX(hbox), vte, TRUE, TRUE, 0);
+ gpdata->vte = vte;
+ remmina_plugin_ssh_set_vte_pref(gp);
+
+ remmina_plugin_service->protocol_plugin_register_hostkey(gp, vte);
+
+#if VTE_CHECK_VERSION(0, 28, 0)
+ vadjustment = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(vte));
+#else
+ vadjustment = vte_terminal_get_adjustment(VTE_TERMINAL(vc->vte.terminal));
+#endif
+
+ vscrollbar = gtk_scrollbar_new(GTK_ORIENTATION_VERTICAL, vadjustment);
+
+ gtk_widget_show(vscrollbar);
+ gtk_box_pack_start(GTK_BOX(hbox), vscrollbar, FALSE, TRUE, 0);
+
+ const gchar *dir;
+ const gchar *sshlogname;
+ gchar *fp;
+
+ GFile *rf = g_file_new_for_path(remminafile->filename);
+
+ if (remmina_plugin_service->file_get_string(remminafile, "sshlogfolder") == NULL)
+ dir = g_build_path("/", g_get_user_cache_dir(), "remmina", NULL);
+ else
+ dir = remmina_plugin_service->file_get_string(remminafile, "sshlogfolder");
+
+ if (remmina_plugin_service->file_get_string(remminafile, "sshlogname") == NULL)
+ sshlogname = g_strconcat(g_file_get_basename(rf), ".", "log", NULL);
+ else
+ sshlogname = remmina_plugin_service->file_get_string(remminafile, "sshlogname");
+
+ sshlogname = remmina_file_format_properties(remminafile, sshlogname);
+
+ fp = g_strconcat(dir, "/", sshlogname, NULL);
+ g_free((gpointer) sshlogname);
+
+ gpdata->vte_session_file = g_file_new_for_path(fp);
+ g_free(fp);
+
+ g_signal_connect(G_OBJECT(vte), "size-allocate", G_CALLBACK(remmina_plugin_ssh_on_size_allocate), gp);
+ g_signal_connect(G_OBJECT(vte), "unrealize", G_CALLBACK(remmina_plugin_ssh_eof), gp);
+ g_signal_connect(G_OBJECT(vte), "eof", G_CALLBACK(remmina_plugin_ssh_eof), gp);
+ g_signal_connect(G_OBJECT(vte), "child-exited", G_CALLBACK(remmina_plugin_ssh_eof), gp);
+ remmina_plugin_ssh_popup_ui(gp);
+ gtk_widget_show_all(hbox);
+}
+
+/**
+ * Initialize the main window properties and the pthread.
+ *
+ * The call of this function is a requirement of remmina_protocol_widget_open_connection_real().
+ * @return TRUE
+ * @return FALSE and remmina_protocol_widget_open_connection_real() will fails.
+ */
+static gboolean
+remmina_plugin_ssh_open_connection(RemminaProtocolWidget *gp)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+
+ remmina_plugin_service->protocol_plugin_set_expand(gp, TRUE);
+ remmina_plugin_service->protocol_plugin_set_width(gp, 640);
+ remmina_plugin_service->protocol_plugin_set_height(gp, 480);
+
+ if (pthread_create(&gpdata->thread, NULL, remmina_plugin_ssh_main_thread, gp)) {
+ remmina_plugin_service->protocol_plugin_set_error(gp,
+ "Failed to initialize pthread. Falling back to non-thread mode…");
+ gpdata->thread = 0;
+ return FALSE;
+ } else {
+ return TRUE;
+ }
+ return TRUE;
+}
+
+/**
+ * Not used by the plugin.
+ *
+ * @return Always TRUE
+ */
+static gboolean
+remmina_plugin_ssh_query_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ return TRUE;
+}
+
+/**
+ * Functions to call when an entry in the Tool menu in the Remmina Connection Window is clicked.
+ *
+ * In the Remmina Connection Window toolbar, there is a tool menu, this function is used to
+ * call the right function for each entry with its parameters.
+ *
+ * At the moment it’s possible to:
+ * - Open a new SSH session.
+ * - Open an SFTP session.
+ * - Select, copy and paste text.
+ * - Send recorded Key Strokes.
+ * .
+ *
+ * @return the return value of the calling function.
+ *
+ */
+static void
+remmina_plugin_ssh_call_feature(RemminaProtocolWidget *gp, const RemminaProtocolFeature *feature)
+{
+ TRACE_CALL(__func__);
+ RemminaPluginSshData *gpdata = GET_PLUGIN_DATA(gp);
+
+ switch (feature->id) {
+ case REMMINA_PROTOCOL_FEATURE_TOOL_SSH:
+ remmina_plugin_service->open_connection(
+ remmina_file_dup_temp_protocol(remmina_plugin_service->protocol_plugin_get_file(gp), "SSH"),
+ NULL, gpdata->shell, NULL);
+ return;
+ case REMMINA_PROTOCOL_FEATURE_TOOL_SFTP:
+ remmina_plugin_service->open_connection(
+ /** @todo start the direct tunnel here */
+ remmina_file_dup_temp_protocol(remmina_plugin_service->protocol_plugin_get_file(gp), "SFTP"),
+ NULL, gpdata->shell, NULL);
+ return;
+ case REMMINA_PLUGIN_SSH_FEATURE_TOOL_COPY:
+#if VTE_CHECK_VERSION(0, 50, 0)
+ vte_terminal_copy_clipboard_format(VTE_TERMINAL(gpdata->vte), VTE_FORMAT_TEXT);
+#else
+ vte_terminal_copy_clipboard(VTE_TERMINAL(gpdata->vte));
+#endif
+ return;
+ case REMMINA_PLUGIN_SSH_FEATURE_TOOL_PASTE:
+ vte_terminal_paste_clipboard(VTE_TERMINAL(gpdata->vte));
+ return;
+ case REMMINA_PLUGIN_SSH_FEATURE_TOOL_SELECT_ALL:
+ vte_terminal_select_all(VTE_TERMINAL(gpdata->vte));
+ return;
+ case REMMINA_PLUGIN_SSH_FEATURE_TOOL_INCREASE_FONT:
+ vte_terminal_set_font_scale(VTE_TERMINAL(gpdata->vte),
+ vte_terminal_get_font_scale(VTE_TERMINAL(gpdata->vte)) + SCALE_FACTOR);
+ return;
+ case REMMINA_PLUGIN_SSH_FEATURE_TOOL_DECREASE_FONT:
+ vte_terminal_set_font_scale(VTE_TERMINAL(gpdata->vte),
+ vte_terminal_get_font_scale(VTE_TERMINAL(gpdata->vte)) - SCALE_FACTOR);
+ return;
+ case REMMINA_PLUGIN_SSH_FEATURE_TOOL_SEARCH:
+ remmina_plugin_pop_search(NULL, gp);
+ return;
+ }
+}
+
+/** Array of key/value pairs for SSH auth type
+ * libssh methods:
+ *
+ * #define SSH_AUTH_METHOD_UNKNOWN 0x0000u
+ * #define SSH_AUTH_METHOD_NONE 0x0001u
+ * #define SSH_AUTH_METHOD_PASSWORD 0x0002u
+ * #define SSH_AUTH_METHOD_PUBLICKEY 0x0004u
+ * #define SSH_AUTH_METHOD_HOSTBASED 0x0008u
+ * #define SSH_AUTH_METHOD_INTERACTIVE 0x0010u
+ * #define SSH_AUTH_METHOD_GSSAPI_MIC 0x0020u
+ */
+static gpointer ssh_auth[] =
+{
+ "0", N_("Password"),
+ "1", N_("SSH identity file"),
+ "2", N_("SSH agent"),
+ "3", N_("Public key (automatic)"),
+ "4", N_("Kerberos (GSSAPI)"),
+ NULL
+};
+
+/** Charset list */
+static gpointer ssh_charset_list[] =
+{
+ "", "",
+ "", "ASCII",
+ "", "BIG5",
+ "", "CP437",
+ "", "CP720",
+ "", "CP737",
+ "", "CP775",
+ "", "CP850",
+ "", "CP852",
+ "", "CP855",
+ "", "CP857",
+ "", "CP858",
+ "", "CP862",
+ "", "CP866",
+ "", "CP874",
+ "", "CP1125",
+ "", "CP1250",
+ "", "CP1251",
+ "", "CP1252",
+ "", "CP1253",
+ "", "CP1254",
+ "", "CP1255",
+ "", "CP1256",
+ "", "CP1257",
+ "", "CP1258",
+ "", "EUC-JP",
+ "", "EUC-KR",
+ "", "GBK",
+ "", "ISO 8859-1",
+ "", "ISO 8859-2",
+ "", "ISO 8859-3",
+ "", "ISO 8859-4",
+ "", "ISO 8859-5",
+ "", "ISO 8859-6",
+ "", "ISO 8859-7",
+ "", "ISO 8859-8",
+ "", "ISO 8859-9",
+ "", "ISO 8859-10",
+ "", "ISO 8859-11",
+ "", "ISO 8859-12",
+ "", "ISO 8859-13",
+ "", "ISO 8859-14",
+ "", "ISO 8859-15",
+ "", "ISO 8859-16",
+ "", "KOI8-R",
+ "", "SJIS",
+ "", "UTF-8",
+ NULL
+};
+
+static gpointer ssh_terminal_palette[] =
+{
+ "0", "Linux",
+ "1", "Tango",
+ "2", "Gruvbox",
+ "3", "Solarized Dark",
+ "4", "Solarized Light",
+ "5", "XTerm",
+ "6", "Custom (Configured in Remmina preferences)",
+ NULL, NULL
+};
+
+/**
+ * Array for available features.
+ * The last element of the array must be REMMINA_PROTOCOL_FEATURE_TYPE_END.
+ */
+static RemminaProtocolFeature remmina_plugin_ssh_features[] =
+{
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_SSH_FEATURE_TOOL_COPY, N_("Copy"), N_("_Copy"), NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_SSH_FEATURE_TOOL_PASTE, N_("Paste"), N_("_Paste"), NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_SSH_FEATURE_TOOL_SELECT_ALL, N_("Select all"), N_("_Select all"), NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_SSH_FEATURE_TOOL_INCREASE_FONT, N_("Increase font size"), N_("_Increase font size"), NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_SSH_FEATURE_TOOL_DECREASE_FONT, N_("Decrease font size"), N_("_Decrease font size"), NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PLUGIN_SSH_FEATURE_TOOL_SEARCH, N_("Find text"), N_("_Find text"), NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_TOOL, REMMINA_PROTOCOL_FEATURE_TOOL_SFTP, N_("Open SFTP transfer…"), "folder-remote", NULL },
+ { REMMINA_PROTOCOL_FEATURE_TYPE_END, 0, NULL, NULL, 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 remmina_ssh_basic_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_SERVER, "server", NULL, FALSE, "_ssh._tcp", NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "ssh_auth", N_("Authentication type"), FALSE, ssh_auth, NULL, 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_FILE, "ssh_privatekey", N_("SSH identity file"), FALSE, NULL, NULL, NULL, NULL },
+#if LIBSSH_VERSION_INT >= SSH_VERSION_INT(0, 9, 0)
+ { REMMINA_PROTOCOL_SETTING_TYPE_FILE, "ssh_certfile", N_("SSH certificate file"), FALSE, NULL, NULL, NULL, NULL },
+#endif
+ { REMMINA_PROTOCOL_SETTING_TYPE_PASSWORD, "ssh_passphrase", N_("Password to unlock private key"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "run_line", N_("Opening command"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "exec", N_("Start-up background program"), FALSE, NULL, NULL, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL, NULL, NULL }
+};
+
+static gchar log_tips[] =
+ N_("The filename can use the following placeholders:\n\n"
+ " • %h is substituted with the server name\n"
+ " • %t is substituted with the SSH server name\n"
+ " • %u is substituted with the username\n"
+ " • %U is substituted with the SSH username\n"
+ " • %p is substituted with Remmina profile name\n"
+ " • %g is substituted with Remmina profile group name\n"
+ " • %d is substituted with local date and time in ISO 8601 format\n");
+
+/**
+ * Array of RemminaProtocolSetting for advanced settings.
+ * - Each item is composed by:
+ * 1. RemminaProtocolSettingType for setting type.
+ * 2. Setting name.
+ * 3. Setting description.
+ * 4. Compact disposition.
+ * 5. Values for REMMINA_PROTOCOL_SETTING_TYPE_SELECT or REMMINA_PROTOCOL_SETTING_TYPE_COMBO.
+ * 6. Setting Tooltip.
+ *
+ */
+static const RemminaProtocolSetting remmina_ssh_advanced_settings[] =
+{
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "ssh_color_scheme", N_("Terminal colour scheme"), FALSE, ssh_terminal_palette, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_SELECT, "ssh_charset", N_("Character set"), FALSE, ssh_charset_list, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "ssh_proxycommand", N_("SSH Proxy Command"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "ssh_kex_algorithms", N_("KEX (Key Exchange) algorithms"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "ssh_ciphers", N_("Symmetric cipher client to server"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "ssh_hostkeytypes", N_("Preferred server host key types"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "sshlogfolder", N_("Folder for SSH session log"), FALSE, NULL, N_("Full path of an existing folder") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_TEXT, "sshlogname", N_("Filename for SSH session log"), FALSE, NULL, log_tips },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "sshlogenabled", N_("Log SSH session when exiting Remmina"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "sshsavesession", N_("Log SSH session asynchronously"), TRUE, NULL, N_("Saving the session asynchronously may have a notable performance impact") },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "audiblebell", N_("Audible terminal bell"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "ssh_forward_x11", N_("SSH X11 Forwarding"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "ssh_compression", N_("SSH compression"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "disablepasswordstoring", N_("Don't remember passwords"), TRUE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_CHECK, "ssh_stricthostkeycheck", N_("Strict host key checking"), FALSE, NULL, NULL },
+ { REMMINA_PROTOCOL_SETTING_TYPE_END, NULL, NULL, FALSE, NULL, NULL }
+};
+
+/**
+ * SSH Protocol plugin definition and features.
+ *
+ * Array used to define the SSH Protocol plugin Type, name, description, version
+ * Plugin icon, features, initialization and closing functions.
+ */
+static RemminaProtocolPlugin remmina_plugin_ssh =
+{
+ REMMINA_PLUGIN_TYPE_PROTOCOL, /**< Type */
+ "SSH", /**< Name */
+ N_("SSH - Secure Shell"), /**< Description */
+ GETTEXT_PACKAGE, /**< Translation domain */
+ VERSION, /**< Version number */
+ "org.remmina.Remmina-ssh-symbolic", /**< Icon for normal connection */
+ "org.remmina.Remmina-ssh-symbolic", /**< Icon for SSH connection */
+ remmina_ssh_basic_settings, /**< Array for basic settings */
+ remmina_ssh_advanced_settings, /**< Array for advanced settings */
+ REMMINA_PROTOCOL_SSH_SETTING_TUNNEL, /**< SSH settings type */
+ remmina_plugin_ssh_features, /**< Array for available features */
+ remmina_plugin_ssh_init, /**< Plugin initialization */
+ remmina_plugin_ssh_open_connection, /**< Plugin open connection */
+ remmina_plugin_ssh_close_connection, /**< Plugin close connection */
+ remmina_plugin_ssh_query_feature, /**< Query for available features */
+ remmina_plugin_ssh_call_feature, /**< Call a feature */
+ remmina_ssh_keystroke, /**< Send a keystroke */
+ NULL, /**< No screenshot support available */
+ NULL, /**< RCW map event */
+ NULL /**< RCW unmap event */
+};
+
+
+/*
+ * this function is used for
+ * - inserting into the list to became a sorted list [g_list_insert_sorted()]
+ * - checking the list to avoid duplicate entries [g_list_find_custom()]
+ */
+static gint
+compare(gconstpointer a, gconstpointer b)
+{
+ return strcmp((gchar *)a, (gchar *)b);
+}
+
+void
+remmina_ssh_plugin_load_terminal_palettes(gpointer *ssh_terminal_palette_new)
+{
+ unsigned int preset_rec_size = sizeof(ssh_terminal_palette) / sizeof(gpointer);
+
+ GError *error = NULL;
+ GList *files = NULL;
+ unsigned int rec_size = 0;
+ /*
+ * count number of (all) files to reserve enough memory
+ */
+ /* /usr/local/share/remmina */
+ const gchar *const *dirs = g_get_system_data_dirs();
+
+ unsigned int i = 0;
+
+ for (i = 0; dirs[i] != NULL; ++i) {
+ GDir *system_data_dir = NULL;
+ gchar *remmina_dir = g_build_path("/", dirs[i], "remmina", "theme", NULL);
+ system_data_dir = g_dir_open(remmina_dir, 0, &error);
+ g_free(remmina_dir);
+ // ignoring this error is OK, because the folder may not exist
+ if (error) {
+ g_error_free(error);
+ error = NULL;
+ } else {
+ if (system_data_dir) {
+ const gchar *filename;
+ while ((filename = g_dir_read_name(system_data_dir))) {
+ if (!g_file_test(filename, G_FILE_TEST_IS_DIR)) {
+ if (g_str_has_suffix(filename, ".colors")) {
+ gsize len = strrchr(filename, '.') - filename;
+ gchar *menu_str = g_strndup(filename, len);
+ if (g_list_find_custom(files, menu_str, compare) == NULL)
+ files = g_list_insert_sorted(files, menu_str, compare);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /* ~/.config/remmina/colors */
+ gchar *remmina_dir = g_build_path("/", g_get_user_config_dir(), "remmina", "theme", NULL);
+ GDir *user_data_dir;
+
+ user_data_dir = g_dir_open(remmina_dir, 0, &error);
+ g_free(remmina_dir);
+ if (error) {
+ g_error_free(error);
+ error = NULL;
+ } else {
+ if (user_data_dir) {
+ const gchar *filename;
+ while ((filename = g_dir_read_name(user_data_dir))) {
+ if (!g_file_test(filename, G_FILE_TEST_IS_DIR)) {
+ if (g_str_has_suffix(filename, ".colors")) {
+ char *menu_str = g_malloc(strlen(filename) + 1);
+ strcpy(menu_str, filename);
+ char *t2 = strrchr(menu_str, '.');
+ t2[0] = 0;
+ if (g_list_find_custom(files, menu_str, compare) == NULL)
+ files = g_list_insert_sorted(files, menu_str, compare);
+ }
+ }
+ }
+ }
+ }
+
+ rec_size = g_list_length(files) * 2;
+
+ gpointer *color_palette = g_malloc((preset_rec_size + rec_size) * sizeof(gpointer));
+
+ unsigned int field_idx = 0;
+
+ *ssh_terminal_palette_new = color_palette;
+ // preset with (old) static ssh_terminal_palette data
+ for (; field_idx < preset_rec_size; field_idx++) {
+ color_palette[field_idx] = ssh_terminal_palette[field_idx];
+ if (!color_palette[field_idx])
+ break;
+ }
+
+ GList *l_files = NULL;
+
+ for (l_files = g_list_first(files); l_files != NULL; l_files = l_files->next) {
+ gchar *menu_str = (gchar *)l_files->data;
+
+ color_palette[field_idx++] = menu_str;
+ color_palette[field_idx++] = menu_str;
+ }
+ g_list_free(files);
+
+ color_palette[field_idx] = NULL;
+}
+
+void
+remmina_ssh_plugin_register(void)
+{
+ TRACE_CALL(__func__);
+ remmina_plugin_ssh_features[0].opt3 = GUINT_TO_POINTER(remmina_pref.vte_shortcutkey_copy);
+ remmina_plugin_ssh_features[1].opt3 = GUINT_TO_POINTER(remmina_pref.vte_shortcutkey_paste);
+ remmina_plugin_ssh_features[2].opt3 = GUINT_TO_POINTER(remmina_pref.vte_shortcutkey_select_all);
+ remmina_plugin_ssh_features[3].opt3 = GUINT_TO_POINTER(remmina_pref.vte_shortcutkey_increase_font);
+ remmina_plugin_ssh_features[4].opt3 = GUINT_TO_POINTER(remmina_pref.vte_shortcutkey_decrease_font);
+ remmina_plugin_ssh_features[5].opt3 = GUINT_TO_POINTER(remmina_pref.vte_shortcutkey_search_text);
+
+ remmina_plugin_service = &remmina_plugin_manager_service;
+
+ RemminaProtocolSettingOpt *settings;
+
+ // preset new settings with (old) static remmina_ssh_advanced_settings data
+#if GLIB_CHECK_VERSION(2,68,0)
+ settings = g_memdup2(remmina_ssh_advanced_settings, sizeof(remmina_ssh_advanced_settings));
+#else
+ settings = g_memdup(remmina_ssh_advanced_settings, sizeof(remmina_ssh_advanced_settings));
+#endif
+
+ // create dynamic advanced settings to made replacing of ssh_terminal_palette possible
+ gpointer ssh_terminal_palette_new = NULL;
+
+ remmina_ssh_plugin_load_terminal_palettes(&ssh_terminal_palette_new);
+
+ settings[0].opt1 = ssh_terminal_palette_new;
+ remmina_plugin_ssh.advanced_settings = (RemminaProtocolSetting *)settings;
+
+ remmina_plugin_service->register_plugin((RemminaPlugin *)&remmina_plugin_ssh);
+
+ ssh_threads_set_callbacks(ssh_threads_get_pthread());
+ ssh_init();
+}
+
+#else
+
+void remmina_ssh_plugin_register(void)
+{
+ TRACE_CALL(__func__);
+}
+
+#endif
diff --git a/src/remmina_ssh_plugin.h b/src/remmina_ssh_plugin.h
new file mode 100644
index 0000000..b02a982
--- /dev/null
+++ b/src/remmina_ssh_plugin.h
@@ -0,0 +1,67 @@
+/*
+ * 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
+
+#ifdef HAVE_LIBVTE
+#include <vte/vte.h>
+#endif
+
+G_BEGIN_DECLS
+
+void remmina_ssh_plugin_register(void);
+
+typedef struct _RemminaProtocolSettingOpt {
+ RemminaProtocolSettingType type;
+ const gchar * name;
+ const gchar * label;
+ gboolean compact;
+ gpointer opt1;
+ gpointer opt2;
+} RemminaProtocolSettingOpt;
+
+/* For callback in main thread */
+#if defined (HAVE_LIBSSH) && defined (HAVE_LIBVTE)
+void remmina_plugin_ssh_vte_terminal_set_encoding_and_pty(VteTerminal *terminal, const char *codeset, int master, int slave);
+void remmina_plugin_ssh_vte_select_all(GtkMenuItem *menuitem, gpointer vte);
+void remmina_plugin_ssh_vte_copy_clipboard(GtkMenuItem *menuitem, gpointer vte);
+void remmina_plugin_ssh_vte_paste_clipboard(GtkMenuItem *menuitem, gpointer vte);
+void remmina_plugin_ssh_vte_decrease_font(GtkMenuItem *menuitem, gpointer vte);
+void remmina_plugin_ssh_vte_increase_font(GtkMenuItem *menuitem, gpointer vte);
+gboolean remmina_ssh_plugin_popup_menu(GtkWidget *widget, GdkEvent *event, GtkWidget *menu);
+#endif
+
+G_END_DECLS
diff --git a/src/remmina_string_array.c b/src/remmina_string_array.c
new file mode 100644
index 0000000..e4c41e6
--- /dev/null
+++ b/src/remmina_string_array.c
@@ -0,0 +1,176 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <glib.h>
+#include <gmodule.h>
+#include <string.h>
+#include "remmina_string_array.h"
+#include "remmina/remmina_trace_calls.h"
+
+RemminaStringArray*
+remmina_string_array_new(void)
+{
+ TRACE_CALL(__func__);
+ return g_ptr_array_new();
+}
+
+RemminaStringArray*
+remmina_string_array_new_from_string(const gchar *strs)
+{
+ TRACE_CALL(__func__);
+ RemminaStringArray *array;
+ gchar *buf, *ptr1, *ptr2;
+
+ array = remmina_string_array_new();
+ if (!strs || strs[0] == '\0')
+ return array;
+
+ buf = g_strdup(strs);
+ ptr1 = buf;
+ while (ptr1) {
+ ptr2 = strchr(ptr1, ',');
+ if (ptr2)
+ *ptr2++ = '\0';
+ remmina_string_array_add(array, ptr1);
+ ptr1 = ptr2;
+ }
+
+ g_free(buf);
+
+ return array;
+}
+
+RemminaStringArray*
+remmina_string_array_new_from_allocated_string(gchar *strs)
+{
+ TRACE_CALL(__func__);
+ RemminaStringArray *array;
+ array = remmina_string_array_new_from_string(strs);
+ g_free(strs);
+ return array;
+}
+
+void remmina_string_array_add(RemminaStringArray* array, const gchar *str)
+{
+ TRACE_CALL(__func__);
+ g_ptr_array_add(array, g_strdup(str));
+}
+
+gint remmina_string_array_find(RemminaStringArray* array, const gchar *str)
+{
+ TRACE_CALL(__func__);
+ gint i;
+
+ for (i = 0; i < array->len; i++) {
+ if (g_strcmp0(remmina_string_array_index(array, i), str) == 0)
+ return i;
+ }
+ return -1;
+}
+
+void remmina_string_array_remove_index(RemminaStringArray* array, gint i)
+{
+ TRACE_CALL(__func__);
+ g_ptr_array_remove_index(array, i);
+}
+
+void remmina_string_array_remove(RemminaStringArray* array, const gchar *str)
+{
+ TRACE_CALL(__func__);
+ gint i;
+
+ i = remmina_string_array_find(array, str);
+ if (i >= 0) {
+ remmina_string_array_remove_index(array, i);
+ }
+}
+
+void remmina_string_array_intersect(RemminaStringArray* array, const gchar *dest_strs)
+{
+ TRACE_CALL(__func__);
+ RemminaStringArray *dest_array;
+ gint i, j;
+
+ dest_array = remmina_string_array_new_from_string(dest_strs);
+
+ i = 0;
+ while (i < array->len) {
+ j = remmina_string_array_find(dest_array, remmina_string_array_index(array, i));
+ if (j < 0) {
+ remmina_string_array_remove_index(array, i);
+ continue;
+ }
+ i++;
+ }
+
+ remmina_string_array_free(dest_array);
+}
+
+static gint remmina_string_array_compare_func(const gchar **a, const gchar **b)
+{
+ TRACE_CALL(__func__);
+ return g_strcmp0(*a, *b);
+}
+
+void remmina_string_array_sort(RemminaStringArray *array)
+{
+ TRACE_CALL(__func__);
+ g_ptr_array_sort(array, (GCompareFunc)remmina_string_array_compare_func);
+}
+
+gchar*
+remmina_string_array_to_string(RemminaStringArray* array)
+{
+ TRACE_CALL(__func__);
+ GString *gstr;
+ gint i;
+
+ gstr = g_string_new("");
+ for (i = 0; i < array->len; i++) {
+ if (i > 0)
+ g_string_append_c(gstr, ',');
+ g_string_append(gstr, remmina_string_array_index(array, i));
+ }
+ return g_string_free(gstr, FALSE);
+}
+
+void remmina_string_array_free(RemminaStringArray *array)
+{
+ TRACE_CALL(__func__);
+ g_ptr_array_foreach(array, (GFunc)g_free, NULL);
+ g_ptr_array_free(array, TRUE);
+}
+
diff --git a/src/remmina_string_array.h b/src/remmina_string_array.h
new file mode 100644
index 0000000..5b93e03
--- /dev/null
+++ b/src/remmina_string_array.h
@@ -0,0 +1,56 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <gmodule.h>
+
+G_BEGIN_DECLS
+
+typedef GPtrArray RemminaStringArray;
+
+RemminaStringArray *remmina_string_array_new(void);
+#define remmina_string_array_index(array, i) (gchar *)g_ptr_array_index(array, i)
+RemminaStringArray *remmina_string_array_new_from_string(const gchar *strs);
+RemminaStringArray *remmina_string_array_new_from_allocated_string(gchar *strs);
+void remmina_string_array_add(RemminaStringArray *array, const gchar *str);
+gint remmina_string_array_find(RemminaStringArray *array, const gchar *str);
+void remmina_string_array_remove_index(RemminaStringArray *array, gint i);
+void remmina_string_array_remove(RemminaStringArray *array, const gchar *str);
+void remmina_string_array_intersect(RemminaStringArray *array, const gchar *dest_strs);
+void remmina_string_array_sort(RemminaStringArray *array);
+gchar *remmina_string_array_to_string(RemminaStringArray *array);
+void remmina_string_array_free(RemminaStringArray *array);
+
+G_END_DECLS
diff --git a/src/remmina_string_list.c b/src/remmina_string_list.c
new file mode 100644
index 0000000..0fafacb
--- /dev/null
+++ b/src/remmina_string_list.c
@@ -0,0 +1,312 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <string.h>
+#include "config.h"
+#include "remmina_public.h"
+#include "remmina_string_list.h"
+#include "remmina/remmina_trace_calls.h"
+
+static RemminaStringList *string_list;
+
+#define COLUMN_DESCRIPTION 0
+#define COLUMN_VALUE 1
+#define GET_OBJECT(object_name) gtk_builder_get_object(string_list->builder, object_name)
+
+/* Update the buttons state on the items in the TreeModel */
+void remmina_string_list_update_buttons_state(void)
+{
+ gint items_count = gtk_tree_model_iter_n_children(
+ GTK_TREE_MODEL(string_list->liststore_items), NULL);
+
+ gtk_widget_set_sensitive(GTK_WIDGET(string_list->button_remove), items_count > 0);
+ gtk_widget_set_sensitive(GTK_WIDGET(string_list->button_up), items_count > 1);
+ gtk_widget_set_sensitive(GTK_WIDGET(string_list->button_down), items_count > 1);
+}
+
+/* Check the text inserted in the list */
+void remmina_string_list_on_cell_edited(GtkCellRendererText *cell, const gchar *path_string, const gchar *new_text)
+{
+ TRACE_CALL(__func__);
+ gchar *text;
+ gchar *error;
+ GtkTreePath *path = gtk_tree_path_new_from_string(path_string);
+ GtkTreeIter iter;
+
+ gtk_tree_model_get_iter(GTK_TREE_MODEL(string_list->liststore_items), &iter, path);
+ /* Remove delimitors from the string */
+ text = remmina_public_str_replace(new_text, STRING_DELIMITOR, " ");
+ if (cell == string_list->cellrenderertext_item1) {
+ gtk_list_store_set(string_list->liststore_items, &iter, COLUMN_DESCRIPTION, text, -1);
+ }else {
+ /* Check for validation only in second field */
+ if (string_list->priv->validation_func) {
+ if (!((*string_list->priv->validation_func)(text, &error))) {
+ gtk_label_set_text(string_list->label_status, error);
+ gtk_widget_show(GTK_WIDGET(string_list->label_status));
+ g_free(error);
+ }else {
+ gtk_widget_hide(GTK_WIDGET(string_list->label_status));
+ }
+ }
+ gtk_list_store_set(string_list->liststore_items, &iter, COLUMN_VALUE, text, -1);
+ }
+ gtk_tree_path_free(path);
+ g_free(text);
+}
+
+/* Move a TreeIter position */
+static void remmina_string_list_move_iter(GtkTreeIter *from, GtkTreeIter *to)
+{
+ TRACE_CALL(__func__);
+ GtkTreePath *path;
+
+ gtk_list_store_swap(string_list->liststore_items, from, to);
+ path = gtk_tree_model_get_path(GTK_TREE_MODEL(string_list->liststore_items), from);
+ gtk_tree_view_scroll_to_cell(string_list->treeview_items, path, NULL, 0, 0, 0);
+ gtk_tree_path_free(path);
+}
+
+/* Move down the selected TreeRow */
+void remmina_string_list_on_action_down(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+ GtkTreeIter target_iter;
+
+ if (gtk_tree_selection_get_selected(string_list->treeview_selection, NULL, &iter)) {
+ gtk_tree_selection_get_selected(string_list->treeview_selection, NULL, &target_iter);
+ if (gtk_tree_model_iter_next(GTK_TREE_MODEL(string_list->liststore_items), &target_iter)) {
+ remmina_string_list_move_iter(&iter, &target_iter);
+ }
+ }
+}
+
+/* Move up the selected TreeRow */
+void remmina_string_list_on_action_up(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+ GtkTreeIter target_iter;
+ GtkTreePath *path;
+
+ if (gtk_tree_selection_get_selected(string_list->treeview_selection, NULL, &iter)) {
+ gtk_tree_selection_get_selected(string_list->treeview_selection, NULL, &target_iter);
+ path = gtk_tree_model_get_path(GTK_TREE_MODEL(string_list->liststore_items), &target_iter);
+ /* Before moving the TreeRow check if there’s a previous item */
+ if (gtk_tree_path_prev(path)) {
+ gtk_tree_model_get_iter(GTK_TREE_MODEL(string_list->liststore_items), &target_iter, path);
+ gtk_tree_path_free(path);
+ remmina_string_list_move_iter(&iter, &target_iter);
+ }
+ }
+}
+
+/* Add a new TreeRow to the list */
+void remmina_string_list_on_action_add(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+ GtkTreePath *path;
+
+ gtk_list_store_append(string_list->liststore_items, &iter);
+ gtk_tree_selection_select_iter(string_list->treeview_selection, &iter);
+
+ path = gtk_tree_model_get_path(GTK_TREE_MODEL(string_list->liststore_items), &iter);
+ gtk_tree_view_set_cursor_on_cell(string_list->treeview_items, path,
+ string_list->treeviewcolumn_item,
+ GTK_CELL_RENDERER(string_list->priv->two_columns ? string_list->cellrenderertext_item1 : string_list->cellrenderertext_item2),
+ TRUE);
+ gtk_tree_path_free(path);
+ remmina_string_list_update_buttons_state();
+}
+
+/* Remove the selected TreeRow from the list */
+void remmina_string_list_on_action_remove(GtkWidget *widget, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+
+ if (gtk_tree_selection_get_selected(string_list->treeview_selection, NULL, &iter)) {
+ gtk_list_store_remove(string_list->liststore_items, &iter);
+ }
+ gtk_widget_hide(GTK_WIDGET(string_list->label_status));
+ remmina_string_list_update_buttons_state();
+}
+
+/* Load a string list by splitting a string value */
+void remmina_string_list_set_text(const gchar *text, const gboolean clear_data)
+{
+ TRACE_CALL(__func__);
+ GtkTreeIter iter;
+ gchar **items;
+ gchar **values;
+ gint i;
+ /* Clear the data before to load new items */
+ if (clear_data)
+ gtk_list_store_clear(string_list->liststore_items);
+ /* Split the string and insert each snippet in the string list */
+ items = g_strsplit(text, STRING_DELIMITOR, -1);
+ for (i = 0; i < g_strv_length(items); i++) {
+ values = g_strsplit(items[i], string_list->priv->fields_separator, -1);
+ gtk_list_store_append(string_list->liststore_items, &iter);
+ if (g_strv_length(values) > 1) {
+ /* Two columns data */
+ gtk_list_store_set(string_list->liststore_items, &iter,
+ COLUMN_DESCRIPTION, values[0],
+ COLUMN_VALUE, values[1],
+ -1);
+ }else {
+ /* Single column data */
+ gtk_list_store_set(string_list->liststore_items, &iter,
+ COLUMN_VALUE, values[0],
+ -1);
+ }
+ g_strfreev(values);
+ }
+ g_strfreev(items);
+ remmina_string_list_update_buttons_state();
+}
+
+/* Get a string value representing the string list */
+gchar* remmina_string_list_get_text(void)
+{
+ TRACE_CALL(__func__);
+ GString *str;
+ GtkTreeIter iter;
+ gboolean first;
+ gboolean ret;
+ const gchar *item_description;
+ const gchar *item_value;
+
+ str = g_string_new(NULL);
+ first = TRUE;
+ /* Cycle each GtkTreeIter in the ListStore */
+ ret = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(string_list->liststore_items), &iter);
+ while (ret) {
+ gtk_tree_model_get(GTK_TREE_MODEL(string_list->liststore_items), &iter,
+ COLUMN_DESCRIPTION, &item_description,
+ COLUMN_VALUE, &item_value,
+ -1);
+ if (!item_description)
+ item_description = "";
+ if (item_value && strlen(item_value) > 0) {
+ /* Add a delimitor after the first element */
+ if (!first) {
+ g_string_append(str, STRING_DELIMITOR);
+ }else {
+ first = FALSE;
+ }
+ /* Add the description for two columns list */
+ if (string_list->priv->two_columns) {
+ g_string_append(str, item_description);
+ g_string_append(str, string_list->priv->fields_separator);
+ }
+ /* Add the element to the string */
+ g_string_append(str, item_value);
+ }
+ ret = gtk_tree_model_iter_next(GTK_TREE_MODEL(string_list->liststore_items), &iter);
+ }
+ return g_string_free(str, FALSE);
+}
+
+/* Set a function that will be used to validate the new rows */
+void remmina_string_list_set_validation_func(RemminaStringListValidationFunc func)
+{
+ TRACE_CALL(__func__);
+ string_list->priv->validation_func = func;
+}
+
+/* Set the dialog titles */
+void remmina_string_list_set_titles(gchar *title1, gchar *title2)
+{
+ /* Set dialog titlebar */
+ gtk_window_set_title(GTK_WINDOW(string_list->dialog),
+ (title1 && strlen(title1) > 0) ? title1 : "");
+ /* Set title label */
+ if (title2 && strlen(title2) > 0) {
+ gtk_label_set_text(string_list->label_title, title2);
+ gtk_widget_show(GTK_WIDGET(string_list->label_title));
+ }else {
+ gtk_widget_hide(GTK_WIDGET(string_list->label_title));
+ }
+}
+
+/* RemminaStringList initialization */
+static void remmina_string_list_init(void)
+{
+ TRACE_CALL(__func__);
+ string_list->priv->validation_func = NULL;
+ /* When two columns are requested, show also the first column */
+ if (string_list->priv->two_columns)
+ gtk_cell_renderer_set_visible(GTK_CELL_RENDERER(string_list->cellrenderertext_item1), TRUE);
+ remmina_string_list_update_buttons_state();
+}
+
+/* RemminaStringList instance */
+GtkDialog* remmina_string_list_new(gboolean two_columns, const gchar *fields_separator)
+{
+ TRACE_CALL(__func__);
+ string_list = g_new0(RemminaStringList, 1);
+ string_list->priv = g_new0(RemminaStringListPriv, 1);
+
+ string_list->builder = remmina_public_gtk_builder_new_from_resource("/org/remmina/Remmina/src/../data/ui/remmina_string_list.glade");
+ string_list->dialog = GTK_DIALOG(gtk_builder_get_object(string_list->builder, "DialogStringList"));
+
+ string_list->liststore_items = GTK_LIST_STORE(GET_OBJECT("liststore_items"));
+ string_list->treeview_items = GTK_TREE_VIEW(GET_OBJECT("treeview_items"));
+ string_list->treeviewcolumn_item = GTK_TREE_VIEW_COLUMN(GET_OBJECT("treeviewcolumn_item"));
+ string_list->treeview_selection = GTK_TREE_SELECTION(GET_OBJECT("treeview_selection"));
+ string_list->cellrenderertext_item1 = GTK_CELL_RENDERER_TEXT(GET_OBJECT("cellrenderertext_item1"));
+ string_list->cellrenderertext_item2 = GTK_CELL_RENDERER_TEXT(GET_OBJECT("cellrenderertext_item2"));
+ string_list->button_add = GTK_BUTTON(GET_OBJECT("button_add"));
+ string_list->button_remove = GTK_BUTTON(GET_OBJECT("button_remove"));
+ string_list->button_up = GTK_BUTTON(GET_OBJECT("button_up"));
+ string_list->button_down = GTK_BUTTON(GET_OBJECT("button_down"));
+ string_list->label_title = GTK_LABEL(GET_OBJECT("label_title"));
+ string_list->label_status = GTK_LABEL(GET_OBJECT("label_status"));
+
+ /* Connect signals */
+ gtk_builder_connect_signals(string_list->builder, NULL);
+ /* Initialize the window and load the values */
+ if (!fields_separator)
+ fields_separator = STRING_DELIMITOR2;
+ string_list->priv->fields_separator = fields_separator;
+ string_list->priv->two_columns = two_columns;
+ remmina_string_list_init();
+
+ return string_list->dialog;
+}
diff --git a/src/remmina_string_list.h b/src/remmina_string_list.h
new file mode 100644
index 0000000..5d33c52
--- /dev/null
+++ b/src/remmina_string_list.h
@@ -0,0 +1,84 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <gtk/gtk.h>
+
+typedef gboolean (*RemminaStringListValidationFunc)(const gchar *new_str, gchar **error);
+
+typedef struct _RemminaStringListPriv {
+ RemminaStringListValidationFunc validation_func;
+ const gchar * fields_separator;
+ gboolean two_columns;
+} RemminaStringListPriv;
+
+typedef struct _RemminaStringList {
+ GtkBuilder * builder;
+ GtkDialog * dialog;
+
+ GtkListStore * liststore_items;
+ GtkTreeView * treeview_items;
+ GtkTreeViewColumn * treeviewcolumn_item;
+ GtkTreeSelection * treeview_selection;
+ GtkCellRendererText * cellrenderertext_item1;
+ GtkCellRendererText * cellrenderertext_item2;
+
+ GtkButton * button_add;
+ GtkButton * button_remove;
+ GtkButton * button_up;
+ GtkButton * button_down;
+
+ GtkLabel * label_title;
+ GtkLabel * label_status;
+
+ RemminaStringListPriv * priv;
+} RemminaStringList;
+
+G_BEGIN_DECLS
+
+/* RemminaStringList instance */
+GtkDialog *remmina_string_list_new(gboolean two_columns, const gchar *fields_separator);
+/* Load a string list by splitting a string value */
+void remmina_string_list_set_text(const gchar *text, const gboolean clear_data);
+/* Get a string value representing the string list */
+gchar *remmina_string_list_get_text(void);
+/* Set the dialog titles */
+void remmina_string_list_set_titles(gchar *title1, gchar *title2);
+/* Set a function that will be used to validate the new rows */
+void remmina_string_list_set_validation_func(RemminaStringListValidationFunc func);
+
+G_END_DECLS
diff --git a/src/remmina_sysinfo.c b/src/remmina_sysinfo.c
new file mode 100644
index 0000000..cfbe1c3
--- /dev/null
+++ b/src/remmina_sysinfo.c
@@ -0,0 +1,155 @@
+/*
+ * 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 "config.h"
+#include <glib/gi18n.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_sysinfo.h"
+
+gboolean remmina_sysinfo_is_appindicator_available()
+{
+ /* Check if we have an appindicator available (which uses
+ * DBUS KDE StatusNotifier)
+ */
+
+ TRACE_CALL(__func__);
+ GDBusConnection *con;
+ GVariant *v;
+ GError *error;
+ gboolean available;
+
+ available = FALSE;
+ con = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
+ if (con) {
+ error = NULL;
+ v = g_dbus_connection_call_sync(con,
+ "org.kde.StatusNotifierWatcher",
+ "/StatusNotifierWatcher",
+ "org.freedesktop.DBus.Introspectable",
+ "Introspect",
+ NULL,
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ &error);
+ if (v) {
+ available = TRUE;
+ g_variant_unref(v);
+ }
+ g_object_unref(con);
+ }
+ return available;
+}
+
+/**
+ * Query DBUS to get GNOME Shell version.
+ * @return the GNOME Shell version as a string or NULL if error or no GNOME Shell found.
+ * @warning The returned string must be freed with g_free.
+ */
+gchar *remmina_sysinfo_get_gnome_shell_version()
+{
+ TRACE_CALL(__func__);
+ GDBusConnection *con;
+ GDBusProxy *p;
+ GVariant *v;
+ GError *error;
+ gsize sz;
+ gchar *ret;
+
+ ret = NULL;
+
+ con = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
+ if (con) {
+ error = NULL;
+ p = g_dbus_proxy_new_sync(con,
+ G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START,
+ NULL,
+ "org.gnome.Shell",
+ "/org/gnome/Shell",
+ "org.gnome.Shell",
+ NULL,
+ &error);
+ if (p) {
+ v = g_dbus_proxy_get_cached_property(p, "ShellVersion");
+ if (v) {
+ if (g_variant_is_of_type(v, G_VARIANT_TYPE_STRING)) {
+ ret = g_strdup(g_variant_get_string(v, &sz));
+ }
+ g_variant_unref(v);
+ }
+ g_object_unref(p);
+ }
+ g_object_unref(con);
+ }
+ return ret;
+}
+
+/**
+ * Query environment variables to get the Window manager name.
+ * @return a string composed by XDG_CURRENT_DESKTOP and GDMSESSION as a string
+ * or \0 if nothing has been found.
+ * @warning The returned string must be freed with g_free.
+ */
+gchar *remmina_sysinfo_get_wm_name()
+{
+ TRACE_CALL(__func__);
+ const gchar *xdg_current_desktop;
+ const gchar *gdmsession;
+ gchar *ret;
+
+ xdg_current_desktop = g_environ_getenv(g_get_environ(), "XDG_CURRENT_DESKTOP");
+ gdmsession = g_environ_getenv(g_get_environ(), "GDMSESSION");
+
+ if (!xdg_current_desktop || xdg_current_desktop[0] == '\0') {
+ if (!gdmsession || gdmsession[0] == '\0') {
+ ret = NULL;
+ }else {
+ ret = g_strdup_printf("%s", gdmsession);
+ }
+ }else if (!gdmsession || gdmsession[0] == '\0') {
+ ret = g_strdup_printf("%s", xdg_current_desktop);
+ return ret;
+ }else if (g_strcmp0(xdg_current_desktop,gdmsession) == 0) {
+ ret = g_strdup_printf("%s", xdg_current_desktop);
+ }else {
+ ret = g_strdup_printf("%s %s", xdg_current_desktop, gdmsession);
+ }
+ return ret;
+}
diff --git a/src/remmina_sysinfo.h b/src/remmina_sysinfo.h
new file mode 100644
index 0000000..7aa51fe
--- /dev/null
+++ b/src/remmina_sysinfo.h
@@ -0,0 +1,45 @@
+/*
+ * 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+gboolean remmina_sysinfo_is_appindicator_available(void);
+gchar *remmina_sysinfo_get_gnome_shell_version(void);
+gchar *remmina_sysinfo_get_wm_name(void);
+
+G_END_DECLS
diff --git a/src/remmina_unlock.c b/src/remmina_unlock.c
new file mode 100644
index 0000000..d05000f
--- /dev/null
+++ b/src/remmina_unlock.c
@@ -0,0 +1,238 @@
+/*
+ * 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 <stdlib.h>
+
+#include <glib/gi18n.h>
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "config.h"
+#include "remmina_sodium.h"
+#include "remmina_pref.h"
+#include "remmina_log.h"
+#include "remmina_unlock.h"
+#include "remmina_passwd.h"
+#include "remmina_public.h"
+#include "remmina/remmina_trace_calls.h"
+
+#if SODIUM_VERSION_INT >= 90200
+static RemminaUnlockDialog *remmina_unlock_dialog;
+#define GET_OBJ(object_name) gtk_builder_get_object(remmina_unlock_dialog->builder, object_name)
+
+GTimer *timer;
+gboolean unlocked;
+
+static void remmina_unlock_timer_init()
+{
+ TRACE_CALL(__func__);
+
+ timer = g_timer_new();
+ REMMINA_DEBUG("Validity timer for Remmina password started");
+}
+
+static void remmina_unlock_timer_reset(gpointer user_data)
+{
+ TRACE_CALL(__func__);
+
+ g_timer_reset(timer);
+ REMMINA_DEBUG("Validity timer for Remmina password reset");
+}
+
+void remmina_unlock_timer_destroy()
+{
+ TRACE_CALL(__func__);
+
+ g_timer_destroy(timer);
+ timer = NULL;
+}
+
+static void remmina_unlock_unlock_clicked(GtkButton *btn, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ //g_timer_reset(NULL);
+
+ gchar *unlock_password;
+ const gchar *entry_passwd;
+ gint rc;
+
+ unlock_password = remmina_pref_get_value("unlock_password");
+ entry_passwd = gtk_entry_get_text(remmina_unlock_dialog->entry_unlock);
+ rc = remmina_sodium_pwhash_str_verify(unlock_password, entry_passwd);
+ //REMMINA_DEBUG("remmina_sodium_pwhash_str_verify returned %i", rc);
+
+ if (rc == 0) {
+ REMMINA_DEBUG("Password verified");
+ //unlocked = FALSE;
+ remmina_unlock_dialog->retval = TRUE;
+ } else {
+ REMMINA_WARNING ("The password is wrong. Reset it by editing the remmina.pref file by hand");
+ remmina_unlock_timer_reset(NULL);
+ remmina_unlock_dialog->retval = FALSE;
+ }
+ gtk_dialog_response(remmina_unlock_dialog->dialog, GTK_RESPONSE_ACCEPT);
+}
+
+static void remmina_unlock_cancel_clicked(GtkButton *btn, gpointer user_data)
+{
+ TRACE_CALL(__func__);
+ gtk_dialog_response(remmina_unlock_dialog->dialog, GTK_RESPONSE_CANCEL);
+}
+
+gint remmina_unlock_new(GtkWindow *parent)
+{
+ TRACE_CALL(__func__);
+
+ gdouble unlock_timeout;
+ gdouble elapsed = 0.0;
+ gboolean lock = FALSE;
+ gboolean rc;
+
+ /* We don't have a timer, so this is the first time
+ * or the timer has been destroyed
+ */
+ if (timer == NULL) {
+ remmina_unlock_timer_init();
+ unlocked = FALSE;
+ }
+
+ /* We have a timer, we get the elapsed time since its start */
+ if (timer != NULL)
+ elapsed = g_timer_elapsed(timer, NULL);
+
+ /* We stop the timer while we enter the password) */
+ if (timer) g_timer_stop(timer);
+
+ unlock_timeout = remmina_pref.unlock_timeout;
+ REMMINA_DEBUG ("unlock_timeout = %f", unlock_timeout);
+ REMMINA_DEBUG ("elapsed = %f", elapsed);
+ REMMINA_DEBUG ("INT unlock_timeout = %d", (int)unlock_timeout);
+ REMMINA_DEBUG ("INT elapsed = %d", (int)elapsed);
+ /* always LOCK and ask password */
+ if ((elapsed) && ((int)unlock_timeout - elapsed) <= 0) {
+ unlocked = FALSE;
+ remmina_unlock_timer_reset(NULL);
+ }
+ /* We don't lock as it has been already requested */
+ //if (!unlocked && ((int)unlock_timeout - elapsed) > 0) unlocked = TRUE;
+
+ REMMINA_DEBUG("Based on settings and current status, the unlocking dialog is set to %d", unlocked);
+
+ if (unlocked) {
+ REMMINA_DEBUG ("No need to request a password");
+ rc = TRUE;
+ return rc;
+ } else {
+
+ remmina_unlock_dialog = g_new0(RemminaUnlockDialog, 1);
+ remmina_unlock_dialog->retval = FALSE;
+
+ remmina_unlock_dialog->builder = remmina_public_gtk_builder_new_from_resource("/org/remmina/Remmina/src/../data/ui/remmina_unlock.glade");
+ remmina_unlock_dialog->dialog = GTK_DIALOG(gtk_builder_get_object(remmina_unlock_dialog->builder, "RemminaUnlockDialog"));
+ if (parent)
+ gtk_window_set_transient_for(GTK_WINDOW(remmina_unlock_dialog->dialog), parent);
+
+ remmina_unlock_dialog->entry_unlock = GTK_ENTRY(GET_OBJ("entry_unlock"));
+ gtk_entry_set_activates_default(GTK_ENTRY(remmina_unlock_dialog->entry_unlock), TRUE);
+ remmina_unlock_dialog->button_unlock = GTK_BUTTON(GET_OBJ("button_unlock"));
+ gtk_widget_set_can_default(GTK_WIDGET(remmina_unlock_dialog->button_unlock), TRUE);
+ gtk_widget_grab_default(GTK_WIDGET(remmina_unlock_dialog->button_unlock));
+ remmina_unlock_dialog->button_unlock_cancel = GTK_BUTTON(GET_OBJ("button_unlock_cancel"));
+
+ g_signal_connect(remmina_unlock_dialog->button_unlock, "clicked",
+ G_CALLBACK(remmina_unlock_unlock_clicked), (gpointer)remmina_unlock_dialog);
+ g_signal_connect(remmina_unlock_dialog->button_unlock_cancel, "clicked",
+ G_CALLBACK(remmina_unlock_cancel_clicked), (gpointer)remmina_unlock_dialog);
+ g_signal_connect (remmina_unlock_dialog->dialog, "close",
+ G_CALLBACK (remmina_unlock_cancel_clicked), (gpointer)remmina_unlock_dialog);
+
+ /* Connect signals */
+ gtk_builder_connect_signals(remmina_unlock_dialog->builder, NULL);
+
+ g_object_set_data_full (G_OBJECT(remmina_unlock_dialog->dialog), "builder", remmina_unlock_dialog->builder, g_object_unref);
+
+ gchar *unlock_password = NULL;
+ unlock_password = g_strdup(remmina_pref_get_value("unlock_password"));
+ //REMMINA_DEBUG ("Password from preferences is: %s", unlock_password);
+ if ((unlock_password == NULL) || (g_strcmp0(unlock_password, "") == 0)) {
+ if (remmina_passwd (GTK_WINDOW(remmina_unlock_dialog->dialog), &unlock_password)) {
+ //REMMINA_DEBUG ("Password is: %s", unlock_password);
+ remmina_pref_set_value("unlock_password", g_strdup(unlock_password));
+ remmina_unlock_dialog->retval = TRUE;
+ } else {
+ remmina_unlock_dialog->retval = FALSE;
+ }
+ }
+
+ int result = GTK_RESPONSE_NONE;
+ if (g_strcmp0(unlock_password, "") != 0) {
+ result = gtk_dialog_run (GTK_DIALOG (remmina_unlock_dialog->dialog));
+ } else
+ remmina_unlock_dialog->retval = lock;
+
+ switch (result)
+ {
+ case GTK_RESPONSE_ACCEPT:
+ if (!remmina_unlock_dialog->retval)
+ REMMINA_DEBUG ("Wrong password");
+ else {
+ REMMINA_DEBUG ("Correct password. Unlocking…");
+ unlocked = TRUE;
+ }
+ REMMINA_DEBUG ("retval: %d", remmina_unlock_dialog->retval);
+ break;
+ default:
+ //unlocked = FALSE;
+ remmina_unlock_dialog->retval = FALSE;
+ remmina_unlock_timer_destroy ();
+ REMMINA_DEBUG ("Password not requested. Return value: %d", remmina_unlock_dialog->retval);
+ break;
+ }
+
+ rc = remmina_unlock_dialog->retval;
+
+ g_free(unlock_password), unlock_password = NULL;
+ gtk_widget_destroy(GTK_WIDGET(remmina_unlock_dialog->dialog));
+ remmina_unlock_dialog->dialog = NULL;
+ }
+ if (timer) g_timer_start(timer);
+ return(rc);
+}
+
+#else
+gint remmina_unlock_new(GtkWindow *parent)
+{
+ return 1;
+}
+#endif
diff --git a/src/remmina_unlock.h b/src/remmina_unlock.h
new file mode 100644
index 0000000..f0c4a1d
--- /dev/null
+++ b/src/remmina_unlock.h
@@ -0,0 +1,63 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * 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 <sodium.h>
+#include <gtk/gtk.h>
+
+#if SODIUM_VERSION_INT >= 90200
+typedef struct _RemminaUnlockDialog {
+ GtkBuilder * builder;
+ GtkDialog * dialog;
+
+ GtkEntry * entry_unlock;
+ GtkButton * button_unlock;
+ GtkButton * button_unlock_cancel;
+
+ gboolean unlock_init;
+
+ gint retval;
+} RemminaUnlockDialog;
+
+extern GTimer *timer;
+extern gboolean unlocked;
+
+#endif
+
+G_BEGIN_DECLS
+
+gint remmina_unlock_new(GtkWindow *parent);
+
+G_END_DECLS
diff --git a/src/remmina_utils.c b/src/remmina_utils.c
new file mode 100644
index 0000000..14c0283
--- /dev/null
+++ b/src/remmina_utils.c
@@ -0,0 +1,1121 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-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.
+ *
+ */
+
+/**
+ * General utility functions, non-GTK related.
+ */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/utsname.h>
+#include <locale.h>
+
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+#include <openssl/evp.h>
+#include <openssl/decoder.h>
+#include <openssl/core_names.h>
+#include <openssl/rsa.h>
+#include <openssl/err.h>
+#include "remmina_sodium.h"
+#include "remmina/remmina_trace_calls.h"
+#include "remmina_utils.h"
+#include "remmina_log.h"
+#include "remmina_plugin_manager.h"
+
+/** Returns @c TRUE if @a ptr is @c NULL or @c *ptr is @c FALSE. */
+#define EMPTY(ptr) \
+ (!(ptr) || !*(ptr))
+
+/* Copyright (C) 1998 VMware, Inc. All rights reserved.
+ * Some of the code in this file is taken from the VMware open client.
+ */
+typedef struct lsb_distro_info {
+ gchar * name;
+ gchar * scanstring;
+} LSBDistroInfo;
+
+/*
+ * static LSBDistroInfo lsbFields[] = {
+ * { "DISTRIB_ID=", "DISTRIB_ID=%s" },
+ * { "DISTRIB_RELEASE=", "DISTRIB_RELEASE=%s" },
+ * { "DISTRIB_CODENAME=", "DISTRIB_CODENAME=%s" },
+ * { "DISTRIB_DESCRIPTION=", "DISTRIB_DESCRIPTION=%s" },
+ * { NULL, NULL },
+ * };
+ */
+
+typedef struct distro_info {
+ gchar * name;
+ gchar * filename;
+} DistroInfo;
+
+static DistroInfo distroArray[] = {
+ { "RedHat", "/etc/redhat-release" },
+ { "RedHat", "/etc/redhat_version" },
+ { "Sun", "/etc/sun-release" },
+ { "SuSE", "/etc/SuSE-release" },
+ { "SuSE", "/etc/novell-release" },
+ { "SuSE", "/etc/sles-release" },
+ { "SuSE", "/etc/os-release" },
+ { "Debian", "/etc/debian_version" },
+ { "Debian", "/etc/debian_release" },
+ { "Ubuntu", "/etc/lsb-release" },
+ { "Mandrake", "/etc/mandrake-release" },
+ { "Mandriva", "/etc/mandriva-release" },
+ { "Mandrake", "/etc/mandrakelinux-release" },
+ { "TurboLinux", "/etc/turbolinux-release" },
+ { "Fedora Core", "/etc/fedora-release" },
+ { "Gentoo", "/etc/gentoo-release" },
+ { "Novell", "/etc/nld-release" },
+ { "Annvix", "/etc/annvix-release" },
+ { "Arch", "/etc/arch-release" },
+ { "Arklinux", "/etc/arklinux-release" },
+ { "Aurox", "/etc/aurox-release" },
+ { "BlackCat", "/etc/blackcat-release" },
+ { "Cobalt", "/etc/cobalt-release" },
+ { "Conectiva", "/etc/conectiva-release" },
+ { "Immunix", "/etc/immunix-release" },
+ { "Knoppix", "/etc/knoppix_version" },
+ { "Linux-From-Scratch", "/etc/lfs-release" },
+ { "Linux-PPC", "/etc/linuxppc-release" },
+ { "MkLinux", "/etc/mklinux-release" },
+ { "PLD", "/etc/pld-release" },
+ { "Slackware", "/etc/slackware-version" },
+ { "Slackware", "/etc/slackware-release" },
+ { "SMEServer", "/etc/e-smith-release" },
+ { "Solaris", "/etc/release" },
+ { "Solus", "/etc/solus-release" },
+ { "Tiny Sofa", "/etc/tinysofa-release" },
+ { "UltraPenguin", "/etc/ultrapenguin-release" },
+ { "UnitedLinux", "/etc/UnitedLinux-release" },
+ { "VALinux", "/etc/va-release" },
+ { "Yellow Dog", "/etc/yellowdog-release" },
+ { NULL, NULL },
+};
+
+const char *remmina_RSA_PubKey_v1 =
+ "-----BEGIN PUBLIC KEY-----\n"
+ "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyTig5CnpPOIZQn5yVfsg\n"
+ "Y+fNzVS3u0zgyZ7QPstVMgCR234KobRDy/6NOa6tHAzohn166dYZOrkOqFtAUUzQ\n"
+ "wVT0P9KkA0RTIChNBCA7G+LE3BYgomoSm2ofBRsfax8RdwYzsqArBHwluv3F6zOT\n"
+ "aLwUbtUbDYRwzeIOlfcBLxNlPxHKR8Jq9zREVaEHm8Vj8/e6vNZ66v5I8tg+NaUV\n"
+ "omR1h0SPfU0R77x1q/LX41jHISq/cCRLoi4ft+zEJ03HhtmPeV84DfWwtQtpv9P4\n"
+ "q2bC5pEH73VHs/igVC44JA9eNcJ/qGxd1uI7K3DGrqtVfa+csEzGtWD3THEo/ZXW\n"
+ "bBlGIbOvh58DxKpheR4C3xzWJfeHRUPueGaanwpALydaV5uE4yFwPmQ9KvBhEQAg\n"
+ "ck9d10FPKQfHXOAfXkTWToeVLoIk/oDIo9XUbEnFHELtw3gIGNHpMUmUOEk7th80\n"
+ "Cldf86fyEw0SPnP+y+SR6gw1zOvs1gbBfMxHsNKIfmpps162JV0bpP0vz7eZMyme\n"
+ "EuEEJ6TiyPHLaHaAluljc4UlpA+Huh/S8pZeObUhFpk3bvKVol/BHp3p388PRzZ1\n"
+ "eYLNXO8muQqL2SX4k9ndFggsDnr2QYp/dkLaoclNJUiBWPDSHDCuRMX1rCm+wv1p\n"
+ "wGWm2KJS9Iz7J5Bc19pcm90CAwEAAQ==\n"
+ "-----END PUBLIC KEY-----\n";
+
+
+const char *remmina_RSA_PubKey_v2 =
+ "-----BEGIN PUBLIC KEY-----\n"
+ "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxn7UtcGVxDbTvbprKNnw\n"
+ "CQws91Gg7as+A6PcqJ1GkRP46OKEiwJcVigbKZH53LQXLZUY+kOTYtD21+IicrAP\n"
+ "QL1er52oQf47ECEsNQ94kNirs/iqoGo+igNd+4scpFhm8WnrPgVvjbulqwGFY40d\n"
+ "/zqHaKgYhLvt8OUXF+YSIAC8cQ5qkXy8ncnrpBi22Slly5MyPfXciZj4oKvtXA54\n"
+ "8+TP9E+hwagH6xF7aH6fyEgTmzuI2y4hXzkWRXNg5umifpkdHfppWETA+FHIZ7Dh\n"
+ "/jy2NnR3wvoDMSqxNaea/LrGJS2cCQpO5d0EzAT+umA4ou2iDi6DfUgkAU/Vh7Jt\n"
+ "fedB6x2iewsMU12ef6UkRFrcf7v1tgfAjbWDUXCZTR5zNBm+nrWI6J8jsQ2h6ykP\n"
+ "qFdcUjMilLisDB8XPccUoEa0xCAMS2CgPiaC/CPeZoxpBXXxUlcwHUKLpl7wKxn7\n"
+ "/MxAu+ynbfL44xEJ74ka1z06Zi7pa4v16Pv0CAgoIRWI0mvZV7iANiBIvNQ5jE8I\n"
+ "k273owYRDfnZEHbFMF3TBzFdG6dALpJYqPA649p6VKNwoEksubf9ygWHWmaheUA1\n"
+ "Vq3SYIEx+Kymr650VdWLVXrLF+Gl+QXcGtfd5rTnVwW+erKWJU8bFDkmKLw8xADC\n"
+ "LlH/YYsHZOx0hXoxQJf+VHcCAwEAAQ==\n"
+ "-----END PUBLIC KEY-----\n";
+
+
+gint remmina_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 remmina_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 = remmina_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 remmina_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 remmina_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 = remmina_utils_string_find(haystack, pos, -1, needle);
+
+ if (pos == -1)
+ break;
+
+ pos = remmina_utils_string_replace(haystack, pos, needle_length, replace);
+ count++;
+ }
+ return count;
+}
+
+/**
+ * Strip \n, \t and \" from a given string.
+ * This function is particularly useful with g_spawn_command_line_sync that does
+ * not strip control characters from the output.
+ * @warning the result should be freed.
+ * @param a string.
+ * @return a newly allocated copy of string cleaned by \t, \n and \"
+ */
+gchar *remmina_utils_string_strip(const gchar *s)
+{
+ gchar *p = g_malloc(strlen(s) + 1);
+ if (p == NULL) {
+ return NULL;
+ }
+
+ if (p) {
+ gchar *p2 = p;
+ while (*s != '\0') {
+ if (*s != '\t' && *s != '\n' && *s != '\"')
+ *p2++ = *s++;
+ else
+ ++s;
+ }
+ *p2 = '\0';
+ }
+ return p;
+}
+
+/** OS related functions */
+
+/**
+ * remmina_utils_read_distrofile.
+ *
+ * Look for a distro version file /etc/xxx-release.
+ * Once found, read the file in and figure out which distribution.
+ *
+ * @param filename The file path of a Linux distribution release file.
+ * @param distroSize The size of the distribution name.
+ * @param distro The full distro name.
+ * @return Returns a string containing distro information verbatim from /etc/xxx-release (distro). Use g_free to free the string.
+ *
+ */
+static gchar *remmina_utils_read_distrofile(gchar *filename)
+{
+ TRACE_CALL(__func__);
+ gsize file_sz;
+ struct stat st;
+ gchar *distro_desc = NULL;
+ GError *err = NULL;
+
+ if (g_stat(filename, &st) == -1) {
+ return NULL;
+ }
+
+ if (st.st_size > 131072)
+ return NULL;
+
+ if (!g_file_get_contents(filename, &distro_desc, &file_sz, &err)) {
+ g_error_free(err);
+ return NULL;
+ }
+
+ if (file_sz == 0) {
+ return NULL;
+ }
+
+ return distro_desc;
+}
+
+/**
+ * Return the current language defined in the LC_ALL.
+ * @return a language string or en_US.
+ */
+gchar *remmina_utils_get_lang()
+{
+ TRACE_CALL(__func__);
+ gchar *lang = setlocale(LC_ALL, NULL);
+ gchar *ptr;
+
+ if (!lang || lang[0] == '\0') {
+ lang = "en_US\0";
+ } else {
+ ptr = strchr(lang, '.');
+ if (ptr != NULL)
+ *ptr = '\0';
+ }
+
+ return lang;
+}
+
+/**
+ * Return the OS name as in "uname -s".
+ * @return The OS name or NULL.
+ */
+gchar *remmina_utils_get_kernel_name()
+{
+ TRACE_CALL(__func__);
+ struct utsname u;
+
+ if (uname(&u) == -1) {
+ return NULL;
+ }
+ return g_strdup(u.sysname);
+}
+
+/**
+ * Return the OS version as in "uname -r".
+ * @return The OS release or NULL.
+ */
+gchar *remmina_utils_get_kernel_release()
+{
+ TRACE_CALL(__func__);
+ struct utsname u;
+
+ if (uname(&u) == -1) {
+ return NULL;
+ }
+ return g_strdup(u.release);
+}
+
+/**
+ * Return the machine hardware name as in "uname -m".
+ * @return The machine hardware name or NULL.
+ */
+gchar *remmina_utils_get_kernel_arch()
+{
+ TRACE_CALL(__func__);
+ struct utsname u;
+
+ if (uname(&u) == -1) {
+ return NULL;
+ }
+ return g_strdup(u.machine);
+}
+
+/**
+ * Print the Distributor as specified by the lsb_release command.
+ * @return the distributor ID string or NULL. Caller must free it with g_free().
+ */
+gchar *remmina_utils_get_lsb_id()
+{
+ TRACE_CALL(__func__);
+ gchar *lsb_id = NULL;
+ if (g_spawn_command_line_sync("/usr/bin/lsb_release -si", &lsb_id, NULL, NULL, NULL))
+ return lsb_id;
+ return NULL;
+}
+
+/**
+ * Print the Distribution description as specified by the lsb_release command.
+ * @return the Distribution description string or NULL. Caller must free it with g_free().
+ */
+gchar *remmina_utils_get_lsb_description()
+{
+ TRACE_CALL(__func__);
+ gchar *lsb_description = NULL;
+ if (g_spawn_command_line_sync("/usr/bin/lsb_release -sd", &lsb_description, NULL, NULL, NULL))
+ return lsb_description;
+ return NULL;
+}
+
+/**
+ * Print the Distribution release name as specified by the lsb_release command.
+ * @return the Distribution release name string or NULL. Caller must free it with g_free().
+ */
+gchar *remmina_utils_get_lsb_release()
+{
+ TRACE_CALL(__func__);
+ gchar *lsb_release = NULL;
+ if (g_spawn_command_line_sync("/usr/bin/lsb_release -sr", &lsb_release, NULL, NULL, NULL))
+ return lsb_release;
+ return NULL;
+}
+
+/**
+ * Print the Distribution codename as specified by the lsb_release command.
+ * @return the codename string or NULL. Caller must free it with g_free().
+ */
+gchar *remmina_utils_get_lsb_codename()
+{
+ TRACE_CALL(__func__);
+ gchar *lsb_codename = NULL;
+ if (g_spawn_command_line_sync("/usr/bin/lsb_release -sc", &lsb_codename, NULL, NULL, NULL))
+ return lsb_codename;
+ return NULL;
+}
+
+/**
+ * Print the distribution description if found.
+ * Test each known distribution specific information file and print it’s content.
+ * @return a string or NULL. Caller must free it with g_free().
+ */
+GHashTable *remmina_utils_get_etc_release()
+{
+ TRACE_CALL(__func__);
+ gchar *etc_release = NULL;
+ gint i;
+ GHashTable *r;
+
+ r = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
+
+ for (i = 0; distroArray[i].filename != NULL; i++) {
+ etc_release = remmina_utils_read_distrofile(distroArray[i].filename);
+ if (etc_release) {
+ if (etc_release[0] != '\0') {
+ g_hash_table_insert(r, distroArray[i].filename, etc_release);
+ } else {
+ g_free(etc_release);
+ }
+ }
+ }
+ return r;
+}
+
+/**
+ * Print device associated with default route.
+ * @return a string or NULL. Caller must free it with g_free().
+ */
+gchar *remmina_utils_get_dev()
+{
+ TRACE_CALL(__func__);
+ gchar *dif;
+ gint pos = 0;
+ GString *dev;
+
+ if (g_spawn_command_line_sync("ip route show default", &dif, NULL, NULL, NULL)) {
+ dev = g_string_new(dif);
+ g_free(dif);
+ pos = remmina_utils_string_find(dev, pos, -1, "dev ");
+ dev = g_string_erase(dev, 0, pos + 4);
+ pos = remmina_utils_string_find(dev, 0, -1, " ");
+ dev = g_string_truncate(dev, pos);
+ return g_string_free(dev, FALSE);
+ }
+ return NULL;
+}
+
+/**
+ * Print address associated with default route.
+ * @return a string or NULL. Caller must free it with g_free().
+ */
+gchar *remmina_utils_get_logical()
+{
+ TRACE_CALL(__func__);
+ gchar *dev = NULL;
+ gchar *dlog;
+ gint pos = 0;
+
+ dev = remmina_utils_get_dev();
+ GString *lbuf = g_string_new("ip -4 -o addr show ");
+ if ( dev != NULL ) {
+ lbuf = g_string_append(lbuf, dev);
+ g_free(dev);
+ }
+
+ if (g_spawn_command_line_sync(lbuf->str, &dlog, NULL, NULL, NULL)) {
+ g_string_free(lbuf, TRUE);
+ GString *log = g_string_new(dlog);
+ g_free(dlog);
+ pos = remmina_utils_string_find(log, pos, -1, "inet ");
+ log = g_string_erase(log, 0, pos + 5);
+ pos = remmina_utils_string_find(log, 0, -1, " ");
+ log = g_string_truncate(log, pos);
+ return g_string_free(log, FALSE);
+ }
+ g_string_free(lbuf, TRUE);
+ return NULL;
+}
+
+/**
+ * Print link associated with default route.
+ * @return a string or NULL. Caller must free it with g_free().
+ */
+gchar *remmina_utils_get_link()
+{
+ TRACE_CALL(__func__);
+ gchar *dev = NULL;
+ gchar *plink;
+ gint pos = 0;
+
+ dev = remmina_utils_get_dev();
+ GString *pbuf = g_string_new("ip -4 -o link show ");
+ if ( dev != NULL ) {
+ pbuf = g_string_append(pbuf, dev);
+ g_free(dev);
+ }
+
+ if (g_spawn_command_line_sync(pbuf->str, &plink, NULL, NULL, NULL))
+ {
+ g_string_free(pbuf, TRUE);
+ GString *link = g_string_new(plink);
+ g_free(plink);
+ pos = remmina_utils_string_find(link, pos, -1, "link/ether ");
+ link = g_string_erase(link, 0, pos + 11);
+ pos = remmina_utils_string_find(link, 0, -1, " ");
+ link = g_string_truncate(link, pos);
+ return g_string_free(link, FALSE);
+ }
+ g_string_free(pbuf, TRUE);
+ return NULL;
+}
+
+/**
+ * Print python version.
+ * @return a string or NULL. Caller must free it with g_free().
+ */
+gchar *remmina_utils_get_python()
+{
+ TRACE_CALL(__func__);
+ gchar *version;
+
+ version = (g_spawn_command_line_sync("python -V", &version, NULL, NULL, NULL)) ||
+ (g_spawn_command_line_sync("python3 -V", &version, NULL, NULL, NULL)) ? version : NULL;
+ if (version != NULL)
+ version = remmina_utils_string_strip(version);
+
+ return version;
+}
+
+
+/**
+ * Print machine age.
+ * @return a string or NULL. Caller must free it with g_free().
+ */
+gchar *remmina_utils_get_mage()
+{
+ TRACE_CALL(__func__);
+ gchar *mage = malloc(21);
+ if (mage == NULL) {
+ return "";
+ }
+ struct stat sb;
+
+ if (stat("/etc/machine-id", &sb) == 0) {
+ sprintf(mage, "%ld", (long)(time(NULL) - sb.st_mtim.tv_sec));
+ }
+ else {
+ strcpy(mage, "0");
+ }
+
+ return mage;
+}
+
+/**
+ * Create a hexadecimal string version of the SHA-256 digest of the
+ * buffer.
+ *
+ * @return a newly allocated string which the caller should free() when
+ * finished.
+ *
+ * If any error occurs, this function returns NULL.
+ */
+gchar *remmina_sha256_buffer(const guchar *buffer, gssize length)
+{
+ GChecksum *sha256 = NULL;
+ gchar *digest_string = NULL;
+
+ sha256 = g_checksum_new(G_CHECKSUM_SHA256);
+ if (sha256 == NULL) {
+ goto DONE;
+ }
+
+ g_checksum_update(sha256, buffer, length);
+
+ digest_string = g_strdup(g_checksum_get_string(sha256));
+
+DONE:
+ if (sha256) {
+ g_checksum_free(sha256);
+ }
+
+ return digest_string;
+}
+
+/**
+ * Create a hexadecimal string version of the SHA-1 digest of the
+ * contents of the named file.
+ *
+ * @return a newly allocated string which the caller
+ * should free() when finished.
+ *
+ * If any error occurs while reading the file, (permission denied,
+ * file not found, etc.), this function returns NULL.
+ *
+ * Taken from https://github.com/ttuegel/notmuch do PR in case of substantial modifications.
+ *
+ */
+gchar *remmina_sha1_file(const gchar *filename)
+{
+ FILE *file;
+
+#define BLOCK_SIZE 4096
+ unsigned char block[BLOCK_SIZE];
+ size_t bytes_read;
+ GChecksum *sha1;
+ char *digest = NULL;
+
+ file = fopen(filename, "r");
+ if (file == NULL)
+ return NULL;
+
+ sha1 = g_checksum_new(G_CHECKSUM_SHA1);
+ if (sha1 == NULL)
+ goto DONE;
+
+ while (1) {
+ bytes_read = fread(block, 1, 4096, file);
+ if (bytes_read == 0) {
+ if (feof(file))
+ break;
+ else if (ferror(file))
+ goto DONE;
+ } else {
+ g_checksum_update(sha1, block, bytes_read);
+ }
+ }
+
+ digest = g_strdup(g_checksum_get_string(sha1));
+
+DONE:
+ if (sha1)
+ g_checksum_free(sha1);
+ if (file)
+ fclose(file);
+
+ return digest;
+}
+
+/**
+ * Generate a random sting of chars to be used as part of UID for news or stats
+ * @return a string or NULL. Caller must free it with g_free().
+ */
+gchar *remmina_gen_random_uuid()
+{
+ TRACE_CALL(__func__);
+ gchar *result;
+ int i;
+ static char alpha[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+
+ result = g_malloc0(15);
+ if (result == NULL) {
+ return "";
+ }
+
+ for (i = 0; i < 7; i++)
+ result[i] = alpha[randombytes_uniform(sizeof(alpha) - 1)];
+
+ for (i = 0; i < 7; i++)
+ result[i + 7] = alpha[randombytes_uniform(sizeof(alpha) - 1)];
+
+ return result;
+}
+
+/**
+ * Uses OSSL_DECODER_CTX to convert public key string to usable OpenSSL structs
+ * @return an EVP_PKEY that can be used for public encryption
+ */
+EVP_PKEY *remmina_get_pubkey(const char *keytype, const char *public_key_selection) {
+ BIO *pkbio;
+ OSSL_DECODER_CTX *dctx;
+
+ EVP_PKEY *pubkey = NULL;
+ const char *format = "PEM";
+ const char *structure = NULL;
+ dctx = OSSL_DECODER_CTX_new_for_pkey(&pubkey, format, structure,
+ keytype,
+ EVP_PKEY_PUBLIC_KEY,
+ NULL, NULL);
+
+ if (dctx == NULL) {
+ return NULL;
+ }
+ pkbio = BIO_new_mem_buf(public_key_selection, -1);
+
+ if (OSSL_DECODER_from_bio(dctx, pkbio) == 0) {
+ BIO_free(pkbio);
+ OSSL_DECODER_CTX_free(dctx);
+ return NULL;
+ }
+ BIO_free(pkbio);
+ OSSL_DECODER_CTX_free(dctx);
+ return pubkey;
+}
+
+/**
+ * Calls EVP_PKEY_encrypt multiple times to encrypt instr. At the end, base64
+ * encode the resulting buffer.
+ *
+ * @param pubkey an RSA public key
+ * @param instr input string to be encrypted
+ *
+ * @return a buffer ptr. Use g_free() to deallocate it
+ */
+gchar *remmina_rsa_encrypt_string(EVP_PKEY *pubkey, const char *instr)
+{
+#define RSA_OAEP_PAD_SIZE 42
+ gchar *enc;
+ int remaining;
+ size_t blksz, maxblksz;
+ unsigned char *outptr;
+
+ unsigned char *ebuf = NULL;
+ gsize ebufLen;
+ EVP_PKEY_CTX *ctx = NULL;
+ int pkeyLen = EVP_PKEY_size(pubkey);
+ int inLen = strlen(instr);
+ remaining = inLen;
+
+ maxblksz = pkeyLen - RSA_OAEP_PAD_SIZE;
+ ebufLen = (((inLen - 1) / maxblksz) + 1) * pkeyLen;
+ ebuf = g_malloc(ebufLen);
+ if (ebuf == NULL) {
+ return NULL;
+ }
+ outptr = ebuf;
+
+ ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pubkey, NULL);
+ if (ctx == NULL) {
+ g_free(ebuf);
+ return NULL;
+ }
+
+ if (EVP_PKEY_encrypt_init_ex(ctx, NULL) <= 0) {;
+ EVP_PKEY_CTX_free(ctx);
+ g_free(ebuf);
+ return NULL;
+ }
+
+ if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ g_free(ebuf);
+ return NULL;
+ }
+
+ /* encrypt input string block by block */
+ while (remaining > 0) {
+ blksz = remaining > maxblksz ? maxblksz : remaining;
+ size_t out_blksz;
+
+ int calc_size_return_val = EVP_PKEY_encrypt(ctx, NULL, &out_blksz, (const unsigned char *)instr, blksz);
+ if (calc_size_return_val == -2) {
+ EVP_PKEY_CTX_free(ctx);
+ g_free(ebuf);
+ return NULL;
+ }
+ else if (calc_size_return_val <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ g_free(ebuf);
+ return NULL;
+ }
+
+ if (EVP_PKEY_encrypt(ctx, outptr, &out_blksz, (const unsigned char *)instr, blksz) <= 0) {;
+ EVP_PKEY_CTX_free(ctx);
+ g_free(ebuf);
+ return NULL;
+ }
+
+ instr += blksz;
+ remaining -= blksz;
+ outptr += out_blksz;
+ }
+
+ enc = g_base64_encode(ebuf, ebufLen);
+ g_free(ebuf);
+ EVP_PKEY_CTX_free(ctx);
+ return enc;
+}
+
+/**
+ * Attempts to verify the contents of @a plugin_file with base64 encoded @a signature. This function uses the OpenSSL EVP API
+ * and expects a well-formed EC signature in DER format. The digest function used is SHA256.
+ *
+ * @param signature A base64 encoded signature.
+ * @param plugin_file The file containing the message you wish to verify.
+ * @param message_length The length of the message.
+ * @param signature_length The length of the signature.
+ *
+ * @return TRUE if the signature can be verified or FALSE if it cannot or there is an error.
+ */
+gboolean remmina_verify_plugin_signature(const guchar *signature, GFile* plugin_file, size_t message_length,
+ size_t signature_length) {
+ EVP_PKEY *public_key = NULL;
+
+ /* Get public key */
+ public_key = remmina_get_pubkey(RSA_KEYTYPE, remmina_RSA_PubKey_v2);
+ if (public_key == NULL) {
+ return FALSE;
+ }
+
+ gboolean verified = remmina_execute_plugin_signature_verification(plugin_file, message_length, signature,
+ signature_length, public_key);
+
+ EVP_PKEY_free(public_key);
+ if (verified) {
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+}
+
+/**
+ * A helper function that performs the actual signature verification (that is, this function makes direct calls to the
+ * OPENSSL EVP API) of @a plugin_file using signature @a sig with @a public_key.
+ *
+ * @param plugin_file The file containing the message you wish to verify.
+ * @param msg_len The length of the message.
+ * @param sig The signature to verify the message with.
+ * @param sig_len The length of the signature.
+ * @param public_key The public key to use in the signature verification.
+ *
+ * @return TRUE if the signature can be verified or FALSE if there is an error or the signature cannot be verified.
+ */
+gboolean remmina_execute_plugin_signature_verification(GFile* plugin_file, size_t msg_len, const guchar *sig, size_t sig_len, EVP_PKEY* public_key)
+{
+ /* Returned to caller */
+ if(!plugin_file || !msg_len || !sig || !sig_len || !public_key) {
+ return FALSE;
+ }
+
+ EVP_MD_CTX* ctx = NULL;
+ gssize read = 0;
+ gssize total_read = 0;
+ unsigned char buffer[1025];
+
+
+ /* Create context for verification */
+ ctx = EVP_MD_CTX_create();
+ if(ctx == NULL) {
+ return FALSE;
+ }
+
+ /* Returns the message digest (hash function) specified*/
+ const EVP_MD* message_digest = EVP_get_digestbyname(NAME_OF_DIGEST);
+ if(message_digest == NULL) {
+ EVP_MD_CTX_free(ctx);
+ return FALSE;
+ }
+
+ /* Set up context with the corresponding message digest. 1 is the only code for success */
+ int return_val = EVP_DigestInit_ex(ctx, message_digest, NULL);
+
+ if(return_val != 1) {
+ EVP_MD_CTX_free(ctx);
+ return FALSE;
+ }
+
+ /* Add the public key and initialize the context for verification*/
+
+ return_val = EVP_DigestVerifyInit(ctx, NULL, message_digest, NULL, public_key);
+
+ if(return_val != 1) {
+ EVP_MD_CTX_free(ctx);
+ return FALSE;
+ }
+
+ GFileInputStream *in_stream = g_file_read(plugin_file, NULL, NULL);
+
+ /* Add the msg to the context. The next step will be to actually verify the added message */
+ read = g_input_stream_read(G_INPUT_STREAM(in_stream), buffer, 1024, NULL, NULL);
+ while (read > 0){
+ total_read += read;
+ return_val = EVP_DigestVerifyUpdate(ctx, buffer, read);
+
+ if(return_val != 1) {
+ EVP_MD_CTX_free(ctx);
+ return FALSE;
+ }
+ read = g_input_stream_read(G_INPUT_STREAM(in_stream), buffer, 1024, NULL, NULL);
+ }
+ msg_len = total_read;
+
+ /* Clear any errors for the call below */
+ ERR_clear_error();
+
+ /* Execute the signature verification. */
+ return_val = EVP_DigestVerifyFinal(ctx, sig, sig_len);
+
+ if(return_val != 1) {
+ EVP_MD_CTX_free(ctx);
+ return FALSE;
+ }
+
+ if(ctx != NULL) {
+ EVP_MD_CTX_destroy(ctx);
+ ctx = NULL;
+ }
+
+ return TRUE;
+}
+
+/**
+ *
+ * Decompresses pointer @source with length @len to file pointed to by @plugin_file. It is the caller's responsibility
+ * to free @source and @plugin_file when no longer in use.
+ *
+ * @return The total number of compressed bytes read or -1 on error.
+ */
+int remmina_decompress_from_memory_to_file(guchar *source, int len, GFile* plugin_file) {
+ GError *error = NULL;
+ //Compress with gzip. The level is -1 for default
+ GZlibDecompressor* gzlib_decompressor = g_zlib_decompressor_new(G_ZLIB_COMPRESSOR_FORMAT_GZIP);
+ if (gzlib_decompressor == NULL) {
+ return -1;
+ }
+
+ GInputStream *base_stream = g_memory_input_stream_new_from_data(source, len, NULL);
+
+ if (base_stream == NULL) {
+ g_object_unref(gzlib_decompressor);
+ return -1;
+ }
+ GInputStream *converted_input_stream = g_converter_input_stream_new(base_stream, G_CONVERTER(gzlib_decompressor));
+
+ if (converted_input_stream == NULL) {
+ g_object_unref(base_stream);
+ g_object_unref(gzlib_decompressor);
+ return -1;
+ }
+
+ int total_read = 0;
+
+ GFileOutputStream *f_out_stream = g_file_replace(plugin_file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error);
+ if (f_out_stream == NULL){
+ g_object_unref(base_stream);
+ g_object_unref(gzlib_decompressor);
+ return -1;
+ }
+
+ g_output_stream_splice(G_OUTPUT_STREAM(f_out_stream), converted_input_stream, G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET, NULL, NULL);
+ GFileInfo* info = g_file_query_info(plugin_file, G_FILE_ATTRIBUTE_STANDARD_SIZE, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL);
+ total_read = g_file_info_get_size(info);
+
+
+ //Freeing stuff
+ g_object_unref(converted_input_stream);
+ g_object_unref(f_out_stream);
+ g_object_unref(gzlib_decompressor);
+ g_object_unref(base_stream);
+ g_object_unref(info);
+
+
+ return total_read;
+
+}
+
+/**
+ * Compresses file @source to file @dest. Creates enough space to compress MAX_COMPRESSED_FILE_SIZE. It is the caller's
+ * responsibility to close @source and @dest.
+ *
+ * @return Total number of bytes read from source or -1 on error.
+ */
+int remmina_compress_from_file_to_file(GFile *source, GFile *dest)
+{
+ TRACE_CALL(__func__);
+ GError *error = NULL;
+
+ // Compress with gzip. The level is -1 for default
+ GZlibCompressor *gzlib_compressor = g_zlib_compressor_new(G_ZLIB_COMPRESSOR_FORMAT_GZIP, -1);
+
+ if (gzlib_compressor == NULL) {
+ REMMINA_DEBUG("Compressor instantiation failed");
+ return -1;
+ }
+
+ GFileInputStream *base_stream = g_file_read(source, NULL, &error);
+
+ if (base_stream == NULL) {
+ REMMINA_DEBUG("Base stream is null, error: %s", error->message);
+ g_free(error);
+ g_object_unref(gzlib_compressor);
+ return -1;
+ }
+
+ GInputStream *converted_input_stream = g_converter_input_stream_new((GInputStream *) base_stream, G_CONVERTER(gzlib_compressor));
+
+ if (converted_input_stream == NULL) {
+ REMMINA_DEBUG("Failed to allocate converted input stream");
+ g_object_unref(base_stream);
+ g_object_unref(gzlib_compressor);
+ return -1;
+ }
+
+ int total_read = 0;
+
+ guint8 *converted, *current_position;
+ converted = g_malloc(sizeof(guint8) * MAX_COMPRESSED_FILE_SIZE); // 100kb file max
+ if (converted == NULL) {
+ REMMINA_DEBUG("Failed to allocate compressed file memory");
+ g_object_unref(base_stream);
+ g_object_unref(gzlib_compressor);
+ g_object_unref(converted_input_stream);
+ return -1;
+ }
+
+ current_position = converted; // Set traveling pointer
+
+ gsize bytes_read = 0;
+ while (total_read < MAX_COMPRESSED_FILE_SIZE) {
+ bytes_read = g_input_stream_read(G_INPUT_STREAM(converted_input_stream), current_position, 1, NULL, &error);
+ if (error != NULL) {
+ REMMINA_DEBUG("Error reading input stream: %s", error->message);
+ g_free(error);
+ g_object_unref(base_stream);
+ g_object_unref(gzlib_compressor);
+ g_object_unref(converted_input_stream);
+ g_free(converted);
+ return -1;
+ }
+
+ if (bytes_read == 0) {
+ break; // Reached EOF, break read
+ }
+ current_position += bytes_read;
+
+ total_read += bytes_read;
+ }
+
+ if (total_read >= MAX_COMPRESSED_FILE_SIZE) {
+ REMMINA_DEBUG("Detected heap overflow in allocating compressed file");
+ g_object_unref(base_stream);
+ g_object_unref(gzlib_compressor);
+ g_object_unref(converted_input_stream);
+ g_free(converted);
+ return -1;
+ }
+
+ gsize *bytes_written = g_malloc(sizeof(gsize) * total_read);
+ if (bytes_written == NULL) {
+ REMMINA_DEBUG("Failed to allocate bytes written");
+ g_object_unref(base_stream);
+ g_object_unref(gzlib_compressor);
+ g_object_unref(converted_input_stream);
+ g_free(converted);
+ return -1;
+ }
+
+ GFileOutputStream *g_output_stream = g_file_append_to(dest, G_FILE_CREATE_NONE, NULL, &error);
+
+ if (g_output_stream == NULL) {
+ REMMINA_DEBUG("Error appending to file: %s", error->message);
+ g_error_free(error);
+ g_object_unref(base_stream);
+ g_object_unref(gzlib_compressor);
+ g_object_unref(converted_input_stream);
+ g_free(bytes_written);
+ g_free(converted);
+ return -1;
+ }
+ g_output_stream_write_all(G_OUTPUT_STREAM(g_output_stream), converted, total_read, bytes_written, NULL, &error);
+ if (error != NULL) {
+ REMMINA_DEBUG("Error writing all bytes to file: %s", error->message);
+ g_error_free(error);
+ g_object_unref(base_stream);
+ g_object_unref(gzlib_compressor);
+ g_object_unref(converted_input_stream);
+ g_object_unref(g_output_stream);
+ g_free(bytes_written);
+ g_free(converted);
+ return -1;
+ }
+
+ g_object_unref(base_stream);
+ g_object_unref(gzlib_compressor);
+ g_object_unref(converted_input_stream);
+ g_object_unref(g_output_stream);
+ g_free(bytes_written);
+ g_free(converted);
+ return total_read;
+}
diff --git a/src/remmina_utils.h b/src/remmina_utils.h
new file mode 100644
index 0000000..8c3af3f
--- /dev/null
+++ b/src/remmina_utils.h
@@ -0,0 +1,86 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2016-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.
+ *
+ */
+
+/**
+ * @file: remmina_utils.h
+ * General utility functions, non-GTK related.
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <gio/gio.h>
+#include <openssl/rsa.h>
+#include <openssl/evp.h>
+#include <openssl/decoder.h>
+
+#define MAX_COMPRESSED_FILE_SIZE 100000 //100kb file size max
+#define NAME_OF_DIGEST "SHA256"
+#define RSA_KEYTYPE "RSA"
+
+extern const char *remmina_RSA_PubKey_v1;
+extern const char *remmina_RSA_PubKey_v2;
+extern const char *remmina_EC_PubKey;
+G_BEGIN_DECLS
+gint remmina_utils_string_find(GString *haystack, gint start, gint end, const gchar *needle);
+gint remmina_utils_string_replace(GString *str, gint pos, gint len, const gchar *replace);
+
+guint remmina_utils_string_replace_all(GString *haystack, const gchar *needle, const gchar *replace);
+gchar *remmina_utils_string_strip(const gchar *s);
+
+gchar *remmina_utils_get_lang();
+gchar *remmina_utils_get_kernel_name();
+gchar *remmina_utils_get_kernel_release();
+gchar *remmina_utils_get_kernel_arch();
+gchar *remmina_utils_get_lsb_id();
+gchar *remmina_utils_get_lsb_description();
+gchar *remmina_utils_get_lsb_release();
+gchar *remmina_utils_get_lsb_codename();
+GHashTable *remmina_utils_get_etc_release();
+gchar *remmina_utils_get_dev();
+gchar *remmina_utils_get_logical();
+gchar *remmina_utils_get_link();
+gchar *remmina_utils_get_python();
+gchar *remmina_utils_get_mage();
+gchar *remmina_sha256_buffer(const guchar *buffer, gssize length);
+gchar *remmina_sha1_file(const gchar *filename);
+gchar *remmina_gen_random_uuid();
+EVP_PKEY *remmina_get_pubkey(const char *keytype, const char *public_key_selection);
+gchar *remmina_rsa_encrypt_string(EVP_PKEY *pubkey, const char *instr);
+int remmina_decompress_from_memory_to_file(guchar *source, int len, GFile* plugin_file);
+int remmina_compress_from_file_to_file(GFile *source, GFile *dest);
+gboolean remmina_verify_plugin_signature(const guchar *signature, GFile* plugin_file, size_t message_length, size_t signature_length);
+gboolean remmina_execute_plugin_signature_verification(GFile* plugin_file, size_t msg_len, const guchar *sig, size_t sig_len, EVP_PKEY* public_key);
+G_END_DECLS
diff --git a/src/remmina_widget_pool.c b/src/remmina_widget_pool.c
new file mode 100644
index 0000000..53c32a0
--- /dev/null
+++ b/src/remmina_widget_pool.c
@@ -0,0 +1,144 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <gmodule.h>
+#include "remmina_public.h"
+#include "remmina_widget_pool.h"
+#include "remmina/remmina_trace_calls.h"
+
+static GPtrArray *remmina_widget_pool = NULL;
+
+void remmina_widget_pool_init(void)
+{
+ TRACE_CALL(__func__);
+ remmina_widget_pool = g_ptr_array_new();
+}
+
+static void remmina_widget_pool_on_widget_destroy(GtkWidget *widget, gpointer data)
+{
+ TRACE_CALL(__func__);
+ g_ptr_array_remove(remmina_widget_pool, widget);
+}
+
+void remmina_widget_pool_register(GtkWidget *widget)
+{
+ TRACE_CALL(__func__);
+ g_ptr_array_add(remmina_widget_pool, widget);
+ g_signal_connect(G_OBJECT(widget), "destroy", G_CALLBACK(remmina_widget_pool_on_widget_destroy), NULL);
+}
+
+GtkWidget*
+remmina_widget_pool_find(GType type, const gchar *tag)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+ gint i;
+
+ if (remmina_widget_pool == NULL)
+ return NULL;
+
+ for (i = 0; i < remmina_widget_pool->len; i++) {
+ widget = GTK_WIDGET(g_ptr_array_index(remmina_widget_pool, i));
+ if (!G_TYPE_CHECK_INSTANCE_TYPE(widget, type))
+ continue;
+ if (tag && g_strcmp0((const gchar*)g_object_get_data(G_OBJECT(widget), "tag"), tag) != 0)
+ continue;
+ return widget;
+ }
+ return NULL;
+}
+
+GtkWidget*
+remmina_widget_pool_find_by_window(GType type, GdkWindow *window)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+ gint i;
+ GdkWindow *parent;
+
+ if (window == NULL || remmina_widget_pool == NULL)
+ return NULL;
+
+ for (i = 0; i < remmina_widget_pool->len; i++) {
+ widget = GTK_WIDGET(g_ptr_array_index(remmina_widget_pool, i));
+ if (!G_TYPE_CHECK_INSTANCE_TYPE(widget, type))
+ continue;
+ /* gdk_window_get_toplevel won’t work here, if the window is an embedded client. So we iterate the window tree */
+ for (parent = window; parent && parent != GDK_WINDOW_ROOT; parent = gdk_window_get_parent(parent)) {
+ if (gtk_widget_get_window(widget) == parent)
+ return widget;
+ }
+ }
+ return NULL;
+}
+
+gint remmina_widget_pool_foreach(RemminaWidgetPoolForEachFunc callback, gpointer data)
+{
+ TRACE_CALL(__func__);
+ GtkWidget *widget;
+ gint i;
+ gint n = 0;
+ GPtrArray *wpcpy = NULL;
+
+ if (remmina_widget_pool == NULL)
+ return 0;
+
+ /* Make a copy of remmina_widget_pool, so we can survive when callback()
+ * remove an element from remmina_widget_pool */
+
+ wpcpy = g_ptr_array_sized_new(remmina_widget_pool->len);
+
+ for (i = 0; i < remmina_widget_pool->len; i++)
+ g_ptr_array_add(wpcpy, g_ptr_array_index(remmina_widget_pool, i));
+
+ /* Scan the remmina_widget_pool and call callbac() on every element */
+ for (i = 0; i < wpcpy->len; i++) {
+ widget = GTK_WIDGET(g_ptr_array_index(wpcpy, i));
+ if (callback(widget, data))
+ n++;
+ }
+
+ /* Free the copy */
+ g_ptr_array_unref(wpcpy);
+ return n;
+}
+
+gint remmina_widget_pool_count()
+{
+ TRACE_CALL(__func__);
+ return remmina_widget_pool->len;
+}
+
diff --git a/src/remmina_widget_pool.h b/src/remmina_widget_pool.h
new file mode 100644
index 0000000..81fd130
--- /dev/null
+++ b/src/remmina_widget_pool.h
@@ -0,0 +1,51 @@
+/*
+ * Remmina - The GTK+ Remote Desktop Client
+ * Copyright (C) 2009 - 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+typedef gboolean (*RemminaWidgetPoolForEachFunc)(GtkWidget *widget, gpointer data);
+
+void remmina_widget_pool_init(void);
+void remmina_widget_pool_register(GtkWidget *widget);
+GtkWidget *remmina_widget_pool_find(GType type, const gchar *tag);
+GtkWidget *remmina_widget_pool_find_by_window(GType type, GdkWindow *window);
+gint remmina_widget_pool_foreach(RemminaWidgetPoolForEachFunc callback, gpointer data);
+gint remmina_widget_pool_count(void);
+
+G_END_DECLS