/********************************************************************
 * gnc-html-webkit.c -- gnucash report renderer using webkit        *
 *                                                                  *
 * Copyright (C) 2000 Bill Gribble <grib@billgribble.com>           *
 * Copyright (C) 2001 Linas Vepstas <linas@linas.org>               *
 * Copyright (C) 2009 Phil Longstaff <plongstaff@rogers.com>        *
 *                                                                  *
 * This program is free software; you can redistribute it and/or    *
 * modify it under the terms of the GNU General Public License as   *
 * published by the Free Software Foundation; either version 2 of   *
 * the License, or (at your option) any later version.              *
 *                                                                  *
 * This program is distributed in the hope that it will be useful,  *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of   *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the    *
 * GNU 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 <config.h>

#include <platform.h>
#ifdef __MINGW32__
#define _GL_UNISTD_H //Deflect poisonous define of close in Guile's GnuLib
#endif
#include <libguile.h>
#if PLATFORM(WINDOWS)
#include <windows.h>
#endif

#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdlib>
#include <string.h>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>
#include <regex.h>

#include <webkit2/webkit2.h>

#include "Account.h"
#include "gnc-prefs.h"
#include "gnc-gui-query.h"
#include "gnc-engine.h"
#include "gnc-html.h"
#include "gnc-html-webkit.hpp"
#include "gnc-html-history.h"
#include "print-session.h"


G_DEFINE_TYPE(GncHtmlWebkit, gnc_html_webkit, GNC_TYPE_HTML )

static void gnc_html_webkit_dispose( GObject* obj );
static void gnc_html_webkit_finalize( GObject* obj );

#define GNC_HTML_WEBKIT_GET_PRIVATE(o) (GNC_HTML_WEBKIT(o)->priv)

#include "gnc-html-webkit-p.hpp"

/* indicates the debugging module that this .o belongs to.  */
static QofLogModule log_module = GNC_MOD_HTML;

/* hashes an HTML <object classid="ID"> 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[] = "<html><body><h3>%s</h3><p>%s</body></html>";
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 gboolean webkit_decide_policy_cb (WebKitWebView* web_view,
                     WebKitPolicyDecision *decision,
                     WebKitPolicyDecisionType decision_type,
                     gpointer user_data);
static void webkit_mouse_target_cb (WebKitWebView* web_view,
                     WebKitHitTestResult *hit,
                     guint modifiers, gpointer data);
static gboolean webkit_notification_cb (WebKitWebView *web_view,
                     WebKitNotification *note,
                     gpointer user_data);
static gboolean webkit_load_failed_cb (WebKitWebView *web_view,
                     WebKitLoadEvent event,
                     gchar *uri, GError *error,
                     gpointer user_data);
static void webkit_resource_load_started_cb (WebKitWebView *web_view,
                                             WebKitWebResource *resource,
                                             WebKitURIRequest *request,
                                             gpointer data);
static gchar* handle_embedded_object( GncHtmlWebkit* self, gchar* html_str );
static void impl_webkit_show_url( GncHtml* self, URLType type,
                                  const gchar* location, const gchar* label,
                                  gboolean new_window_hint );
static void impl_webkit_show_data( GncHtml* self, const gchar* data, int datalen );
static void impl_webkit_reload( GncHtml* self, gboolean force_rebuild );
static void impl_webkit_copy_to_clipboard( GncHtml* self );
static gboolean impl_webkit_export_to_file( GncHtml* self, const gchar* filepath );
static void impl_webkit_print (GncHtml* self,const gchar* jobname);
static void impl_webkit_cancel( GncHtml* self );
static void impl_webkit_set_parent( GncHtml* self, GtkWindow* parent );
static void impl_webkit_default_zoom_changed(gpointer prefs, gchar *pref, gpointer user_data);

static GtkWidget*
gnc_html_webkit_webview_new (void)
{
     GtkWidget *view = webkit_web_view_new ();
     WebKitSettings *webkit_settings = nullptr;
     const char *default_font_family = nullptr;
     GtkStyleContext *style = gtk_widget_get_style_context (view);
     GValue val = G_VALUE_INIT;
     GtkStateFlags state = gtk_style_context_get_state (style);
     gtk_style_context_get_property (style, GTK_STYLE_PROPERTY_FONT,
                     state, &val);

     if (G_VALUE_HOLDS_BOXED (&val))
     {
      const PangoFontDescription *font =
           (const PangoFontDescription*)g_value_get_boxed (&val);
      default_font_family = pango_font_description_get_family (font);
     }
/* Set default webkit settings */
     webkit_settings = webkit_web_view_get_settings (WEBKIT_WEB_VIEW (view));
     g_object_set (G_OBJECT(webkit_settings),
                   "default-charset", "utf-8",
                   "allow-file-access-from-file-urls", TRUE,
                   "allow-universal-access-from-file-urls", TRUE,
                   "enable-java", FALSE,
                   "enable-page-cache", FALSE,
                   "enable-plugins", FALSE,
                   "enable-site-specific-quirks", FALSE,
                   "enable-xss-auditor", FALSE,
                   "enable-developer-extras", TRUE,
                   nullptr);
     if (default_font_family != nullptr)
     {
          g_object_set (G_OBJECT (webkit_settings),
              "default-font-family", default_font_family, nullptr);
     }
     g_value_unset (&val);
     return view;
}

