diff --git a/gnucash/gnome/gnc-plugin-page-report.cpp b/gnucash/gnome/gnc-plugin-page-report.cpp index a700a233c2..716f4d4c04 100644 --- a/gnucash/gnome/gnc-plugin-page-report.cpp +++ b/gnucash/gnome/gnc-plugin-page-report.cpp @@ -127,7 +127,6 @@ typedef struct GncPluginPageReportPrivate /// the gnc_html abstraction this PluginPage contains // gnc_html *html; GncHtml *html; - gboolean webkit2; /// the container the above HTML widget is in. GtkContainer *container; @@ -519,11 +518,6 @@ gnc_plugin_page_report_create_widget( GncPluginPage *page ) report = GNC_PLUGIN_PAGE_REPORT(page); priv = GNC_PLUGIN_PAGE_REPORT_GET_PRIVATE(report); -#ifndef WEBKIT1 - /* Hide the ExportPdf action for Webkit2 */ - priv->webkit2 = TRUE; -#endif - topLvl = gnc_ui_get_main_window (nullptr); // priv->html = gnc_html_new( topLvl ); priv->html = gnc_html_factory_create_html(); @@ -1249,8 +1243,6 @@ gnc_plugin_page_report_menu_update (GncPluginPage *plugin_page, static void gnc_plugin_page_report_menu_updates (GncPluginPage *plugin_page) { - GncPluginPageReportPrivate *priv; - GncPluginPageReport *report; GncMainWindow *window; action_toolbar_labels tooltip_list[3]; GAction *action; @@ -1263,9 +1255,6 @@ gnc_plugin_page_report_menu_updates (GncPluginPage *plugin_page) _("Add the current report's configuration to the 'Reports->Saved Report Configurations' menu. " "The report configuration will be saved in the file %s."), saved_reports_path); - report = GNC_PLUGIN_PAGE_REPORT(plugin_page); - priv = GNC_PLUGIN_PAGE_REPORT_GET_PRIVATE(report); - window = (GncMainWindow*)gnc_plugin_page_get_window (GNC_PLUGIN_PAGE(plugin_page)); tooltip_list[0] = { "ReportSaveAction", N_("Save _Report Configuration"), report_save_str }; @@ -1278,11 +1267,6 @@ gnc_plugin_page_report_menu_updates (GncPluginPage *plugin_page) action = gnc_main_window_find_action (window, "FilePrintAction"); g_simple_action_set_enabled (G_SIMPLE_ACTION(action), true); - if (priv->webkit2) - { - GtkWidget *pdf_item = gnc_main_window_menu_find_menu_item (window, "FilePrintPDFAction"); - gtk_widget_hide (pdf_item); - } g_free (saved_reports_path); g_free (report_save_str); g_free (report_saveas_str); @@ -1299,7 +1283,6 @@ gnc_plugin_page_report_constr_init (GncPluginPageReport *plugin_page, gint repor DEBUG("property reportId=%d", reportId); priv = GNC_PLUGIN_PAGE_REPORT_GET_PRIVATE(plugin_page); priv->reportId = reportId; - priv->webkit2 = FALSE; gnc_plugin_page_report_setup( GNC_PLUGIN_PAGE(plugin_page)); @@ -1991,11 +1974,7 @@ gnc_plugin_page_report_print_cb (GSimpleAction *simple, //g_warning("Setting job name=%s", job_name); -#ifdef WEBKIT1 - gnc_html_print (priv->html, job_name, FALSE); -#else gnc_html_print (priv->html, job_name); -#endif g_free (job_name); } @@ -2037,11 +2016,7 @@ gnc_plugin_page_report_exportpdf_cb (GSimpleAction *simple, //g_warning("Setting job name=%s", job_name); -#ifdef WEBKIT1 - gnc_html_print (priv->html, job_name, TRUE); -#else gnc_html_print (priv->html, job_name); -#endif if (owner) { diff --git a/gnucash/html/CMakeLists.txt b/gnucash/html/CMakeLists.txt index 1efdaf0133..9c5bb12946 100644 --- a/gnucash/html/CMakeLists.txt +++ b/gnucash/html/CMakeLists.txt @@ -5,8 +5,10 @@ set (html_HEADERS gnc-html-p.h gnc-html-factory.h gnc-html-extras.h - gnc-html-webkit-p.h - gnc-html-webkit.h + gnc-html-browser-p.h + gnc-html-browser.h + gnc-ws-server.h + gnc-ws-protocol.h ) # Command to generate the swig-gnc-html.c wrapper file @@ -19,20 +21,11 @@ set (html_SOURCES gnc-html.c gnc-html-history.c gnc-html-factory.c + gnc-html-browser.c + gnc-ws-server.c + gnc-ws-protocol.c ) -if (WEBKIT1) - list(APPEND html_HEADERS gnc-html-webkit1.h) - list(APPEND html_SOURCES gnc-html-webkit1.c) - set(html_EXTRA_DIST gnc-html-webkit2.h gnc-html-webkit2.c) -else () - list(APPEND html_HEADERS gnc-html-webkit2.h) - list(APPEND html_SOURCES gnc-html-webkit2.c) - set(html_EXTRA_DIST gnc-html-webkit1.h gnc-html-webkit1.c) -endif() - - - set (gnc_html_SCHEME html.scm) set(GUILE_OUTPUT_DIR gnucash) @@ -57,7 +50,6 @@ target_link_libraries(gnc-html gnc-engine gnc-gnome-utils PkgConfig::GTK3 - PkgConfig::WEBKIT ${GUILE_LDFLAGS}) target_compile_definitions(gnc-html PRIVATE -DG_LOG_DOMAIN=\"gnc.html\") diff --git a/gnucash/html/gnc-html-browser-p.h b/gnucash/html/gnc-html-browser-p.h new file mode 100644 index 0000000000..5a4803edd5 --- /dev/null +++ b/gnucash/html/gnc-html-browser-p.h @@ -0,0 +1,40 @@ +/******************************************************************** + * gnc-html-browser-p.h -- display html with gnc special tags * + * Copyright (C) 2024 Bob Fewell * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License* + * along with this program; if not, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * +\********************************************************************/ + +#ifndef GNC_HTML_BROWSER_P_H +#define GNC_HTML_BROWSER_P_H + +#include "gnc-html-p.h" + +struct _GncHtmlBrowserPrivate +{ + struct _GncHtmlPrivate base; + + GtkWidget *web_view; + gchar* html_string; /* html string being displayed */ + + GncWsServer *gws; + + const gchar *file_name; +}; + +#endif diff --git a/gnucash/html/gnc-html-browser.c b/gnucash/html/gnc-html-browser.c new file mode 100644 index 0000000000..13b4f4372d --- /dev/null +++ b/gnucash/html/gnc-html-browser.c @@ -0,0 +1,1046 @@ +/******************************************************************** + * gnc-html-browser.c -- gnucash report renderer using a browser * + * Copyright (C) 2024 Bob Fewell * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License* + * along with this program; if not, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * + ********************************************************************/ + +#include + +#include +#ifdef __MINGW32__ +#define _GL_UNISTD_H //Deflect poisonous define of close in Guile's GnuLib +#endif +#include +#if PLATFORM(WINDOWS) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Account.h" +#include "gnc-prefs.h" +#include "gnc-gui-query.h" +#include "gnc-engine.h" +#include "gnc-html.h" +#include "gnc-html-browser.h" +#include "gnc-html-history.h" +#include "print-session.h" + +#include "gnc-ws-server.h" + +G_DEFINE_TYPE(GncHtmlBrowser, gnc_html_browser, GNC_TYPE_HTML) + +static void gnc_html_browser_dispose (GObject* obj); +static void gnc_html_browser_finalize (GObject* obj); + +#define GNC_HTML_BROWSER_GET_PRIVATE(o) (GNC_HTML_BROWSER(o)->priv) + +#include "gnc-html-browser-p.h" + +/* indicates the debugging module that this .o belongs to. */ +static QofLogModule log_module = GNC_MOD_HTML; + +/* hashes an HTML classid to a handler function */ +extern GHashTable* gnc_html_object_handlers; + +/* hashes handlers for loading different URLType data */ +extern GHashTable* gnc_html_stream_handlers; + +/* hashes handlers for handling different URLType data */ +extern GHashTable* gnc_html_url_handlers; + +static char error_404_format[] = "

