/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2008 Novell, Inc.
 * Copyright (C) 2008 - 2010 Red Hat, Inc.
 * Copyright (C) 2015 Red Hat, Inc.
 */

#include "libnm-core-impl/nm-default-libnm-core.h"

#include "nm-vpn-editor-plugin.h"

#include <dlfcn.h>
#include <gmodule.h>

#include "libnm-core-intern/nm-core-internal.h"

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

static void nm_vpn_editor_plugin_default_init(NMVpnEditorPluginInterface *iface);

G_DEFINE_INTERFACE(NMVpnEditorPlugin, nm_vpn_editor_plugin, G_TYPE_OBJECT)

static void
nm_vpn_editor_plugin_default_init(NMVpnEditorPluginInterface *iface)
{
    /**
     * NMVpnEditorPlugin:name:
     *
     * Short display name of the VPN plugin.
     */
    g_object_interface_install_property(
        iface,
        g_param_spec_string(NM_VPN_EDITOR_PLUGIN_NAME,
                            "",
                            "",
                            NULL,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

    /**
     * NMVpnEditorPlugin:description:
     *
     * Longer description of the VPN plugin.
     */
    g_object_interface_install_property(
        iface,
        g_param_spec_string(NM_VPN_EDITOR_PLUGIN_DESCRIPTION,
                            "",
                            "",
                            NULL,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));

    /**
     * NMVpnEditorPlugin:service:
     *
     * D-Bus service name of the plugin's VPN service.
     */
    g_object_interface_install_property(
        iface,
        g_param_spec_string(NM_VPN_EDITOR_PLUGIN_SERVICE,
                            "",
                            "",
                            NULL,
                            G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
}

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

typedef struct {
    NMVpnPluginInfo *plugin_info;
} NMVpnEditorPluginPrivate;

static void
_private_destroy(gpointer data)
{
    NMVpnEditorPluginPrivate *priv = data;

    if (priv->plugin_info)
        g_object_remove_weak_pointer((GObject *) priv->plugin_info,
                                     (gpointer *) &priv->plugin_info);

    g_slice_free(NMVpnEditorPluginPrivate, priv);
}

static NMVpnEditorPluginPrivate *
_private_get(NMVpnEditorPlugin *plugin, gboolean create)
{
    static GQuark             quark = 0;
    NMVpnEditorPluginPrivate *priv;

    nm_assert(NM_IS_VPN_EDITOR_PLUGIN(plugin));

    if (G_UNLIKELY(quark == 0))
        quark = g_quark_from_string("nm-vpn-editor-plugin-private");

    priv = g_object_get_qdata((GObject *) plugin, quark);
    if (G_LIKELY(priv))
        return priv;
    if (!create)
        return NULL;
    priv = g_slice_new0(NMVpnEditorPluginPrivate);
    g_object_set_qdata_full((GObject *) plugin, quark, priv, _private_destroy);
    return priv;
}

#define NM_VPN_EDITOR_PLUGIN_GET_PRIVATE(plugin)     _private_get(plugin, TRUE)
#define NM_VPN_EDITOR_PLUGIN_TRY_GET_PRIVATE(plugin) _private_get(plugin, FALSE)

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

/**
 * nm_vpn_editor_plugin_get_plugin_info:
 * @plugin: the #NMVpnEditorPlugin instance
 *
 * Returns: (transfer none): if set, return the #NMVpnPluginInfo instance.
 *
 * Since: 1.4
 */
NMVpnPluginInfo *
nm_vpn_editor_plugin_get_plugin_info(NMVpnEditorPlugin *plugin)
{
    NMVpnEditorPluginPrivate *priv;

    g_return_val_if_fail(NM_IS_VPN_EDITOR_PLUGIN(plugin), NULL);

    priv = NM_VPN_EDITOR_PLUGIN_TRY_GET_PRIVATE(plugin);
    return priv ? priv->plugin_info : NULL;
}

/**
 * nm_vpn_editor_plugin_set_plugin_info:
 * @plugin: the #NMVpnEditorPlugin instance
 * @plugin_info: (nullable): a #NMVpnPluginInfo instance or %NULL
 *
 * Set or clear the plugin-info instance.
 * This takes a weak reference on @plugin_info, to avoid circular
 * reference as the plugin-info might also reference the editor-plugin.
 *
 * Since: 1.4
 */
void
nm_vpn_editor_plugin_set_plugin_info(NMVpnEditorPlugin *plugin, NMVpnPluginInfo *plugin_info)
{
    NMVpnEditorPluginInterface *interface;
    NMVpnEditorPluginPrivate   *priv;

    g_return_if_fail(NM_IS_VPN_EDITOR_PLUGIN(plugin));

    if (!plugin_info) {
        priv = NM_VPN_EDITOR_PLUGIN_TRY_GET_PRIVATE(plugin);
        if (!priv)
            return;
    } else {
        g_return_if_fail(NM_IS_VPN_PLUGIN_INFO(plugin_info));
        priv = NM_VPN_EDITOR_PLUGIN_GET_PRIVATE(plugin);
    }

    if (priv->plugin_info == plugin_info)
        return;
    if (priv->plugin_info)
        g_object_remove_weak_pointer((GObject *) priv->plugin_info,
                                     (gpointer *) &priv->plugin_info);
    priv->plugin_info = plugin_info;
    if (priv->plugin_info)
        g_object_add_weak_pointer((GObject *) priv->plugin_info, (gpointer *) &priv->plugin_info);

    if (plugin_info) {
        interface = NM_VPN_EDITOR_PLUGIN_GET_INTERFACE(plugin);
        if (interface->notify_plugin_info_set)
            interface->notify_plugin_info_set(plugin, plugin_info);
    }
}

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

/**
 * nm_vpn_editor_plugin_get_vt:
 * @plugin: the #NMVpnEditorPlugin
 * @vt: (out): buffer to be filled with the VT table of the plugin
 * @vt_size: the size of the buffer. Can be 0 to only query the
 *   size of plugin's VT.
 *
 * Returns an opaque VT function table for the plugin to extend
 * functionality. The actual meaning of NMVpnEditorPluginVT is not
 * defined in public API of libnm, instead it must be agreed by
 * both the plugin and the caller. See the header-only file
 * 'nm-vpn-editor-plugin-call.h' which defines the meaning.
 *
 * Returns: the actual size of the @plugin's virtual function table.
 *
 * Since: 1.4
 **/
gsize
nm_vpn_editor_plugin_get_vt(NMVpnEditorPlugin *plugin, NMVpnEditorPluginVT *vt, gsize vt_size)
{
    const NMVpnEditorPluginVT  *p_vt      = NULL;
    gsize                       p_vt_size = 0;
    NMVpnEditorPluginInterface *interface;

    g_return_val_if_fail(NM_IS_VPN_EDITOR_PLUGIN(plugin), 0);

    if (vt_size) {
        g_return_val_if_fail(vt, 0);
        memset(vt, 0, vt_size);
    }

    interface = NM_VPN_EDITOR_PLUGIN_GET_INTERFACE(plugin);
    if (interface->get_vt) {
        p_vt = interface->get_vt(plugin, &p_vt_size);
        if (!p_vt)
            p_vt_size = 0;
        g_return_val_if_fail(p_vt_size, 0);
        memcpy(vt, p_vt, NM_MIN(vt_size, p_vt_size));
    }

    return p_vt_size;
}

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

static NMVpnEditorPlugin *
_nm_vpn_editor_plugin_load(const char               *plugin_name,
                           gboolean                  do_file_checks,
                           const char               *check_service,
                           int                       check_owner,
                           NMUtilsCheckFilePredicate check_file,
                           gpointer                  user_data,
                           GError                  **error)
{
    void                              *dl_module = NULL;
    gboolean                           loaded_before;
    NMVpnEditorPluginFactory           factory              = NULL;
    gs_unref_object NMVpnEditorPlugin *editor_plugin        = NULL;
    gs_free char                      *plugin_filename_free = NULL;
    const char                        *plugin_filename;
    gs_free_error GError              *factory_error = NULL;
    gs_free char                      *plug_name     = NULL;
    gs_free char                      *plug_service  = NULL;

    g_return_val_if_fail(plugin_name && *plugin_name, NULL);

    /* if @do_file_checks is FALSE, we pass plugin_name directly to
     * g_module_open().
     *
     * Otherwise, we allow for library names without path component.
     * In which case, we prepend the plugin directory and form an
     * absolute path. In that case, we perform checks on the file.
     *
     * One exception is that we don't allow for the "la" suffix. The
     * reason is that g_module_open() interprets files with this extension
     * special and we don't want that. */
    plugin_filename = plugin_name;
    if (do_file_checks) {
        if (!strchr(plugin_name, '/') && !g_str_has_suffix(plugin_name, ".la")) {
            plugin_filename_free = g_module_build_path(NMVPNDIR, plugin_name);
            plugin_filename      = plugin_filename_free;
        }
    }

    dl_module = dlopen(plugin_filename, RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD);
    if (!dl_module && do_file_checks) {
        /* If the module is already loaded, we skip the file checks.
         *
         * _nm_utils_check_module_file() fails with ENOENT if the plugin file
         * does not exist. That is relevant, because nm-applet checks for that. */
        if (!_nm_utils_check_module_file(plugin_filename,
                                         check_owner,
                                         check_file,
                                         user_data,
                                         error))
            return NULL;
    }

    if (dl_module) {
        loaded_before = TRUE;
    } else {
        loaded_before = FALSE;
        dl_module     = dlopen(plugin_filename, RTLD_LAZY | RTLD_LOCAL);
    }

    if (!dl_module) {
        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("cannot load plugin \"%s\": %s"),
                    plugin_name,
                    dlerror() ?: "unknown reason");
        return NULL;
    }

    factory = dlsym(dl_module, "nm_vpn_editor_plugin_factory");
    if (!factory) {
        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("failed to load nm_vpn_editor_plugin_factory() from %s (%s)"),
                    plugin_name,
                    dlerror());
        dlclose(dl_module);
        return NULL;
    }

    /* Note that factory() shouldn't be returning errors or failing.
     * We can't change its prototype as it would consistute an ABI break,
     * however it returning a failure would indicate a bug in the plugin. */
    editor_plugin = factory(&factory_error);

    if (loaded_before) {
        /* we want to leak the library, because the factory will register glib
         * types, which cannot be unregistered.
         *
         * However, if the library was already loaded before, we want to return
         * our part of the reference count. */
        dlclose(dl_module);
    }

    if (!editor_plugin) {
        if (factory_error) {
            g_propagate_error(error, factory_error);
            factory_error = NULL;
        } else {
            g_set_error(error,
                        NM_VPN_PLUGIN_ERROR,
                        NM_VPN_PLUGIN_ERROR_FAILED,
                        _("unknown error initializing plugin %s"),
                        plugin_name);
        }
        return NULL;
    }

    g_return_val_if_fail(G_IS_OBJECT(editor_plugin), NULL);

    /* Validate plugin properties */
    g_object_get(G_OBJECT(editor_plugin),
                 NM_VPN_EDITOR_PLUGIN_NAME,
                 &plug_name,
                 NM_VPN_EDITOR_PLUGIN_SERVICE,
                 &plug_service,
                 NULL);

    if (!plug_name || !*plug_name) {
        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("cannot load VPN plugin in '%s': missing plugin name"),
                    plugin_name);
        return NULL;
    }
    if (check_service && g_strcmp0(plug_service, check_service) != 0) {
        g_set_error(error,
                    NM_VPN_PLUGIN_ERROR,
                    NM_VPN_PLUGIN_ERROR_FAILED,
                    _("cannot load VPN plugin in '%s': invalid service name"),
                    plugin_name);
        return NULL;
    }

    return g_steal_pointer(&editor_plugin);
}

/**
 * nm_vpn_editor_plugin_load_from_file:
 * @plugin_name: The path or name of the shared library to load.
 *  The path must either be an absolute filename to an existing file.
 *  Alternatively, it can be the name (without path) of a library in the
 *  plugin directory of NetworkManager.
 * @check_service: if not-null, check that the loaded plugin advertises
 *  the given service.
 * @check_owner: if non-negative, check whether the file is owned
 *  by UID @check_owner or by root. In this case also check that
 *  the file is not writable by anybody else.
 * @check_file: (scope call): optional callback to validate the file prior to
 *   loading the shared library.
 * @user_data: user data for @check_file
 * @error: on failure the error reason.
 *
 * Load the shared library @plugin_name and create a new
 * #NMVpnEditorPlugin instance via the #NMVpnEditorPluginFactory
 * function.
 *
 * If @plugin_name is not an absolute path name, it assumes the file
 * is in the plugin directory of NetworkManager. In any case, the call
 * will do certain checks on the file before passing it to dlopen.
 * A consequence for that is, that you cannot omit the ".so" suffix
 * as you could for nm_vpn_editor_plugin_load().
 *
 * Returns: (transfer full): a new plugin instance or %NULL on error.
 *
 * Since: 1.2
 */
NMVpnEditorPlugin *
nm_vpn_editor_plugin_load_from_file(const char               *plugin_name,
                                    const char               *check_service,
                                    int                       check_owner,
                                    NMUtilsCheckFilePredicate check_file,
                                    gpointer                  user_data,
                                    GError                  **error)
{
    return _nm_vpn_editor_plugin_load(plugin_name,
                                      TRUE,
                                      check_service,
                                      check_owner,
                                      check_file,
                                      user_data,
                                      error);
}

/**
 * nm_vpn_editor_plugin_load:
 * @plugin_name: The name of the shared library to load.
 *  This path will be directly passed to dlopen() without
 *  further checks.
 * @check_service: if not-null, check that the loaded plugin advertises
 *  the given service.
 * @error: on failure the error reason.
 *
 * Load the shared library @plugin_name and create a new
 * #NMVpnEditorPlugin instance via the #NMVpnEditorPluginFactory
 * function.
 *
 * This is similar to nm_vpn_editor_plugin_load_from_file(), but
 * it does no validation of the plugin name, instead passes it directly
 * to dlopen(). If you have the full path to a plugin file,
 * nm_vpn_editor_plugin_load_from_file() is preferred.
 *
 * Returns: (transfer full): a new plugin instance or %NULL on error.
 *
 * Since: 1.4
 */
NMVpnEditorPlugin *
nm_vpn_editor_plugin_load(const char *plugin_name, const char *check_service, GError **error)
{
    return _nm_vpn_editor_plugin_load(plugin_name, FALSE, check_service, -1, NULL, NULL, error);
}

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

/**
 * nm_vpn_editor_plugin_get_editor:
 * @plugin: the #NMVpnEditorPlugin
 * @connection: the #NMConnection to be edited
 * @error: on return, an error or %NULL
 *
 * Returns: (transfer full): a new #NMVpnEditor or %NULL on error
 */
NMVpnEditor *
nm_vpn_editor_plugin_get_editor(NMVpnEditorPlugin *plugin, NMConnection *connection, GError **error)
{
    g_return_val_if_fail(NM_IS_VPN_EDITOR_PLUGIN(plugin), NULL);

    return NM_VPN_EDITOR_PLUGIN_GET_INTERFACE(plugin)->get_editor(plugin, connection, error);
}

NMVpnEditorPluginCapability
nm_vpn_editor_plugin_get_capabilities(NMVpnEditorPlugin *plugin)
{
    g_return_val_if_fail(NM_IS_VPN_EDITOR_PLUGIN(plugin), 0);

    return NM_VPN_EDITOR_PLUGIN_GET_INTERFACE(plugin)->get_capabilities(plugin);
}

/**
 * nm_vpn_editor_plugin_import:
 * @plugin: the #NMVpnEditorPlugin
 * @path: full path to the file to attempt to read into a new #NMConnection
 * @error: on return, an error or %NULL
 *
 * Returns: (transfer full): a new #NMConnection imported from @path, or %NULL
 * on error or if the file at @path was not recognized by this plugin
 */
NMConnection *
nm_vpn_editor_plugin_import(NMVpnEditorPlugin *plugin, const char *path, GError **error)
{
    g_return_val_if_fail(NM_IS_VPN_EDITOR_PLUGIN(plugin), NULL);

    if (nm_vpn_editor_plugin_get_capabilities(plugin) & NM_VPN_EDITOR_PLUGIN_CAPABILITY_IMPORT) {
        gs_free_error GError *error2 = NULL;

        g_return_val_if_fail(NM_VPN_EDITOR_PLUGIN_GET_INTERFACE(plugin)->import_from_file != NULL,
                             NULL);

        if (!error) {
            /* Some VPN plugins crash if error argument is omitted. Work around that
             * in libnm by always requesting an error.
             *
             * https://gitlab.gnome.org/GNOME/NetworkManager-vpnc/-/blob/c7d197477c94c5bae0396f0ef826db4d835e487d/properties/nm-vpnc-editor-plugin.c#L281
             **/
            error = &error2;
        }

        return NM_VPN_EDITOR_PLUGIN_GET_INTERFACE(plugin)->import_from_file(plugin, path, error);
    }

    g_set_error(error,
                NM_VPN_PLUGIN_ERROR,
                NM_VPN_PLUGIN_ERROR_FAILED,
                _("the plugin does not support import capability"));
    return NULL;
}

gboolean
nm_vpn_editor_plugin_export(NMVpnEditorPlugin *plugin,
                            const char        *path,
                            NMConnection      *connection,
                            GError           **error)
{
    g_return_val_if_fail(NM_IS_VPN_EDITOR_PLUGIN(plugin), FALSE);

    if (nm_vpn_editor_plugin_get_capabilities(plugin) & NM_VPN_EDITOR_PLUGIN_CAPABILITY_EXPORT) {
        g_return_val_if_fail(NM_VPN_EDITOR_PLUGIN_GET_INTERFACE(plugin)->export_to_file != NULL,
                             FALSE);
        return NM_VPN_EDITOR_PLUGIN_GET_INTERFACE(plugin)->export_to_file(plugin,
                                                                          path,
                                                                          connection,
                                                                          error);
    }

    g_set_error(error,
                NM_VPN_PLUGIN_ERROR,
                NM_VPN_PLUGIN_ERROR_FAILED,
                _("the plugin does not support export capability"));
    return FALSE;
}

char *
nm_vpn_editor_plugin_get_suggested_filename(NMVpnEditorPlugin *plugin, NMConnection *connection)
{
    g_return_val_if_fail(NM_IS_VPN_EDITOR_PLUGIN(plugin), NULL);

    if (NM_VPN_EDITOR_PLUGIN_GET_INTERFACE(plugin)->get_suggested_filename)
        return NM_VPN_EDITOR_PLUGIN_GET_INTERFACE(plugin)->get_suggested_filename(plugin,
                                                                                  connection);
    return NULL;
}
