/******************************************************************************
 *
 *    Name: UIDialogLibWebKit.c
 *
 *    $Id: //LinuxBasedReceivers/main/ClientEngine/unix/UIDialogLib/WebKit/UIDialogLibWebKit.c#1 $
 *
 *    Purpose: A WebKit implementation for the UI Dialog Library.
 *
 *    Copyright 2017 Citrix Systems, Inc. All rights reserved.
 *
 *****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <webkit/webkit.h>
#include <JavaScriptCore/JavaScript.h>

#include "UIDialogLib.h"
#include "UIDialogLibGTK.h"
#include "UIDialogLibWebKit.h"

/* Defines */
#define WEB_VIEW_MIN_WIDTH               640
#define WEB_VIEW_MIN_HEIGHT              480
#define WEB_VIEW_RELEASE_POINTER(ptr)    releasePointer((void **)&ptr);
#define WEB_VIEW_SECURE_ZERO_MEMORY(ptr) secureZeroMemory(ptr, ptr == NULL ? 0 : strlen(ptr) * sizeof(char));

/* Globals */
extern const char   *gicaroot;    /* Defined in UIDialogGtk.c */
extern unsigned int  option_bits; /* Defined in UIDialogGtk.c */
/* Local Globals */
static pthread_mutex_t  local_mutex       = PTHREAD_MUTEX_INITIALIZER; /* Mutex to synchronize the operations on the wait condition. */
static GtkWidget       *window            = NULL;                      /* The main window of the webview. */
static char            *response          = NULL;                      /* The response returned by the webview. */
static webkit_option    webkit_settings[] = {                          /* The settings to apply to the webview. */
    { "enable-default-context-menu",               FALSE }, /* Since WebKit 1.1.18 */
    { "enable-html5-database",                     FALSE }, /* Since WebKit 1.1.8 */
    { "enable-html5-local-storage",                FALSE }, /* Since WebKit 1.1.8 */
    { "enable-offline-web-application-cache",      FALSE }, /* Since WebKit 1.1.13 */
    { "enable-display-of-insecure-content",        FALSE }, /* Since WebKit 2.0 */
    { "enable-dns-prefetching",                    FALSE }, /* Since WebKit 1.1.13 */
    { "enable-java-applet",                        FALSE }, /* Since WebKit 1.1.22 */
    { "javascript-can-access-clipboard",           FALSE }, /* Since Webkit 1.3.0 */
    { "javascript-can-open-windows-automatically", FALSE }, /* Since WebKit 1.1.11 */
    { "enable-page-cache"                        , FALSE }, /* Since WebKit 1.1.18 */
    { "enable-plugins",                            FALSE }, /* No "Since" specified so assuming WebKit 1.0 */
    { "enable-running-of-insecure-content",        FALSE }, /* Since WebKit 2.0 */
    { "media-playback-allows-inline",              FALSE }, /* Since WebKit 1.10.0 */
    { NULL,                                        FALSE }
};

/* Implements the UIDialogDisplayWebview() function from the UI Dialog Library SDK. */
const char** UIDialogDisplayWebview(const char *url,
                                    const char *window_title,
                                    const char *post_data,
                                    UIDIALOG   *parent)
{
    webview_data *web_view_data = NULL;

    /* Clear the response if necessary. */
    WEB_VIEW_SECURE_ZERO_MEMORY(response);
    WEB_VIEW_RELEASE_POINTER(response);

    /* Prepare the webview data. */
    web_view_data = malloc(sizeof(webview_data));

    pthread_cond_t wait_condition;
    pthread_cond_init(&wait_condition, NULL);

    web_view_data->url            =  url;
    web_view_data->window_title   =  window_title;
    web_view_data->post_data      =  post_data;
    web_view_data->parent         =  parent;
    web_view_data->wait_condition = &wait_condition;

    /* Display the web view and hold onto it until the wait condition has been signalled. */
    pthread_mutex_lock(&local_mutex);
    g_idle_add((gboolean(*)(gpointer user_data)) displayWebview, web_view_data);
    pthread_cond_wait(&wait_condition, &local_mutex);
    pthread_cond_destroy(&wait_condition);
    pthread_mutex_unlock(&local_mutex);

    return (const char**)&response;
}