%s

%s"; +static char error_404_title[] = N_("Not found"); +static char error_404_body[] = N_("The specified URL could not be loaded."); + +#define BASE_URI_NAME "base-uri" +#define GNC_PREF_RPT_DFLT_ZOOM "default-zoom" + +static gchar* handle_embedded_object (GncHtmlBrowser* self, gchar* html_str); +static void impl_browser_show_url (GncHtml* self, URLType type, + const gchar* location, const gchar* label, + gboolean new_window_hint); +static void impl_browser_show_data (GncHtml* self, const gchar* data, int datalen); +static void impl_browser_reload (GncHtml* self, gboolean force_rebuild); +static void impl_browser_copy_to_clipboard (GncHtml* self); +static gboolean impl_browser_export_to_file (GncHtml* self, const gchar* filepath); +static void impl_browser_print (GncHtml* self,const gchar* jobname); +static void impl_browser_cancel (GncHtml* self); +static void impl_browser_set_parent (GncHtml* self, GtkWindow* parent); + +static void gnc_html_open_scm (GncHtmlBrowser* self, const gchar * location, + const gchar * label, int newwin); + +static char *extract_base_name (URLType type, const gchar* path); + +static gboolean load_to_stream (GncHtmlBrowser* self, URLType type, + const gchar* location, const gchar* label); + +/* hashes handlers for handling different URLType data */ +extern GHashTable* gnc_html_url_handlers; + +static void +ws_close_cb (GncWsServer *gws, const gchar *id, gpointer user_data) +{ + GncHtmlBrowser *ghb = user_data; + GncHtmlBrowserPrivate *priv = GNC_HTML_BROWSER_GET_PRIVATE(ghb); + + if (!priv->file_name) + return; + +g_print("%s called, self %p, '%s', id '%s'\n",__FUNCTION__, ghb, priv->file_name, id); + + if (g_str_has_suffix (priv->file_name, id)) + { +g_print(" use close\n"); + } +} + +static void +ws_open_cb (GncWsServer *gws, const gchar *id, gpointer user_data) +{ + GncHtmlBrowser *ghb = user_data; + GncHtmlBrowserPrivate *priv = GNC_HTML_BROWSER_GET_PRIVATE(ghb); + + if (!priv->file_name) + return; + +g_print("%s called, self %p, '%s', id '%s'\n",__FUNCTION__, ghb, priv->file_name, id); + + if (g_str_has_suffix (priv->file_name, id)) + { +g_print(" use open\n"); + } +} + +static void +ws_message_cb (GncWsServer *gws, const gchar *id, const gchar *message, gpointer user_data) +{ + GncHtmlBrowser *ghb = user_data; + GncHtmlBrowserPrivate *priv = GNC_HTML_BROWSER_GET_PRIVATE(ghb); + + if (!priv->file_name) + return; + +g_print("%s called, self %p, message is '%s', '%s', id '%s'\n",__FUNCTION__, ghb, message, priv->file_name, id); + + if (g_str_has_suffix (priv->file_name, id)) + { + GncHtml *self = (GncHtml*)ghb; + GncHTMLUrlCB url_handler; + + gchar *location = NULL, *label = NULL, *uri = NULL; +g_print(" use message\n"); + const gchar *scheme = gnc_html_parse_url (self, message, &location, &label); + URLType type = scheme; + gboolean stream_loaded = FALSE; + +g_print(" location '%s', label '%s'\n", location, label); + + if (gnc_html_url_handlers) + url_handler = (GncHTMLUrlCB)g_hash_table_lookup ((GHashTable*)gnc_html_url_handlers, type); + else + url_handler = NULL; + + if (url_handler) + { + GNCURLResult result; + gboolean ok; + + result.load_to_stream = FALSE; + result.url_type = type; + result.location = NULL; + result.label = NULL; + result.base_type = URL_TYPE_FILE; + result.base_location = NULL; + result.error_message = NULL; +// result.parent = GTK_WINDOW(gnc_html_get_widget(self)); + result.parent = NULL; + + ok = url_handler (location, label, FALSE, &result); +g_print(" ok is %d\n", ok); + + if (!ok) + { + if (result.error_message) + gnc_error_dialog (GTK_WINDOW (priv->base.parent), "%s", result.error_message); + else + { + /* %s is a URL (some location somewhere). */ + gnc_error_dialog (GTK_WINDOW(priv->base.parent), + _("There was an error accessing %s."), location ); + } + + if (priv->base.load_cb) + { + priv->base.load_cb (GNC_HTML(self), result.url_type, + location, label, priv->base.load_cb_data); + } + } + else if (result.load_to_stream) + { + gnc_html_history_node *hnode; + const char *new_location; + const char *new_label; + + new_location = result.location ? result.location : location; + new_label = result.label ? result.label : label; + hnode = gnc_html_history_node_new (result.url_type, new_location, new_label); + gnc_html_history_append (priv->base.history, hnode); + + g_free (priv->base.base_location); + priv->base.base_type = result.base_type; + priv->base.base_location = g_strdup (extract_base_name (result.base_type, new_location)); + + DEBUG("resetting base location to %s", + priv->base.base_location ? priv->base.base_location : "(null)"); + + stream_loaded = load_to_stream (GNC_HTML_BROWSER(self), + result.url_type, + new_location, new_label); + + if (stream_loaded && priv->base.load_cb != NULL) + { + priv->base.load_cb (GNC_HTML(self), result.url_type, + new_location, new_label, priv->base.load_cb_data); + } + } + g_free (result.location); + g_free (result.label); + g_free (result.base_location); + g_free (result.error_message); + } + g_free (uri); + g_free (location); + g_free (label); + } +} + +static void +gnc_html_browser_init (GncHtmlBrowser* self) +{ + GncHtmlBrowserPrivate* priv; + GncHtmlBrowserPrivate* new_priv; +g_print("%s called, self %p\n",__FUNCTION__, self); + new_priv = g_realloc (GNC_HTML(self)->priv, sizeof(GncHtmlBrowserPrivate)); + priv = self->priv = new_priv; + GNC_HTML(self)->priv = (GncHtmlPrivate*)priv; + + priv->html_string = NULL; + priv->web_view = gtk_label_new ("Just a Label"); + + priv->gws = gnc_ws_server_new (); + priv->file_name = NULL; + +// g_object_ref (priv->gws); + +g_print(" gws %p\n", priv->gws); + + gtk_container_add (GTK_CONTAINER(priv->base.container), + GTK_WIDGET(priv->web_view)); + + g_object_ref_sink (priv->base.container); + + /* signals */ + g_signal_connect (G_OBJECT(priv->gws), "close", + G_CALLBACK (ws_close_cb), self); + + g_signal_connect (G_OBJECT(priv->gws), "open", + G_CALLBACK (ws_open_cb), self); + + g_signal_connect (G_OBJECT(priv->gws), "message", + G_CALLBACK (ws_message_cb), self); + + LEAVE("retval %p", self); +} + +static void +gnc_html_browser_class_init (GncHtmlBrowserClass* klass) +{ + GObjectClass* gobject_class = G_OBJECT_CLASS(klass); + GncHtmlClass* html_class = GNC_HTML_CLASS(klass); +g_print("%s called\n",__FUNCTION__); + gobject_class->dispose = gnc_html_browser_dispose; + gobject_class->finalize = gnc_html_browser_finalize; + + html_class->show_url = impl_browser_show_url; + html_class->show_data = impl_browser_show_data; + html_class->reload = impl_browser_reload; + html_class->copy_to_clipboard = impl_browser_copy_to_clipboard; + html_class->export_to_file = impl_browser_export_to_file; + html_class->print = impl_browser_print; + html_class->cancel = impl_browser_cancel; + html_class->set_parent = impl_browser_set_parent; +} + +static void +gnc_html_browser_dispose (GObject* obj) +{ + GncHtmlBrowser* self = GNC_HTML_BROWSER(obj); + GncHtmlBrowserPrivate* priv = GNC_HTML_BROWSER_GET_PRIVATE(self); +g_print("%s called\n",__FUNCTION__); + if (priv->web_view != NULL) + { + gtk_container_remove (GTK_CONTAINER(priv->base.container), + GTK_WIDGET(priv->web_view)); + + priv->web_view = NULL; + } + + guint signals = g_signal_handlers_disconnect_matched (priv->gws, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, self); + +g_print(" Number of signal disconnected: %d\n", signals); + + + if (priv->html_string != NULL) + { + g_free (priv->html_string); + priv->html_string = NULL; + } + +// g_object_unref (priv->gws); + + priv->gws = NULL; + priv->file_name = NULL; + + G_OBJECT_CLASS(gnc_html_browser_parent_class)->dispose (obj); +} + +static void +gnc_html_browser_finalize (GObject* obj) +{ + GncHtmlBrowser* self = GNC_HTML_BROWSER(obj); +g_print("%s called\n",__FUNCTION__); + + self->priv = NULL; + + G_OBJECT_CLASS(gnc_html_browser_parent_class)->finalize (obj); +} + +/*****************************************************************************/ + +static char* +extract_base_name (URLType type, const gchar* path) +{ + gchar machine_rexp[] = "^(//[^/]*)/*(/.*)?$"; + gchar path_rexp[] = "^/*(.*)/+([^/]*)$"; + regex_t compiled_m, compiled_p; + regmatch_t match[4]; + gchar * machine = NULL, * location = NULL, * base = NULL; + gchar * basename = NULL; +g_print("%s called, path '%s'\n",__FUNCTION__, path); + DEBUG(" "); + if (!path) return NULL; + + regcomp (&compiled_m, machine_rexp, REG_EXTENDED); + regcomp (&compiled_p, path_rexp, REG_EXTENDED); + + if (!g_strcmp0 (type, URL_TYPE_HTTP) || + !g_strcmp0 (type, URL_TYPE_SECURE) || + !g_strcmp0 (type, URL_TYPE_FTP)) + { + /* step 1: split the machine name away from the path + * components */ + if (!regexec (&compiled_m, path, 4, match, 0)) + { + /* $1 is the machine name */ + if (match[1].rm_so != -1) + { + machine = g_strndup (path + match[1].rm_so, + match[1].rm_eo - match[1].rm_so); + } + /* $2 is the path */ + if (match[2].rm_so != -1) + { + location = g_strndup (path + match[2].rm_so, + match[2].rm_eo - match[2].rm_so); + } + } + } + else + { + location = g_strdup (path); + } + /* step 2: split up the path into prefix and file components */ + if (location) + { + if (!regexec (&compiled_p, location, 4, match, 0)) + { + if (match[1].rm_so != -1) + { + base = g_strndup (location + match[1].rm_so, + match[1].rm_eo - match[1].rm_so); + } + else + { + base = NULL; + } + } + } + + regfree (&compiled_m); + regfree (&compiled_p); + + if (machine) + { + if (base && (strlen (base) > 0)) + { + basename = g_strconcat (machine, "/", base, "/", NULL); + } + else + { + basename = g_strconcat (machine, "/", NULL); + } + } + else + { + if (base && (strlen (base) > 0)) + { + basename = g_strdup (base); + } + else + { + basename = NULL; + } + } + + g_free (machine); + g_free (base); + g_free (location); + return basename; +} + +static gboolean +http_allowed () +{ +g_print("%s called\n",__FUNCTION__); + return TRUE; +} + +static gboolean +https_allowed () +{ +g_print("%s called\n",__FUNCTION__); + return TRUE; +} + +static gchar* +handle_embedded_object (GncHtmlBrowser* self, gchar* html_str) +{ + // Find the tag and get the classid from it. This will provide the correct + // object callback handler. Pass the entity text to the handler. What should + // come back is embedded image information. + gchar* remainder_str = html_str; + gchar* object_tag; + gchar* end_object_tag; + gchar* object_contents; + gchar* html_str_start = NULL; + gchar* html_str_middle; + gchar* html_str_result = NULL; + gchar* classid_start; + gchar* classid_end; + gchar* classid_str; + gchar* new_chunk; + GncHTMLObjectCB h; +g_print("%s called\n",__FUNCTION__); + object_tag = g_strstr_len (remainder_str, -1, ""); + if (end_object_tag == NULL) + { + /* Hmmm... no object end tag + Return the original html string because we can't properly parse it */ + g_free (classid_str); + g_free (html_str_result); + return g_strdup (html_str); + } + end_object_tag += strlen ( "" ); + object_contents = g_strndup (object_tag, (end_object_tag - object_tag)); + + h = g_hash_table_lookup (gnc_html_object_handlers, classid_str); + if (h != NULL) + { + (void)h(GNC_HTML(self), object_contents, &html_str_middle); + } + else + { + html_str_middle = g_strdup_printf ( "No handler found for classid \"%s\"", classid_str); + } + + html_str_start = html_str_result; + new_chunk = g_strndup (remainder_str, (object_tag - remainder_str)); + if (!html_str_start) + html_str_result = g_strconcat (new_chunk, html_str_middle, NULL); + else + html_str_result = g_strconcat (html_str_start, new_chunk, html_str_middle, NULL); + + g_free (html_str_start); + g_free (new_chunk); + g_free (html_str_middle); + + remainder_str = end_object_tag; + object_tag = g_strstr_len (remainder_str, -1, "base.parent), "%s", + _("Secure HTTP access is disabled. " + "You can enable it in the Network section of " + "the Preferences dialog.")); + break; + } + } + + if (!http_allowed()) + { + gnc_error_dialog (GTK_WINDOW (priv->base.parent), "%s", + _("Network HTTP access is disabled. " + "You can enable it in the Network section of " + "the Preferences dialog.")); + } + else + { + gnc_build_url (type, location, label); + } + } + else + { + PWARN("load_to_stream for inappropriate type\n" + "\turl = '%s#%s'\n", + location ? location : "(null)", + label ? label : "(null)" ); + fdata = g_strdup_printf (error_404_format, + _(error_404_title), _(error_404_body)); + +g_print(" Error2: '%s', '%s'\n", fdata, BASE_URI_NAME); + + g_free (fdata); + } + } + while (FALSE); + return TRUE; +} + +/******************************************************************** + * gnc_html_open_scm + * insert some scheme-generated HTML + ********************************************************************/ + +static void +gnc_html_open_scm (GncHtmlBrowser* self, const gchar * location, + const gchar * label, int newwin) +{ +g_print("%s called\n",__FUNCTION__); + PINFO("location='%s'", location ? location : "(null)"); +} + + +/******************************************************************** + * impl_browser_show_data + * display some HTML that the creator of the gnc-html got from + * somewhere. + ********************************************************************/ + +static void +impl_browser_show_data (GncHtml* self, const gchar* data, int datalen) +{ + GncHtmlBrowserPrivate* priv; +#define TEMPLATE_REPORT_FILE_NAME "gnc-report-XXXXXX.html" + int fd; + gchar* uri; + gchar *filename; +g_print("%s called\n",__FUNCTION__); + g_return_if_fail (self != NULL); + g_return_if_fail (GNC_IS_HTML_BROWSER(self)); + + ENTER("datalen %d, data %20.20s", datalen, data); + + priv = GNC_HTML_BROWSER_GET_PRIVATE(self); + + /* Export the HTML to a file and load the file URI. On Linux, this seems to get around some + security problems (otherwise, it can complain that embedded images aren't permitted to be + viewed because they are local resources). On Windows, this allows the embedded images to + be viewed (maybe for the same reason as on Linux, but I haven't found where it puts those + messages. */ + +g_print(" priv->filename '%s'\n", priv->file_name); + + if (!priv->file_name) + { + fd = g_file_open_tmp (TEMPLATE_REPORT_FILE_NAME, &filename, NULL); + priv->file_name = g_strdup (filename); + } + else + { + filename = g_strdup (priv->file_name); + g_remove (filename); + fd = g_open (filename, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR); + } + +g_print(" priv->filename '%s'\n", priv->file_name); + + impl_browser_export_to_file (self, filename); + close (fd); +#ifdef G_OS_WIN32 + uri = g_strdup_printf ("file:///%s", filename); +#else + uri = g_strdup_printf ("file://%s", filename); +#endif + g_free (filename); + DEBUG("Loading uri '%s'", uri); + + GError *error = NULL; + gboolean retval = g_app_info_launch_default_for_uri (uri, NULL, &error); + + if (!retval) + { + PWARN("Error loading report file: %s, message: %s", uri, error->message); + gnc_error_dialog (GTK_WINDOW(priv->base.parent), "%s", + _(error->message)); + + g_error_free (error); + } + g_free (uri); + + LEAVE(""); +} + +/******************************************************************** + * impl_browser_show_url + * + * open a URL. This is called when the user clicks a link or + * for the creator of the gnc_html window to explicitly request + * a URL. + ********************************************************************/ + +static void +impl_browser_show_url (GncHtml* self, URLType type, + const gchar* location, const gchar* label, + gboolean new_window_hint) +{ + GncHTMLUrlCB url_handler; + gboolean new_window; + GncHtmlBrowserPrivate* priv; + gboolean stream_loaded = FALSE; +g_print("%s called\n",__FUNCTION__); + g_return_if_fail (self != NULL ); + g_return_if_fail (GNC_IS_HTML_BROWSER(self)); + g_return_if_fail (location != NULL ); + + priv = GNC_HTML_BROWSER_GET_PRIVATE(self); + + /* make sure it's OK to show this URL type in this window */ + if (new_window_hint == 0) + { + if (priv->base.urltype_cb) + new_window = !((priv->base.urltype_cb)(type)); + else + new_window = FALSE; + } + else + new_window = TRUE; + + if (!new_window) + gnc_html_cancel (GNC_HTML(self)); + + if (gnc_html_url_handlers) + url_handler = g_hash_table_lookup (gnc_html_url_handlers, type); + else + url_handler = NULL; + + if (url_handler) + { + GNCURLResult result; + gboolean ok; + + result.load_to_stream = FALSE; + result.url_type = type; + result.location = NULL; + result.label = NULL; + result.base_type = URL_TYPE_FILE; + result.base_location = NULL; + result.error_message = NULL; + result.parent = GTK_WINDOW(priv->base.parent); + +g_print(" result parent window is %p\n", GTK_WINDOW(priv->base.parent)); + + ok = url_handler (location, label, new_window, &result); +g_print(" ok is %d\n", ok); + if (!ok) + { + if (result.error_message) + gnc_error_dialog (GTK_WINDOW(priv->base.parent), "%s", result.error_message); + else + { + /* %s is a URL (some location somewhere). */ + gnc_error_dialog (GTK_WINDOW(priv->base.parent), _("There was an error accessing %s."), location); + } + + if (priv->base.load_cb) + { + priv->base.load_cb (GNC_HTML(self), result.url_type, + location, label, priv->base.load_cb_data); + } + } + else if (result.load_to_stream) + { + gnc_html_history_node *hnode; + const char *new_location; + const char *new_label; + + new_location = result.location ? result.location : location; + new_label = result.label ? result.label : label; + hnode = gnc_html_history_node_new (result.url_type, new_location, new_label); + + gnc_html_history_append (priv->base.history, hnode); + + g_free( priv->base.base_location ); + priv->base.base_type = result.base_type; + priv->base.base_location = g_strdup (extract_base_name (result.base_type, new_location)); + DEBUG("resetting base location to %s", + priv->base.base_location ? priv->base.base_location : "(null)"); + + stream_loaded = load_to_stream (GNC_HTML_BROWSER(self), + result.url_type, + new_location, new_label); + + if (stream_loaded && priv->base.load_cb != NULL) + { + priv->base.load_cb (GNC_HTML(self), result.url_type, + new_location, new_label, priv->base.load_cb_data); + } + } + g_free (result.location); + g_free (result.label); + g_free (result.base_location); + g_free (result.error_message); + return; + } + + if (g_strcmp0 (type, URL_TYPE_SCHEME) == 0) + gnc_html_open_scm (GNC_HTML_BROWSER(self), location, label, new_window); + else if (g_strcmp0 (type, URL_TYPE_JUMP) == 0) + { + /* Webkit jumps to the anchor on its own */ + } + else if (g_strcmp0( type, URL_TYPE_SECURE ) == 0 || + g_strcmp0( type, URL_TYPE_HTTP ) == 0 || + g_strcmp0( type, URL_TYPE_FILE ) == 0) + { + do + { + if ( g_strcmp0 (type, URL_TYPE_SECURE) == 0) + { + if (!https_allowed()) + { + gnc_error_dialog (GTK_WINDOW (priv->base.parent), "%s", + _("Secure HTTP access is disabled. " + "You can enable it in the Network section of " + "the Preferences dialog.")); + break; + } + } + + if (g_strcmp0 (type, URL_TYPE_HTTP) == 0) + { + if (!http_allowed()) + { + gnc_error_dialog (GTK_WINDOW (priv->base.parent), "%s", + _("Network HTTP access is disabled. " + "You can enable it in the Network section of " + "the Preferences dialog.")); + break; + } + } + + priv->base.base_type = type; + + if (priv->base.base_location != NULL) + g_free (priv->base.base_location); + priv->base.base_location = extract_base_name (type, location); + + /* FIXME : handle new_window = 1 */ + gnc_html_history_append (priv->base.history, + gnc_html_history_node_new (type, location, label)); + stream_loaded = load_to_stream (GNC_HTML_BROWSER(self), + type, location, label); + + } + while (FALSE); + } + else + PERR("URLType %s not supported.", type); + + if (stream_loaded && priv->base.load_cb != NULL) + (priv->base.load_cb)(GNC_HTML(self), type, location, label, priv->base.load_cb_data); +} + + +/******************************************************************** + * impl_browser_reload + * reload the current page + * if force_rebuild is TRUE, the report is recreated, if FALSE, report + * is reloaded by browser + ********************************************************************/ + +static void +impl_browser_reload (GncHtml* self, gboolean force_rebuild) +{ + GncHtmlBrowserPrivate* priv; +g_print("%s called, %d\n",__FUNCTION__, force_rebuild); + + g_return_if_fail (self != NULL); + g_return_if_fail (GNC_IS_HTML_BROWSER(self)); + + priv = GNC_HTML_BROWSER_GET_PRIVATE(self); + + if (force_rebuild) + { + gnc_html_history_node *n = gnc_html_history_get_current (priv->base.history); + if (n != NULL) + gnc_html_show_url (self, n->type, n->location, n->label, 0); + } + + gnc_ws_server_send_message (priv->gws, priv->file_name, "RELOAD"); +} + + +/******************************************************************** + * gnc_html_browser_new + * create and set up a new browser widget. + ********************************************************************/ + +GncHtml* +gnc_html_browser_new (void) +{ + GncHtmlBrowser* self = g_object_new (GNC_TYPE_HTML_BROWSER, NULL); +g_print("%s called\n",__FUNCTION__); + return GNC_HTML(self); +} + +/******************************************************************** + * impl_browser_cancel + * cancel any outstanding HTML fetch requests. + ********************************************************************/ + +static void +impl_browser_cancel (GncHtml* self) +{ +g_print("%s called\n",__FUNCTION__); +} + +static void +impl_browser_copy_to_clipboard (GncHtml* self) +{ +g_print("%s called\n",__FUNCTION__); +} + +/************************************************************** + * impl_browser_export_to_file + * + * @param self GncHtmlBrowser object + * @param filepath Where to write the HTML + * @return TRUE if successful, FALSE if unsuccessful + **************************************************************/ +static gboolean +impl_browser_export_to_file (GncHtml* self, const char *filepath) +{ + FILE *fh; + GncHtmlBrowserPrivate* priv; +g_print("%s called\n",__FUNCTION__); + g_return_val_if_fail (self != NULL, FALSE); + g_return_val_if_fail (GNC_IS_HTML_BROWSER(self), FALSE); + g_return_val_if_fail (filepath != NULL, FALSE); + + priv = GNC_HTML_BROWSER_GET_PRIVATE(self); + if (priv->html_string == NULL) + { + return FALSE; + } + fh = g_fopen (filepath, "w"); + if (fh != NULL) + { + gint written; + gint len = strlen (priv->html_string); + + written = fwrite (priv->html_string, 1, len, fh); + fclose (fh); + + if (written != len) + return FALSE; + + return TRUE; + } + else + return FALSE; +} + +static void +impl_browser_print (GncHtml* self, const gchar* jobname) +{ + GncHtmlBrowserPrivate* priv; +g_print("%s called\n",__FUNCTION__); + g_return_if_fail (self != NULL ); + g_return_if_fail (GNC_IS_HTML_BROWSER(self)); + + priv = GNC_HTML_BROWSER_GET_PRIVATE(self); + + gnc_ws_server_send_message (priv->gws, priv->file_name, "PRINT"); +} + +static void +impl_browser_set_parent (GncHtml* self, GtkWindow* parent) +{ + GncHtmlBrowserPrivate* priv; +g_print("%s called\n",__FUNCTION__); + g_return_if_fail (self != NULL ); + g_return_if_fail (GNC_IS_HTML_BROWSER(self)); + + priv = GNC_HTML_BROWSER_GET_PRIVATE(self); + priv->base.parent = GTK_WIDGET(parent); +} diff --git a/gnucash/html/gnc-html-browser.h b/gnucash/html/gnc-html-browser.h new file mode 100644 index 0000000000..66811f4727 --- /dev/null +++ b/gnucash/html/gnc-html-browser.h @@ -0,0 +1,64 @@ +/******************************************************************** + * gnc-html-browser.h -- display html with gnc special tags * + * Copyright (C) 2024 Bob Fewell * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License* + * along with this program; if not, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * +\********************************************************************/ + +#ifndef GNC_HTML_BROWSER_H +#define GNC_HTML_BROWSER_H + +#include +#include "gnc-html.h" + +G_BEGIN_DECLS + +#define GNC_TYPE_HTML_BROWSER (gnc_html_browser_get_type()) +#define GNC_HTML_BROWSER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GNC_TYPE_HTML_BROWSER, GncHtmlBrowser)) +#define GNC_HTML_BROWSER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GNC_TYPE_HTML_BROWSER, GncHtmlBrowserClass)) +#define GNC_IS_HTML_BROWSER(o) (G_TYPE_CHECK_INSTANCE_TYPE((o), GNC_TYPE_HTML_BROWSER)) +#define GNC_IS_HTML_BROWSER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE((k), GNC_TYPE_HTML_BROWSER)) +#define GNC_HTML_BROWSER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS((o), GNC_TYPE_HTML_BROWSER, GncHtmlBrowserClass)) + +typedef struct _GncHtmlBrowser GncHtmlBrowser; +typedef struct _GncHtmlBrowserClass GncHtmlBrowserClass; +typedef struct _GncHtmlBrowserPrivate GncHtmlBrowserPrivate; + +/** Key for saving the PDF-export directory in the print settings */ +#define GNC_GTK_PRINT_SETTINGS_EXPORT_DIR "gnc-pdf-export-directory" + +struct _GncHtmlBrowser +{ + GncHtml parent_instance; + + /*< private >*/ + GncHtmlBrowserPrivate* priv; +}; + +struct _GncHtmlBrowserClass +{ + GncHtmlClass parent_class; +}; + +GType gnc_html_browser_get_type (void); + +GncHtml* gnc_html_browser_new (void); + +G_END_DECLS + +#endif diff --git a/gnucash/html/gnc-html-factory.c b/gnucash/html/gnc-html-factory.c index 8e2633af58..218e886449 100644 --- a/gnucash/html/gnc-html-factory.c +++ b/gnucash/html/gnc-html-factory.c @@ -26,7 +26,7 @@ #include #include "gnc-html.h" -#include "gnc-html-webkit.h" +#include "gnc-html-browser.h" #include "qoflog.h" #include "gnc-engine.h" @@ -35,13 +35,13 @@ /* indicates the debugging module that this .o belongs to. */ G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_HTML; -GncHtml* gnc_html_factory_create_html( void ) +GncHtml* gnc_html_factory_create_html (void) { - return gnc_html_webkit_new(); + return gnc_html_browser_new (); } gboolean -gnc_html_engine_supports_css( void ) +gnc_html_engine_supports_css (void) { return TRUE; } diff --git a/gnucash/html/gnc-html.c b/gnucash/html/gnc-html.c index c5e121fd2e..ca350d48a2 100644 --- a/gnucash/html/gnc-html.c +++ b/gnucash/html/gnc-html.c @@ -528,13 +528,9 @@ gnc_html_export_to_file( GncHtml* self, const gchar* filepath ) return FALSE; } } -#ifdef WEBKIT1 -void -gnc_html_print (GncHtml* self, const char *jobname, gboolean export_pdf) -#else + void gnc_html_print (GncHtml* self, const char *jobname) -#endif { g_return_if_fail( self != NULL ); g_return_if_fail( jobname != NULL ); @@ -542,11 +538,7 @@ gnc_html_print (GncHtml* self, const char *jobname) if ( GNC_HTML_GET_CLASS(self)->print != NULL ) { -#ifdef WEBKIT1 - GNC_HTML_GET_CLASS(self)->print (self, jobname, export_pdf); -#else GNC_HTML_GET_CLASS(self)->print (self, jobname); -#endif } else { @@ -589,17 +581,13 @@ gnc_html_get_webview( GncHtml* self ) if (sw_list) // the scroll window has only one child { -#ifdef WEBKIT1 - webview = sw_list->data; -#else GList *vp_list = gtk_container_get_children (GTK_CONTAINER(sw_list->data)); - + if (vp_list) // the viewport has only one child { webview = vp_list->data; g_list_free (vp_list); } -#endif } g_list_free (sw_list); return webview; diff --git a/gnucash/html/gnc-html.h b/gnucash/html/gnc-html.h index 26bee8044b..541516154f 100644 --- a/gnucash/html/gnc-html.h +++ b/gnucash/html/gnc-html.h @@ -138,11 +138,7 @@ struct _GncHtmlClass void (*reload)( GncHtml* html, gboolean force_rebuild ); void (*copy_to_clipboard)( GncHtml* html ); gboolean (*export_to_file)( GncHtml* html, const gchar* file ); -#ifdef WEBKIT1 - void (*print) (GncHtml* html, const gchar* jobname, gboolean export_pdf); -#else void (*print) (GncHtml* html, const gchar* jobname); -#endif void (*cancel)( GncHtml* html ); URLType (*parse_url)( GncHtml* html, const gchar* url, gchar** url_location, gchar** url_label ); @@ -203,25 +199,13 @@ void gnc_html_copy_to_clipboard( GncHtml* html ); */ gboolean gnc_html_export_to_file( GncHtml* html, const gchar* filename ); -#ifdef WEBKIT1 -/** - * Prints the report. - * - * @param html GncHtml object - * @param jobname A jobname for identifying the print job or to provide - * an output filename. - * @param export_pdf If TRUE write a PDF file using the jobname for a - * filename; otherwise put up a print dialog. - */ -void gnc_html_print (GncHtml* html, const char* jobname, gboolean export_pdf); -#else /** * Prints the report. * * @param html GncHtml object */ void gnc_html_print (GncHtml* html, const char* jobname); -#endif + /** * Cancels the current operation * diff --git a/gnucash/html/gnc-ws-protocol.c b/gnucash/html/gnc-ws-protocol.c new file mode 100644 index 0000000000..0235406287 --- /dev/null +++ b/gnucash/html/gnc-ws-protocol.c @@ -0,0 +1,338 @@ +/******************************************************************** + * gnc-ws-protocol.c -- basic websocket server protocol * + * Copyright (C) 2024 Bob Fewell * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License* + * along with this program; if not, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * + ********************************************************************/ + +#include +#include +#include +#include + +#include "gnc-ws-protocol.h" + +#define WS_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +#define WS_HS_ACCEPT "HTTP/1.1 101 Switching Protocols\r\n"\ + "Upgrade: websocket\r\n"\ + "Connection: Upgrade\r\n"\ + "Sec-WebSocket-Accept: " + +#ifdef skip +static void +print_hex (const gchar *s) +{ + while (*s) + g_printf("%02x", (unsigned int) *s++); + g_printf("\n"); +} +#endif + +#ifdef skip +static void +print_bytes (GBytes *bytes_in) +{ + gsize length; + const guint8 *array = g_bytes_get_data (bytes_in, &length); + GString *printable; + guint i = 0; + + printable = g_string_new ("["); + + while (i < length) + { + if (i > 0) + g_string_append_c (printable, ' '); + g_string_append_printf (printable, "%02x", array[i++]); + } + g_string_append_c (printable, ']'); + + gchar *test = g_string_free (printable, FALSE); + +g_print("Print bytes: '%s' %" G_GUINT64_FORMAT "\n", test, length); + + g_free (test); +} +#endif + + +static gchar* +find_header_text (gchar **header, const gchar *find_text) +{ + gchar *ret_text = NULL; + guint parts = g_strv_length (header); + + for (guint i = 0; i < parts; i++) + { + if (g_strstr_len (header[i], -1, find_text)) + { + ret_text = g_strdup (header[i]); + break; + } + } + return ret_text; +} + +#define BUFFER_LEN 200 // only dealing with max message of 125 + +static GBytes * +make_frame (guint8 opcode, const gchar *message) +{ + guint8 first_byte = (0x80 | opcode); + guint8 msg_len = strlen (message); + + if (msg_len > 125) + { +g_print("## Not supporting messages lonmger than 125 characters ##\n"); + return NULL; + } + + guint8 header[2]; + + header[0] = first_byte; + header[1] = msg_len; + + static guint8 array_buf[BUFFER_LEN]; + + memset (array_buf, 0, BUFFER_LEN); + memcpy (array_buf, header, 2); + memcpy (array_buf + 2, message, msg_len); + + GBytes *test = g_bytes_new (array_buf, msg_len + 2); + + return test; +} + +GBytes * +gnc_ws_send_message (const gchar *message) +{ + return make_frame (OPCODE_TEXT, message); +} + + +static gboolean +parse_frame (GBytes *bytes_in, gint *opcode, GBytes **bytes_out) +{ + gsize length; + const guint8 *array = g_bytes_get_data (bytes_in, &length); + + const guint8 first_byte = array[0]; + const guint8 second_byte = array[1]; + + gboolean fin = (first_byte >> 7) & 1; + gboolean rsv1 = (first_byte >> 6) & 1; + gboolean rsv2 = (first_byte >> 5) & 1; + gboolean rsv3 = (first_byte >> 4) & 1; + *opcode = first_byte & 0xf; + + if (rsv1 || rsv2 || rsv3) + { +g_print("WebSocketError: Received frame with non-zero reserved bits\n"); + return FALSE; + } + + if (fin == 0 && *opcode == OPCODE_CONTINUATION) + { +g_print("WebSocketError: Received new fragment frame with non-zero opcode\n"); + return FALSE; + } + + gboolean has_mask = (second_byte >> 7) & 1; + guint8 payload_len = second_byte & 0x7f; + gint offset = 2; + + if ((*opcode > 0x7) && (payload_len > 125)) + { +g_print("WebSocketError: Control frame payload cannot be larger than 125 bytes\n"); + return FALSE; + } + + if (payload_len > 125) + { +g_print("## Payload length greater than 125 bytes, not suported ##\n"); + return FALSE; + } + + if (has_mask) + { + guint8 masks[4]; + gint i = 0; + + masks[0] = array[offset]; + masks[1] = array[offset + 1]; + masks[2] = array[offset + 2]; + masks[3] = array[offset + 3]; + + offset = offset + 4; + + guint8 mypayload[length]; + + while ((i + offset) < length) + { + guint8 data = array[i + offset] ^ masks[i % 4]; + mypayload[i] = data; + i++; + } + *bytes_out = g_bytes_new (mypayload, length - offset); + } + else + *bytes_out = g_bytes_new_from_bytes (bytes_in, offset, length - offset); + + return TRUE; +} + +gchar * +gnc_ws_parse_bytes_in (GBytes *bytes_in, gint *opcode_out) +{ + GBytes *bytes_out = NULL; + gint opcode; + gboolean ok = FALSE; + + ok = parse_frame (bytes_in, &opcode, &bytes_out); + + if (!ok) + { +g_print("## Parsing bytes in failed ##\n"); + return NULL; + } + + *opcode_out = opcode; + + if (opcode == OPCODE_TEXT) + { + gsize length; + const guint8 *array = g_bytes_get_data (bytes_out, &length); + GString *data_out = g_string_new (NULL); + gint i = 0; + + while (i < length) + { + guint8 data = array[i]; + + g_string_append_printf (data_out, "%c", data); + i++; + } + return g_string_free (data_out, FALSE); + } + return NULL; +} + + +gchar * +gnc_ws_make_handshake (const gchar *message, gchar **id) +{ + gchar **message_split = g_strsplit (message, "\r\n", -1); + gchar *message_out = NULL; + gchar *key = NULL; + + gchar *message_test = find_header_text (message_split, "GET"); + + if (!message_test) + { +g_print(" Error: No Get\n"); + return NULL; + } + else + { + gchar *ptr = g_strrstr (message_test, " "); + + if (ptr) + *id = g_utf8_substring (message_test, 5, ptr - message_test); + } + g_free (message_test); + + message_test = find_header_text (message_split, "Upgrade"); + if (!message_test) + { +g_print(" Error: No upgrade hdr\n"); + return NULL; + } + g_free (message_test); + + message_test = find_header_text (message_split, "Sec-WebSocket-Version"); + if (message_test) + { + if (!g_strstr_len (message_test , -1, "13")) + { +g_print(" Error: Unsupported version\n"); + g_free (message_test); + return NULL; + } + } + else + { +g_print(" Error: Did not find websocket version\n"); + return NULL; + } + g_free (message_test); + + message_test = find_header_text (message_split, "Sec-WebSocket-Key"); + + if (message_test) + { + gchar *ptr = g_strstr_len (message_test , -1, ":"); + + if (ptr) + { + key = g_strstrip (g_strdup (ptr + 1)); + + gsize out_len; + guchar *decoded = g_base64_decode (key, &out_len); + + if (out_len != 16) + { +g_print(" Error: Key wrong length\n"); + g_free (message_test); + return NULL; + } + g_free (decoded); + } + else + { +g_print(" Error: Key not found\n"); + g_free (message_test); + return NULL; + } + } + else + { +g_print(" Error: Key not found\n"); + g_free (message_test); + return NULL; + } + g_free (message_test); + + guint8 buf[100]; + gsize bsize = 100; + + GChecksum *cs = g_checksum_new (G_CHECKSUM_SHA1); + + gchar *digest = g_strconcat (key, WS_KEY, NULL); + + g_checksum_update (cs, (guint8*)digest, -1); + g_checksum_get_digest (cs, buf, &bsize); + gchar *str = g_base64_encode (buf, bsize); + message_out = g_strconcat (WS_HS_ACCEPT, str, "\r\n\r\n", NULL); + + g_checksum_free (cs); + g_strfreev (message_split); + g_free (key); + + return message_out; +} diff --git a/gnucash/html/gnc-ws-protocol.h b/gnucash/html/gnc-ws-protocol.h new file mode 100644 index 0000000000..bcf8e47e2f --- /dev/null +++ b/gnucash/html/gnc-ws-protocol.h @@ -0,0 +1,45 @@ +/******************************************************************** + * gnc-ws-protocol.h -- basic websocket server protocol * + * Copyright (C) 2024 Bob Fewell * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License* + * along with this program; if not, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * + ********************************************************************/ + +#ifndef WS_PROTOCOL_H +#define WS_PROTOCOL_H + +#include + +#include +#include +#include + +#define OPCODE_CONTINUATION 0x0 +#define OPCODE_TEXT 0x1 +#define OPCODE_BINARY 0x2 +#define OPCODE_CLOSE 0x8 +#define OPCODE_PING 0x9 +#define OPCODE_PONG 0xa + +gchar *gnc_ws_make_handshake (const gchar *message, gchar **id); + +gchar *gnc_ws_parse_bytes_in (GBytes *bytes_in, gint *opcode); + +GBytes *gnc_ws_send_message (const gchar *message); + +#endif diff --git a/gnucash/html/gnc-ws-server.c b/gnucash/html/gnc-ws-server.c new file mode 100644 index 0000000000..46b634ecd8 --- /dev/null +++ b/gnucash/html/gnc-ws-server.c @@ -0,0 +1,399 @@ +/******************************************************************** + * gnc-ws-server.c -- basic websocket server * + * Copyright (C) 2024 Bob Fewell * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License* + * along with this program; if not, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * + ********************************************************************/ + +#include +#include +#include +#include + +#include "gnc-ws-server.h" +#include "gnc-ws-protocol.h" + +#define GNC_WS_SERVER_PATH "gnc-ws-server-path" + +G_DEFINE_TYPE (GncWsServer, gnc_ws_server, G_TYPE_OBJECT) + +#define PORT 8080 +#define BLOCK_SIZE 1024 + +static GncWsServer *gws = NULL; + +/* Signal codes */ +enum +{ + OPEN, + MESSAGE, + CLOSE, + LAST_SIGNAL +}; + +static guint ws_server_signals [LAST_SIGNAL] = { 0 }; + +static void +gnc_ws_server_init (GncWsServer *self) +{ +g_print("%s called\n",__FUNCTION__); +} + +static void +gnc_ws_server_dispose (GObject *object) +{ +g_print("%s called\n",__FUNCTION__); + g_return_if_fail (object != NULL); + + GncWsServer *gws = (GncWsServer*)object; + +g_print("ws service active %d\n", g_socket_service_is_active (gws->service)); + + if (gws->incoming_id != 0) + g_signal_handler_disconnect (G_OBJECT(gws->service), gws->incoming_id); + gws->incoming_id = 0; + + g_socket_service_stop (gws->service); + +g_print(" ws service active %d\n", g_socket_service_is_active (gws->service)); + + G_OBJECT_CLASS(gnc_ws_server_parent_class)->dispose (object); +} + +static void +gnc_ws_server_finalize (GObject *object) +{ +g_print("%s called\n",__FUNCTION__); + + gws = NULL; + + G_OBJECT_CLASS(gnc_ws_server_parent_class)->finalize (object); +} + +static void +gnc_ws_server_class_init (GncWsServerClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); +g_print("%s called\n",__FUNCTION__); + gobject_class->dispose = gnc_ws_server_dispose; + gobject_class->finalize = gnc_ws_server_finalize; + + ws_server_signals [OPEN] = + g_signal_new ("open", + G_OBJECT_CLASS_TYPE(gobject_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); + + ws_server_signals [MESSAGE] = + g_signal_new ("message", + G_OBJECT_CLASS_TYPE(gobject_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 2, + G_TYPE_STRING, + G_TYPE_STRING); + + ws_server_signals [CLOSE] = + g_signal_new ("close", + G_OBJECT_CLASS_TYPE(gobject_class), + G_SIGNAL_RUN_FIRST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); +} + + +static gboolean +send_message_bytes (GSocketConnection *connection, GBytes *bytes) +{ + gboolean ret = TRUE; + GError *error = NULL; + GOutputStream *ostream = g_io_stream_get_output_stream (G_IO_STREAM(connection)); + + gssize data_out = g_output_stream_write_bytes (ostream, bytes, NULL, &error); +#ifdef G_OS_WIN32 +g_print("%s called, connection %p, data out sent %d\n",__FUNCTION__, connection, data_out); +#else +g_print("%s called, connection %p, data out sent %ld\n",__FUNCTION__, connection, data_out); +#endif + if (error != NULL) + { + g_error ("%s", error->message); +g_print("send error '%s'\n", error->message); + g_clear_error (&error); + ret = FALSE; + } + return ret; +} + + +struct ConnHashData +{ + GSocketConnection *connection; + const gchar *id; +}; + +static gboolean +connection_hash_table_find (gpointer key, gpointer value, gpointer user_data) +{ + struct ConnHashData *data = user_data; + + if (g_str_has_suffix (data->id, value)) + { + data->connection = key; + return TRUE; + } + return FALSE; +} + +void +gnc_ws_server_send_message (GncWsServer *gws, const gchar *id, const gchar *message) +{ + struct ConnHashData *data = g_new (struct ConnHashData, 1); + data->id = id; + data->connection = NULL; + +g_print("%s called, id '%s', message '%s'\n",__FUNCTION__, id, message); + + g_hash_table_foreach (gws->connections_hash, + (GHFunc)connection_hash_table_find, + data); + + if (data->connection) + { + GBytes *bytes = gnc_ws_send_message (message); + send_message_bytes (data->connection, bytes); + g_bytes_unref (bytes); + } + g_free (data); +} + + +static gboolean +send_handshake_message (GSocketConnection *connection, const gchar *message) +{ + gboolean ret = TRUE; + GError *error = NULL; + GOutputStream *ostream = g_io_stream_get_output_stream (G_IO_STREAM(connection)); + +#ifdef G_OS_WIN32 +g_print("%s called, connection %p, msg len %d\n",__FUNCTION__, connection, strlen (message)); +#else +g_print("%s called, connection %p, msg len %ld\n",__FUNCTION__, connection, strlen (message)); +#endif + g_output_stream_write (ostream, + message, + strlen (message), + NULL, + &error); + + if (error != NULL) + { + g_error ("%s", error->message); +g_print("send error '%s'\n", error->message); + g_clear_error (&error); + ret = FALSE; + } + return ret; +} + + +struct ConnData +{ + GncWsServer *gws; + GSocketConnection *connection; + char message[BLOCK_SIZE]; +}; + +static void +bytes_ready_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GInputStream *istream = G_INPUT_STREAM(source_object); + GError *error = NULL; + struct ConnData *data = user_data; + GncWsServer *gws = data->gws; + + GBytes *bytes_in = g_input_stream_read_bytes_finish (istream, res, &error); + +g_print("%s called, istream %p\n",__FUNCTION__, istream); + + if (error != NULL) + { + g_error ("%s", error->message); +g_print("bytes service error is: %s\n", error->message); + g_clear_error (&error); + return; + } + + if (g_bytes_get_size (bytes_in) != 0) + { + gint opcode; + gchar *message = gnc_ws_parse_bytes_in (bytes_in, &opcode); + + if ((opcode == OPCODE_TEXT) && message) + { + gchar *id = g_hash_table_lookup (gws->connections_hash, data->connection); + + if (id) + g_signal_emit_by_name (data->gws, "message", id, message); + else +g_print("Error: Lookup Error\n"); + } + g_free (message); + g_bytes_unref (bytes_in); + + if (opcode == OPCODE_CLOSE) + { + gchar *id = g_hash_table_lookup (gws->connections_hash, data->connection); + + if (id) + g_signal_emit_by_name (data->gws, "close", id); + else +g_print("Error: Lookup Error\n"); + + g_hash_table_remove (gws->connections_hash, data->connection); + + return; + } + g_input_stream_read_bytes_async (istream, 8192, G_PRIORITY_DEFAULT, + NULL, bytes_ready_cb, user_data); + } +} + +static void +message_ready_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GInputStream *istream = G_INPUT_STREAM(source_object); + GError *error = NULL; + struct ConnData *data = user_data; + int count; + + count = g_input_stream_read_finish (istream, res, &error); + +g_print("%s called, istream %p, count %d\n",__FUNCTION__, istream, count); + + if (count == -1) + { +g_print ("Error: receiving message\n"); + if (error != NULL) + { +g_print("Error: incoming stream error: %s\n", error->message); + g_clear_error (&error); + return; + } + } + + gchar *id = NULL; + gchar *handshake_message = gnc_ws_make_handshake (data->message, &id); + + if (handshake_message) + { + if (send_handshake_message (data->connection, handshake_message)) + g_input_stream_read_bytes_async (istream, 8192, G_PRIORITY_DEFAULT, + NULL, bytes_ready_cb, user_data); + } + + if (id) + { + g_hash_table_insert (gws->connections_hash, data->connection, g_strdup(id)); + g_signal_emit_by_name (data->gws, "open", id); + } + g_free (handshake_message); + g_free (id); + +// g_object_unref (G_SOCKET_CONNECTION(data->connection)); +// g_free (data); +} + + +static gboolean +incoming_callback (GSocketService *service, + GSocketConnection *connection, + GObject *source_object, + gpointer user_data) +{ + GncWsServer *gws = user_data; + GInputStream *istream = g_io_stream_get_input_stream (G_IO_STREAM(connection)); + struct ConnData *data = g_new (struct ConnData, 1); + +g_print("%s called, connection %p\n",__FUNCTION__, connection); + + data->connection = g_object_ref (connection); + data->gws = gws; + + g_input_stream_read_async (istream, data->message, sizeof (data->message), + G_PRIORITY_DEFAULT, NULL, message_ready_cb, data); + + return FALSE; +} + +GncWsServer * +gnc_ws_server_new (void) +{ +g_print("%s called\n",__FUNCTION__); + + if (gws) + return gws; + + gws = g_object_new (GNC_TYPE_WS_SERVER, NULL); + +g_print(" ws gws %p\n", gws); + + gws->service = g_socket_service_new (); + GError *error = NULL; + gboolean ret = g_socket_listener_add_inet_port (G_SOCKET_LISTENER(gws->service), + PORT, NULL, &error); + + if (ret && error != NULL) + { + g_error ("%s", error->message); +g_print("service error is: %s\n", error->message); + g_clear_error (&error); + return NULL; + } + + gws->connections_hash = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, g_free); + + g_socket_service_start (gws->service); + + gws->incoming_id = g_signal_connect (gws->service, "incoming", + G_CALLBACK(incoming_callback), gws); + +g_print(" ws service active %d\n", g_socket_service_is_active (gws->service)); + + return gws; +} diff --git a/gnucash/html/gnc-ws-server.h b/gnucash/html/gnc-ws-server.h new file mode 100644 index 0000000000..74a4af326a --- /dev/null +++ b/gnucash/html/gnc-ws-server.h @@ -0,0 +1,57 @@ +/******************************************************************** + * gnc-ws-server.h -- basic websocket server * + * Copyright (C) 2024 Bob Fewell * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License as * + * published by the Free Software Foundation; either version 2 of * + * the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License* + * along with this program; if not, contact: * + * * + * Free Software Foundation Voice: +1-617-542-5942 * + * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * + * Boston, MA 02110-1301, USA gnu@gnu.org * +\********************************************************************/ + +#ifndef WS_SERVER_H +#define WS_SERVER_H + +#include + +#define GNC_TYPE_WS_SERVER (gnc_ws_server_get_type ()) +#define GNC_WS_SERVER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GNC_TYPE_WS_SERVER, GncWsServer)) +#define GNC_WS_SERVER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GNC_TYPE_WS_SERVER, GncWsServerClass)) +#define GNC_IS_WS_SERVER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GNC_TYPE_WS_SERVER)) +#define GNC_IS_WS_SERVER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), GNC_TYPE_WS_SERVER)) +#define GNC_WS_SERVER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GNC_TYPE_WS_SERVER, GncWsServerClass)) + +typedef struct _GncWsServer GncWsServer; +typedef struct _GncWsServerClass GncWsServerClass; + +struct _GncWsServer +{ + GObject parent; + GSocketService *service; + gulong incoming_id; + GHashTable *connections_hash; +}; + +struct _GncWsServerClass +{ + GObjectClass parent; +}; + +GType gnc_ws_server_get_type (void) G_GNUC_CONST; + +GncWsServer *gnc_ws_server_new (void); + +void gnc_ws_server_send_message (GncWsServer *gws, const gchar *id, const gchar *message); + +#endif diff --git a/gnucash/report/html-document.scm b/gnucash/report/html-document.scm index a888b005de..8fbe67cfbb 100644 --- a/gnucash/report/html-document.scm +++ b/gnucash/report/html-document.scm @@ -185,6 +185,66 @@ (push (list "" style-text "