static void
gnc_html_webkit_init( GncHtmlWebkit* self )
{
     const gpointer p = g_realloc (GNC_HTML(self)->priv, sizeof(GncHtmlWebkitPrivate));
     auto new_priv = reinterpret_cast<GncHtmlWebkitPrivate *>(p);
     auto priv = self->priv = new_priv;
     GNC_HTML(self)->priv = (GncHtmlPrivate*)priv;

     priv->html_string = nullptr;
     priv->web_view = WEBKIT_WEB_VIEW (gnc_html_webkit_webview_new ());


     /* Scale everything up */
     gdouble zoom = gnc_prefs_get_float (GNC_PREFS_GROUP_GENERAL_REPORT,
                 GNC_PREF_RPT_DFLT_ZOOM);
     webkit_web_view_set_zoom_level (priv->web_view, zoom);


     gtk_container_add( GTK_CONTAINER(priv->base.container),
                        GTK_WIDGET(priv->web_view) );

     g_object_ref_sink( priv->base.container );

     /* signals */
     g_signal_connect (priv->web_view, "decide-policy",
                       G_CALLBACK (webkit_decide_policy_cb),
                       self);

     g_signal_connect (priv->web_view, "mouse-target-changed",
                       G_CALLBACK (webkit_mouse_target_cb),
                       self);

     g_signal_connect (priv->web_view, "show-notification",
                       G_CALLBACK (webkit_notification_cb),
                       self);

     g_signal_connect (priv->web_view, "load-failed",
                       G_CALLBACK (webkit_load_failed_cb),
                       self);
     g_signal_connect (priv->web_view, "resource-load-started",
                       G_CALLBACK (webkit_resource_load_started_cb),
                       self);
     gnc_prefs_register_cb (GNC_PREFS_GROUP_GENERAL_REPORT,
                            GNC_PREF_RPT_DFLT_ZOOM,
                            reinterpret_cast<gpointer>(impl_webkit_default_zoom_changed),
                            self);

     LEAVE("retval %p", self);
}

static void
gnc_html_webkit_class_init( GncHtmlWebkitClass* klass )
{
     GObjectClass* gobject_class = G_OBJECT_CLASS(klass);
     GncHtmlClass* html_class = GNC_HTML_CLASS(klass);

     gobject_class->dispose = gnc_html_webkit_dispose;
     gobject_class->finalize = gnc_html_webkit_finalize;

     html_class->show_url = impl_webkit_show_url;
     html_class->show_data = impl_webkit_show_data;
     html_class->reload = impl_webkit_reload;
     html_class->copy_to_clipboard = impl_webkit_copy_to_clipboard;
     html_class->export_to_file = impl_webkit_export_to_file;
     html_class->print = impl_webkit_print;
     html_class->cancel = impl_webkit_cancel;
     html_class->set_parent = impl_webkit_set_parent;
}