/* Creates a GTK window with a web view and displays it. */
/* static */ gboolean displayWebview(webview_data *webviewData)
{
    callback_data *on_destroy_data = NULL;

    /* Create the window. */
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    /* Set the callback for when the window is destroyed. */
    on_destroy_data = malloc(sizeof(callback_data));
    on_destroy_data->wait_condition = webviewData->wait_condition;
    on_destroy_data->signal         = g_signal_connect(G_OBJECT(window),
                                                       "destroy",
                                                       G_CALLBACK(onDestroy),
                                                       on_destroy_data);

    /* Monitor for the ESC key being pressed. */
    g_signal_connect(G_OBJECT(window),
                     "key_press_event",
                     G_CALLBACK(onKeyPressEvent),
                     NULL);

    /* Set the minimum size. */
    gtk_widget_set_size_request(window, WEB_VIEW_MIN_WIDTH, WEB_VIEW_MIN_HEIGHT);

    /* Set the dialogs parent. */
    if (webviewData->parent != NULL)
    {
        gtk_window_set_modal(GTK_WINDOW(window), TRUE);
        gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(webviewData->parent->window));

        /* Set dialog position */
        gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER_ON_PARENT);

        if (option_bits & UI_DIALOGS_ABOVE)
        {
            gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
        }
    }
    else
    {
        /* Set dialog position */
        gtk_window_set_modal(GTK_WINDOW(window), TRUE);
        gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);

        /* We don't have a parent and we want to be seen
           so set the dialog window to on top. */
        gtk_window_set_keep_above(GTK_WINDOW(window), TRUE);
    }

    /* Set the dialog title. */
    gtk_window_set_title(GTK_WINDOW(window), webviewData->window_title);

    /* Set the dialog icon. */
    gchar *icon_path = g_strconcat(gicaroot, "/icons/receiver.png", NULL);
    gtk_window_set_default_icon_from_file(icon_path, NULL);
    g_free(icon_path);

    /* Create a WebKitGTK+ view. */
    GtkWidget *web_view = webkit_web_view_new();

    /* Configure the web view. */
    configureWebview(WEBKIT_WEB_VIEW(web_view));

    /* Put the browser area into the main window. */
    gtk_container_add(GTK_CONTAINER(window), web_view);

    /* Set a callback to allow us to setup the JavaScript callbacks. */
    g_signal_connect(G_OBJECT(web_view),
                     "window-object-cleared",
                     G_CALLBACK(onWindowObjectCleared),
                     NULL);

    /* Load the web page into the browser instance. */
    webkit_web_view_load_uri(WEBKIT_WEB_VIEW(web_view), webviewData->url);

    /* Make sure that when the browser area becomes visible, it will get mouse
       and keyboard events. */
    gtk_widget_grab_focus(web_view);

    /* Display the dialog. */
    gtk_widget_show_all(window);

    /* Release the callback data. */
    WEB_VIEW_RELEASE_POINTER(webviewData);

    return FALSE; /* The code we were signalled for has been run so remove from
                     the GTK+ mainloop. */
}

/* Invokes the destruction of the main window. */
/* static */ gboolean closeWebview()
{
    if (window != NULL)
    {
        gtk_widget_destroy(window);
    }

    return FALSE; /* The code we were signalled for has been run so remove from
                     the GTK+ mainloop. */
}

