diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 17:06:32 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 17:06:32 +0000 |
commit | 2dad5357405ad33cfa792f04b3ab62a5d188841e (patch) | |
tree | b8f8893942060fe3cfb04ac374cda96fdfc8f453 /src | |
parent | Initial commit. (diff) | |
download | remmina-2dad5357405ad33cfa792f04b3ab62a5d188841e.tar.xz remmina-2dad5357405ad33cfa792f04b3ab62a5d188841e.zip |
Adding upstream version 1.4.34+dfsg.upstream/1.4.34+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src')
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 |