static void
gnc_html_webkit_dispose( GObject* obj )
{
     GncHtmlWebkit* self = GNC_HTML_WEBKIT(obj);
     GncHtmlWebkitPrivate* priv = GNC_HTML_WEBKIT_GET_PRIVATE(self);

     if ( priv->web_view != nullptr )
     {
          gtk_container_remove (GTK_CONTAINER(priv->base.container),
                                GTK_WIDGET(priv->web_view));

          priv->web_view = nullptr;
     }

     if ( priv->html_string != nullptr )
     {
          g_free( priv->html_string );
          priv->html_string = nullptr;
     }

     gnc_prefs_remove_cb_by_func (GNC_PREFS_GROUP_GENERAL_REPORT,
                                  GNC_PREF_RPT_DFLT_ZOOM,
                                  reinterpret_cast<gpointer>(impl_webkit_default_zoom_changed),
                                  obj);

     G_OBJECT_CLASS(gnc_html_webkit_parent_class)->dispose( obj );
}

static void
gnc_html_webkit_finalize( GObject* obj )
{
     GncHtmlWebkit* self = GNC_HTML_WEBKIT(obj);

     self->priv = nullptr;

     G_OBJECT_CLASS(gnc_html_webkit_parent_class)->finalize( obj );
}

/*****************************************************************************/

static char*
extract_base_name(URLType type, const gchar* path)
{
     constexpr gchar       machine_rexp[] = "^(//[^/]*)/*(/.*)?$";
     constexpr gchar       path_rexp[] = "^/*(.*)/+([^/]*)$";
     regex_t     compiled_m, compiled_p;
     constexpr size_t MATCH_LEN = 4;
     regmatch_t  match[MATCH_LEN];
     gchar       * machine = nullptr, * location = nullptr, * base = nullptr;
     gchar       * basename = nullptr;

     DEBUG(" ");
     if (!path) return nullptr;

     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, MATCH_LEN, 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);
               }
          }
     }

     regfree(&compiled_m);
     regfree(&compiled_p);

     if (machine)
     {
          if (base && (strlen(base) > 0))
          {
               basename = g_strconcat(machine, "/", base, "/", nullptr);
          }
          else
          {
               basename = g_strconcat(machine, "/", nullptr);
          }
     }
     else
     {
          if (base && (strlen(base) > 0))
          {
               basename = g_strdup(base);
          }
     }

     g_free(machine);
     g_free(base);
     g_free(location);
     return basename;
}

static gboolean
http_allowed()
{
     return TRUE;
}

static gboolean
https_allowed()
{
     return TRUE;
}