/* Configures the web view parameters. */
/* static */ void configureWebview(WebKitWebView *webview)
{
    /* Get WebKit Settings. */
    GObject *settings = G_OBJECT(webkit_web_view_get_settings(webview));
    GObjectClass *settings_class = G_OBJECT_GET_CLASS(settings);

    /* Loop through the webkit options to set. */
    webkit_option *webkit_option_ptr = webkit_settings;
    while (webkit_option_ptr->name != NULL)
    {
        /* Set the webkit option. */
        GParamSpec *param_spec = g_object_class_find_property(settings_class,
                                                              webkit_option_ptr->name);
        if (param_spec != NULL)
        {
            g_object_set(settings, webkit_option_ptr->name, webkit_option_ptr->value, NULL);
        }

        /* Move to the next option. */
        webkit_option_ptr++;
    }
}

/* Callback on clicking a key when the webview window has focus. */
/* static */ gboolean onKeyPressEvent(GtkWidget   *unused_widget,
                                      GdkEventKey *event)
{
    /* Annoyingly the Escape key has had its name changed. GDK_Escape is the
       older of the two defines. */
#ifdef GDK_KEY_Escape
    if (event->keyval == GDK_KEY_Escape)
#else
    if (event->keyval == GDK_Escape)
#endif
    {
        /* The Escape key has been pressed. */

        /* The default cancel response is NULL. */
        WEB_VIEW_SECURE_ZERO_MEMORY(response);
        WEB_VIEW_RELEASE_POINTER(response);

        /* The webview can be closed, so we
           just register a close event in the GTK+ event loop.
           This will trigger the "destroy" event and call the "on_destroy" callback,
           where the wait condition is signalled to the UI thread.*/
        g_idle_add((gboolean(*)()) closeWebview, NULL);

        return TRUE; /* Stop other handlers from being invoked for this event. */
    }

    return  FALSE; /* Propagate the event further as normal. */
}

/* Callback to setup Javascript. */
/* static */ void onWindowObjectCleared(WebKitWebView  *unused_webview,
                                        WebKitWebFrame *unused_frame,
                                        gpointer        context,
                                        gpointer        windowObject)
{
    /* According to the WebKitGTK+ documentation this is the best place to
       add to the window object from the JavaScriptCore. */
    loadJavascriptApi((JSContextRef)context, (JSObjectRef)windowObject);
}

/* Callback to signal that the window is being destroyed. */
/* static */ void onDestroy(GtkWidget     *unused_widget,
                            callback_data *onDestroyData)
{
    /* Disconnect the calling signal to avoid recursion. */
    if (window != NULL)
    {
        g_signal_handler_disconnect(window, onDestroyData->signal);
    }

    /* Signal that the window is closed. */
    pthread_mutex_lock(&local_mutex);
    pthread_cond_signal(onDestroyData->wait_condition);
    pthread_mutex_unlock(&local_mutex);

    /* Release the callback data. */
    WEB_VIEW_RELEASE_POINTER(onDestroyData);
}

/* Callback on the "window.external.citrixExitWebview(samlResponse)"
   javascript function in the webview. */
/* static */ JSValueRef onCitrixExitWebview(JSContextRef      ctx,
                                            JSObjectRef       unused_function,
                                            JSObjectRef       unused_thisObject,
                                            size_t            argumentCount,
                                            const JSValueRef  arguments[],
                                            JSValueRef       *unused_exception)
{
    if (window == NULL
            || argumentCount != 1)
    {
        return JSValueMakeUndefined(ctx);
    }

    /* This call dynamically allocates enough memory to store the token.
       It is the caller's resposibility to free the memory after use. */
    getStringFromJavascriptRef(ctx, arguments[0], &response);

    /* Now that we have the response, the webview can be closed, so we
       just register a close event in the GTK+ event loop.
       This will trigger the "destroy" event and call the "on_destroy" callback,
       where the wait condition is signalled to the UI thread.*/
    g_idle_add((gboolean(*)()) closeWebview, NULL);

    return JSValueMakeUndefined(ctx);
}

/* Converts a Javascript string into a null-terminated UTF8 string.
   It is the caller's responsibility to free the newly created string. */