static gchar*
handle_embedded_object( GncHtmlWebkit* self, gchar* html_str )
{
     // Find the <object> tag and get the classid from it.  This will provide the correct
     // object callback handler.  Pass the <object> 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 = nullptr;
     gchar* html_str_middle;
     gchar* html_str_result = nullptr;
     gchar* classid_start;
     gchar* classid_end;
     gchar* classid_str;
     gchar* new_chunk;
     GncHTMLObjectCB h;

     object_tag = g_strstr_len( remainder_str, -1, "<object classid=" );
     while (object_tag)
     {

          classid_start = object_tag + strlen( "<object classid=" ) + 1;
          classid_end = g_strstr_len( classid_start, -1, "\"" );
          classid_str = g_strndup( classid_start, (classid_end - classid_start) );

          end_object_tag = g_strstr_len( object_tag, -1, "</object>" );
          if ( end_object_tag == nullptr )
          {
               /*  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>" );
          object_contents = g_strndup( object_tag, (end_object_tag - object_tag) );

          const gpointer p = g_hash_table_lookup( gnc_html_object_handlers, classid_str );
          h = reinterpret_cast<GncHTMLObjectCB>(p);
          if ( h != nullptr )
          {
               (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, nullptr);
          else
               html_str_result = g_strconcat (html_str_start, new_chunk, html_str_middle, nullptr);

          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, "<object classid=" );
     }

     if (html_str_result)
     {
          html_str_start =  html_str_result;
          html_str_result = g_strconcat (html_str_start, remainder_str, nullptr);
          g_free (html_str_start);
     }
     else
          html_str_result = g_strdup (remainder_str);

     return html_str_result;
}

/********************************************************************
 * load_to_stream : actually do the work of loading the HTML
 * or binary data referenced by a URL and feeding it into the webkit
 * widget.
 ********************************************************************/

static gboolean
load_to_stream( GncHtmlWebkit* self, URLType type,
                const gchar* location, const gchar* label )
{
     gchar* fdata = nullptr;
     int fdata_len = 0;
     GncHtmlWebkitPrivate* priv = GNC_HTML_WEBKIT_GET_PRIVATE(self);

     DEBUG( "type %s, location %s, label %s", type ? type : "(null)",
            location ? location : "(null)", label ? label : "(null)");

     g_return_val_if_fail( self != nullptr, FALSE );

     if ( gnc_html_stream_handlers != nullptr )
     {
          const gpointer p = g_hash_table_lookup( gnc_html_stream_handlers, type );
          GncHTMLStreamCB stream_handler = reinterpret_cast<GncHTMLStreamCB>(p);
          if ( stream_handler )
          {
              GncHtml *weak_html = GNC_HTML(self);

              g_object_add_weak_pointer(G_OBJECT(self),
                                        (gpointer*)(&weak_html));
              bool ok = stream_handler( location, &fdata, &fdata_len );

              if (!weak_html) // will be nullptr if self has been destroyed
              {
                  g_free (fdata);
                  return FALSE;
              }
              else
              {
                  g_object_remove_weak_pointer(G_OBJECT(self),
                                               (gpointer*)(&weak_html));
              }

               if ( ok )
               {
                    fdata = fdata ? fdata : g_strdup( "" );

                    // Until webkitgtk supports download requests,
                    // look for "<object classid=" indicating the
                    // beginning of an embedded graph.  If found,
                    // handle it
                    if ( g_strstr_len( fdata, -1, "<object classid=" ) != nullptr )
                    {
                         gchar *new_fdata = handle_embedded_object( self, fdata );
                         g_free( fdata );
                         fdata = new_fdata;
                    }

                    // Save a copy for export purposes
                    if ( priv->html_string != nullptr )
                    {
                         g_free( priv->html_string );
                    }
                    priv->html_string = g_strdup( fdata );
                    impl_webkit_show_data( GNC_HTML(self), fdata, strlen(fdata) );
               }
               else
               {
                    fdata = fdata ? fdata :
                         g_strdup_printf( error_404_format,
                                          _(error_404_title), _(error_404_body) );
                    webkit_web_view_load_html (priv->web_view, fdata,
                           BASE_URI_NAME);
               }

               g_free( fdata );

               if ( label )
               {
                    while ( gtk_events_pending() )
                    {
                         gtk_main_iteration();
                    }
                    /* No action required: Webkit jumps to the anchor on its own. */
               }
               return TRUE;
          }
     }

     do
     {
          if ( !g_strcmp0( type, URL_TYPE_SECURE ) ||
               !g_strcmp0( type, URL_TYPE_HTTP ) )
          {

               if ( !g_strcmp0( type, URL_TYPE_SECURE ) )
               {
                    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 ( !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) );
               webkit_web_view_load_html (priv->web_view, fdata, BASE_URI_NAME);
               g_free( fdata );
          }
     }
     while ( false );
     return TRUE;
}

static gboolean
perform_navigation_policy (WebKitWebView *web_view,
               WebKitNavigationPolicyDecision *decision,
               GncHtml *self)
{
     gchar *location = nullptr, *label = nullptr;
     bool ignore = false;
     WebKitNavigationAction *action =
      webkit_navigation_policy_decision_get_navigation_action (decision);
     if (webkit_navigation_action_get_navigation_type (action) !=
         WEBKIT_NAVIGATION_TYPE_LINK_CLICKED)
     {
          webkit_policy_decision_use ((WebKitPolicyDecision*)decision);
          return TRUE;
     }
     auto req = webkit_navigation_action_get_request (action);
     const gchar *uri = webkit_uri_request_get_uri (req);
     const gchar *scheme =  gnc_html_parse_url (self, uri, &location, &label);
     if (strcmp (scheme, URL_TYPE_FILE) != 0)
     {
          impl_webkit_show_url (self, scheme, location, label, FALSE);
          ignore = true;
     }
     g_free (location);
     g_free (label);
     if (ignore)
          webkit_policy_decision_ignore ((WebKitPolicyDecision*)decision);
     else
          webkit_policy_decision_use ((WebKitPolicyDecision*)decision);
     return TRUE;
}

static gboolean
webkit_decide_policy_cb (WebKitWebView *web_view,
             WebKitPolicyDecision *decision,
             WebKitPolicyDecisionType decision_type,
             gpointer user_data)
{
/* This turns out to be the signal to intercept for handling a link-click. */
     if (decision_type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION)
     {
          webkit_policy_decision_use (decision);
          return TRUE;
     }
     return perform_navigation_policy (
      web_view, (WebKitNavigationPolicyDecision*) decision,
      GNC_HTML (user_data));
}

static void
webkit_mouse_target_cb (WebKitWebView *web_view, WebKitHitTestResult *hit,
            guint modifiers, gpointer user_data)
{
     if (!webkit_hit_test_result_context_is_link (hit))
         return;

     auto self = static_cast<GncHtmlWebkit*>(user_data);
     auto priv = GNC_HTML_WEBKIT_GET_PRIVATE (self);
     gchar *uri = g_strdup (webkit_hit_test_result_get_link_uri (hit));
     g_free (priv->base.current_link);
     priv->base.current_link = uri;
     if (priv->base.flyover_cb)
     {
          (priv->base.flyover_cb) (GNC_HTML (self), uri,
                   priv->base.flyover_cb_data);
     }
}

static gboolean
webkit_notification_cb (WebKitWebView* web_view, WebKitNotification *note,
            gpointer user_data)
{
     GncHtmlWebkit *self = (GncHtmlWebkit*)user_data;
     g_return_val_if_fail (self != nullptr, FALSE);
     g_return_val_if_fail (note != nullptr, FALSE);

     auto top = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (web_view)));
     auto dialog = gtk_message_dialog_new (top, GTK_DIALOG_MODAL,
                                      GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE,
                                      "%s\n%s",
                                      webkit_notification_get_title (note),
                                      webkit_notification_get_body (note));
     gtk_dialog_run (GTK_DIALOG (dialog));
     gtk_widget_destroy (dialog);
     return TRUE;
}

static gboolean
webkit_load_failed_cb (WebKitWebView *web_view, WebKitLoadEvent event,
                       gchar *uri, GError *error, gpointer user_data)
{
     PERR ("WebKit load of %s failed due to %s\n", uri, error->message);
     return FALSE;
}

static void
webkit_resource_load_failed_cb (WebKitWebResource *resource,
                                GError *error,
                                gpointer data)
{
     WebKitURIResponse *response = webkit_web_resource_get_response (resource);
     const gchar * uri = webkit_web_resource_get_uri (resource);
     PERR ("Load of resource at %s failed with error %s and status code %d.\n",
           uri, error->message, webkit_uri_response_get_status_code (response));
}

static void
webkit_resource_load_finished_cb (WebKitWebResource *resource, gpointer data)
{
     DEBUG ("Load of resource %s completed.\n", webkit_web_resource_get_uri(resource));
}

static void
webkit_resource_load_started_cb (WebKitWebView *web_view,
                                 WebKitWebResource *resource,
                                 WebKitURIRequest *request,
                                 gpointer data)
{
     DEBUG ("Load of resource %s begun.\n", webkit_web_resource_get_uri(resource));
     g_signal_connect (resource, "failed",
                       G_CALLBACK (webkit_resource_load_failed_cb),
                       data);
     g_signal_connect (resource, "finished",
                       G_CALLBACK (webkit_resource_load_finished_cb),
                       data);
}

/********************************************************************
 * gnc_html_open_scm
 * insert some scheme-generated HTML
 ********************************************************************/

static void
gnc_html_open_scm( GncHtmlWebkit* self, const gchar * location,
                   const gchar * label, int newwin )
{
     PINFO("location='%s'", location ? location : "(null)");
}


/********************************************************************
 * gnc_html_show_data
 * display some HTML that the creator of the gnc-html got from
 * somewhere.
 ********************************************************************/

static void
impl_webkit_show_data( GncHtml* self, const gchar* data, int datalen )
{
     constexpr char TEMPLATE_REPORT_FILE_NAME[] = "gnc-report-XXXXXX.html";
     g_return_if_fail( self != nullptr );
     g_return_if_fail( GNC_IS_HTML_WEBKIT(self) );

     ENTER( "datalen %d, data %20.20s", datalen, data );

     auto priv = GNC_HTML_WEBKIT_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. */
     gchar *filename = g_build_filename(g_get_tmp_dir(), TEMPLATE_REPORT_FILE_NAME, (gchar *)nullptr);
     int fd = g_mkstemp( filename );
     impl_webkit_export_to_file( self, filename );
     close( fd );
     gchar *uri = g_strdup_printf( "file://%s", filename );
     g_free(filename);
     DEBUG("Loading uri '%s'", uri);
     webkit_web_view_load_uri( priv->web_view, uri );
     g_free( uri );

     LEAVE("");
}

/********************************************************************
 * gnc_html_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_webkit_show_url( GncHtml* self, URLType type,
                      const gchar* location, const gchar* label,
                      gboolean new_window_hint )
{
     GncHTMLUrlCB url_handler = nullptr;
     bool new_window = false;
     bool stream_loaded = false;

     g_return_if_fail( self != nullptr );
     g_return_if_fail( GNC_IS_HTML_WEBKIT(self) );
     g_return_if_fail( location != nullptr );

     auto priv = GNC_HTML_WEBKIT_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 = true;
     }

     if ( !new_window )
     {
          gnc_html_cancel( GNC_HTML(self) );
     }

     if ( gnc_html_url_handlers )
     {
          const gpointer p = g_hash_table_lookup( gnc_html_url_handlers, type );
          url_handler = reinterpret_cast<GncHTMLUrlCB>(p);
     }

     if ( url_handler )
     {
          GNCURLResult result;

          result.load_to_stream = FALSE;
          result.url_type = type;
          result.location = nullptr;
          result.label = nullptr;
          result.base_type = URL_TYPE_FILE;
          result.base_location = nullptr;
          result.error_message = nullptr;
          result.parent = GTK_WINDOW (priv->base.parent);

          bool ok = url_handler( location, label, new_window, &result );
          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 )
          {
               const char *new_location = result.location ? result.location : location;
               const char *new_label = result.label ? result.label : label;
               auto 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_WEBKIT(self),
                                               result.url_type,
                                               new_location, new_label );

               if ( stream_loaded && priv->base.load_cb != nullptr )
               {
                    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_WEBKIT(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 != nullptr ) 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_WEBKIT(self),
                                               type, location, label );

          }
          while ( false );
     }
     else
     {
          PERR( "URLType %s not supported.", type );
     }

     if ( stream_loaded && priv->base.load_cb != nullptr )
     {
          (priv->base.load_cb)( GNC_HTML(self), type, location, label, priv->base.load_cb_data );
     }
}


/********************************************************************
 * gnc_html_reload
 * reload the current page
 * if force_rebuild is TRUE, the report is recreated, if FALSE, report
 * is reloaded by webkit
 ********************************************************************/

static void
impl_webkit_reload( GncHtml* self, gboolean force_rebuild )
{
     g_return_if_fail( self != nullptr );
     g_return_if_fail( GNC_IS_HTML_WEBKIT(self) );

     auto priv = GNC_HTML_WEBKIT_GET_PRIVATE(self);

     if ( force_rebuild )
     {
          gnc_html_history_node *n = gnc_html_history_get_current( priv->base.history );
          if ( n != nullptr )
               gnc_html_show_url( self, n->type, n->location, n->label, 0 );
     }
     else
          webkit_web_view_reload( priv->web_view );
}


/********************************************************************
 * gnc_html_new
 * create and set up a new webkit widget.
 ********************************************************************/

GncHtml*
gnc_html_webkit_new( void ) noexcept
{
     auto self = static_cast<GncHtmlWebkit*>(g_object_new( GNC_TYPE_HTML_WEBKIT, nullptr ));
     return GNC_HTML(self);
}

/********************************************************************
 * gnc_html_cancel
 * cancel any outstanding HTML fetch requests.
 ********************************************************************/

static gboolean
webkit_cancel_helper(gpointer key, gpointer value, gpointer user_data)
{
     g_free(key);
     g_list_free((GList *)value);
     return TRUE;
}

static void
impl_webkit_cancel( GncHtml* self )
{
     g_return_if_fail( self != nullptr );
     g_return_if_fail( GNC_IS_HTML_WEBKIT(self) );

     auto priv = GNC_HTML_WEBKIT_GET_PRIVATE(self);

     g_hash_table_foreach_remove( priv->base.request_info, webkit_cancel_helper, nullptr );
}

static void
impl_webkit_copy_to_clipboard( GncHtml* self )
{
     g_return_if_fail( self != nullptr );
     g_return_if_fail( GNC_IS_HTML_WEBKIT(self) );

     auto priv = GNC_HTML_WEBKIT_GET_PRIVATE(self);
     webkit_web_view_execute_editing_command (priv->web_view,
                          WEBKIT_EDITING_COMMAND_COPY);
}

/**************************************************************
 * gnc_html_export_to_file
 *
 * @param self GncHtmlWebkit object
 * @param filepath Where to write the HTML
 * @return TRUE if successful, FALSE if unsuccessful
 **************************************************************/
static gboolean
impl_webkit_export_to_file( GncHtml* self, const char *filepath )
{
     g_return_val_if_fail( self != nullptr, FALSE );
     g_return_val_if_fail( GNC_IS_HTML_WEBKIT(self), FALSE );
     g_return_val_if_fail( filepath != nullptr, FALSE );

     auto priv = GNC_HTML_WEBKIT_GET_PRIVATE(self);
     if ( priv->html_string == nullptr )
     {
          return FALSE;
     }
     FILE *fh = g_fopen( filepath, "w" );
     if ( fh != nullptr )
     {
          gint len = strlen( priv->html_string );
          gint written = fwrite( priv->html_string, 1, len, fh );
          fclose (fh);

          if ( written != len )
          {
               return FALSE;
          }

          return TRUE;
     }
     else
     {
          return FALSE;
     }
}

/* The webkit1 comment was
 * If printing on WIN32, in order to prevent the font from being tiny, (see bug
 * #591177), A GtkPrintOperation object needs to be created so that the unit can
 * be set, and then webkit_web_frame_print_full() needs to be called to use that
 * GtkPrintOperation.  On other platforms (specifically linux - not sure about
 * MacOSX), the version of webkit may not contain the function
 * webkit_web_frame_print_full(), so webkit_web_frame_print() is called instead
 * (the font size problem doesn't show up on linux).
 *
 * Webkit2 exposes only a very simple WebKitPrintOperation API. In order to
 * implement the above if it proves still to be necessary we'll have to use
 * GtkPrintOperation instead, passing it the results of
 * webkit_web_view_get_snapshot for each page.
 */
static void
impl_webkit_print (GncHtml* self,const gchar* jobname)
{
     g_return_if_fail (self != nullptr);
     g_return_if_fail (GNC_IS_HTML_WEBKIT (self));

     auto priv = GNC_HTML_WEBKIT_GET_PRIVATE (self);
     auto op = webkit_print_operation_new (priv->web_view);
     gchar *basename = g_path_get_basename(jobname);
     auto print_settings = gtk_print_settings_new();
     webkit_print_operation_set_print_settings(op, print_settings);
     gchar *export_filename = g_strdup(jobname);
     g_free(basename);
     gtk_print_settings_set(print_settings,
                    GTK_PRINT_SETTINGS_OUTPUT_BASENAME,
                    export_filename);
     webkit_print_operation_set_print_settings(op, print_settings);
     // Open a print dialog
     auto top = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (priv->web_view)));
     auto print_response = webkit_print_operation_run_dialog (op, top);
     if (print_response == WEBKIT_PRINT_OPERATION_RESPONSE_PRINT)
     {
          // Get the newly updated print settings
          g_object_unref(print_settings);
          print_settings = g_object_ref(webkit_print_operation_get_print_settings(op));
     }
     g_free(export_filename);
     g_object_unref (op);
     g_object_unref (print_settings);
}

static void
impl_webkit_set_parent( GncHtml* self, GtkWindow* parent )
{
     g_return_if_fail( self != nullptr );
     g_return_if_fail( GNC_IS_HTML_WEBKIT(self) );

     auto priv = GNC_HTML_WEBKIT_GET_PRIVATE(self);
     priv->base.parent = GTK_WIDGET(parent);
}

static void
impl_webkit_default_zoom_changed(gpointer prefs, gchar *pref, gpointer user_data)
{
     g_return_if_fail(user_data != nullptr);

     GncHtmlWebkit* self = GNC_HTML_WEBKIT(user_data);
     GncHtmlWebkitPrivate* priv = GNC_HTML_WEBKIT_GET_PRIVATE(self);
     gdouble zoom = gnc_prefs_get_float (GNC_PREFS_GROUP_GENERAL_REPORT, GNC_PREF_RPT_DFLT_ZOOM);
     webkit_web_view_set_zoom_level (priv->web_view, zoom);
}