/* static */ void getStringFromJavascriptRef(const JSContextRef   ctx,
                                             const JSValueRef     arg,
                                             char               **buf)
{
    if (buf == NULL)
    {
        return;
    }

    /* Clear the buffer if necessary. */
    WEB_VIEW_RELEASE_POINTER(*buf);

    JSStringRef jstring = JSValueToStringCopy(ctx, arg, NULL);

    size_t len = JSStringGetMaximumUTF8CStringSize(jstring);
    *buf = malloc(len * sizeof(char));
    JSStringGetUTF8CString(jstring, *buf, len);

    JSStringRelease(jstring);
}

/* JavaScript functions can only be called from the GTK thread. */
/* static */ void loadJavascriptApi(JSContextRef jsContext,
                                    JSObjectRef  parentObject)
{
    /* Set the callback for the javascript function "window.external.citrixExitWebview",
       called when the authentication process is completed. */
    JSStaticFunction js_static_functions[] = {
    { "citrixExitWebview", onCitrixExitWebview, kJSPropertyAttributeReadOnly },
    { NULL,                NULL,                0                            }
    };

    JSClassDefinition js_class_definition = {
        0,                     /* version */
        kJSClassAttributeNone, /* attributes */
        "JSAPI",               /* className */
        NULL,                  /* parentClass */
        NULL,                  /* staticValues */
        js_static_functions,   /* staticFunctions */
        NULL,                  /* initialize */
        NULL,                  /* finalize */
        NULL,                  /* hasProperty */
        NULL,                  /* getProperty */
        NULL,                  /* setProperty */
        NULL,                  /* deleteProperty */
        NULL,                  /* getPropertyNames */
        NULL,                  /* callAsFunction */
        NULL,                  /* callAsConstructor */
        NULL,                  /* hasInstance */
        NULL                   /* convertToType */
    };

    /* Create the class from the structures above. */
    JSClassRef js_class = JSClassCreate(&js_class_definition);

    /* Create an instance of the class we just created. */
    JSObjectRef js_object = JSObjectMake(jsContext, js_class, NULL);

    /* Add the callback instances to "window.external". */
    JSStringRef prop_str = JSStringCreateWithUTF8CString("external");
    JSObjectSetProperty(jsContext,
                        parentObject,
                        prop_str,
                        js_object,
                        kJSPropertyAttributeNone,
                        NULL);
    JSStringRelease(prop_str);
}

/* Implements the UIDialogClearWebviewCookies() function from the UI Dialog Library SDK. */
void UIDialogClearWebviewCookies(void)
{
    SoupSession *session      = NULL;
    SoupCookieJar *cookie_jar = NULL;
    GSList *cookie_list       = NULL;
    GSList *cookie_iterator   = NULL;
    SoupCookie *cookie        = NULL;

    /* Get the current Soup session. */
    session = webkit_get_default_session();
    if (session == NULL)
    {
        /* Invalid session. */
        return;
    }

    /* Get the cookie jar. */
    cookie_jar = (SoupCookieJar*)(soup_session_get_feature(session, SOUP_TYPE_COOKIE_JAR));
    if (cookie_jar == NULL)
    {
        /* Invalid cookie jar. */
        return;
    }

    /* Get a list of all the cookies. */
    cookie_list = soup_cookie_jar_all_cookies(cookie_jar);
    if (cookie_list == NULL)
    {
        /* Invalid cookie list. */
        return;
    }

    /* Delete the cookies. */
    for (cookie_iterator = cookie_list; cookie_iterator != NULL; cookie_iterator = cookie_iterator->next)
    {
        cookie = (SoupCookie*)(cookie_iterator->data);
        if (cookie == NULL)
        {
            /* Invalid cookie. */
            continue;
        }

        /* Delete the cookie. */
        soup_cookie_jar_delete_cookie(cookie_jar, cookie);
    }
    soup_cookies_free(cookie_list);
}
