/* Copyright (C) 2005 Emmanuele Bassi * Copyright (C) 2012-2021 MATE Developers * * This file is part of MATE Utils. * * MATE Utils 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. * * MATE Utils 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 MATE Utils. If not, see . */ /** * SECTION:gdict-client-context * @short_description: DICT client transport * * #GdictClientContext is an implementation of the #GdictContext interface. * It implements the Dictionary Protocol as defined by the RFC 2229 in order * to connect to a dictionary server. * * You should rarely instantiate this object directely: use an appropriate * #GdictSource instead. */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_UNISTD_H #include #endif #include #include #include "gdict-context-private.h" #include "gdict-context.h" #include "gdict-client-context.h" #include "gdict-enum-types.h" #include "gdict-marshal.h" #include "gdict-debug.h" #include "gdict-utils.h" #include "gdict-private.h" typedef enum { CMD_CLIENT, CMD_SHOW_DB, CMD_SHOW_STRAT, CMD_SHOW_INFO, /* not implemented */ CMD_SHOW_SERVER, /* not implemented */ CMD_MATCH, CMD_DEFINE, CMD_STATUS, /* not implemented */ CMD_OPTION_MIME, /* not implemented */ CMD_AUTH, /* not implemented */ CMD_HELP, /* not implemented */ CMD_QUIT, CMD_INVALID } GdictCommandType; #define IS_VALID_CMD(cmd) (((cmd) >= CMD_CLIENT) || ((cmd) < CMD_INVALID)) /* command strings: keep synced with the enum above! */ static const gchar *dict_command_strings[] = { "CLIENT", "SHOW DB", "SHOW STRAT", "SHOW INFO", "SHOW SERVER", "MATCH", "DEFINE", "STATUS", "OPTION MIME", "AUTH", "HELP", "QUIT", NULL }; /* command stata */ enum { S_START, S_STATUS, S_DATA, S_FINISH }; typedef struct { GdictCommandType cmd_type; gchar *cmd_string; guint state; /* optional parameters passed to the command */ gchar *database; gchar *strategy; gchar *word; /* buffer used to hold the reply from the server */ GString *buffer; gpointer data; GDestroyNotify data_destroy; } GdictCommand; /* The default string to be passed to the CLIENT command */ #define GDICT_DEFAULT_CLIENT "MATE Dictionary (" VERSION ")" /* Default server:port couple */ #define GDICT_DEFAULT_HOSTNAME "dict.org" #define GDICT_DEFAULT_PORT 2628 /* make the hostname lookup expire every five minutes */ #define HOSTNAME_LOOKUP_EXPIRE 300 /* wait 30 seconds between connection and receiving data on the line */ #define CONNECTION_TIMEOUT_SEC 30 enum { PROP_0, PROP_HOSTNAME, PROP_PORT, PROP_STATUS, PROP_CLIENT_NAME }; enum { CONNECTED, DISCONNECTED, LAST_SIGNAL }; static guint gdict_client_context_signals[LAST_SIGNAL] = { 0 }; struct _GdictClientContextPrivate { #ifdef ENABLE_IPV6 struct sockaddr_storage sockaddr; struct addrinfo *host6info; #else struct sockaddr_in sockaddr; #endif struct hostent *hostinfo; time_t last_lookup; gchar *hostname; gint port; GIOChannel *channel; guint source_id; guint timeout_id; GdictCommand *command; GQueue *commands_queue; gchar *client_name; GdictStatusCode status_code; guint local_only : 1; guint is_connecting : 1; }; static void gdict_client_context_iface_init (GdictContextIface *iface); G_DEFINE_TYPE_WITH_CODE (GdictClientContext, gdict_client_context, G_TYPE_OBJECT, G_ADD_PRIVATE (GdictClientContext) G_IMPLEMENT_INTERFACE (GDICT_TYPE_CONTEXT, gdict_client_context_iface_init)); /* GObject methods */ static void gdict_client_context_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); static void gdict_client_context_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); static void gdict_client_context_finalize (GObject *object); /* GdictContext methods */ static gboolean gdict_client_context_get_databases (GdictContext *context, GError **error); static gboolean gdict_client_context_get_strategies (GdictContext *context, GError **error); static gboolean gdict_client_context_define_word (GdictContext *context, const gchar *database, const gchar *word, GError **error); static gboolean gdict_client_context_match_word (GdictContext *context, const gchar *database, const gchar *strategy, const gchar *word, GError **error); static void gdict_client_context_clear_hostinfo (GdictClientContext *context); static gboolean gdict_client_context_lookup_server (GdictClientContext *context, GError **error); static gboolean gdict_client_context_io_watch_cb (GIOChannel *source, GIOCondition condition, GdictClientContext *context); static gboolean gdict_client_context_parse_line (GdictClientContext *context, const gchar *buffer); static void gdict_client_context_disconnect (GdictClientContext *context); static void gdict_client_context_force_disconnect (GdictClientContext *context); static void gdict_client_context_real_connected (GdictClientContext *context); static void gdict_client_context_real_disconnected (GdictClientContext *context); static GdictCommand *gdict_command_new (GdictCommandType cmd_type); static void gdict_command_free (GdictCommand *cmd); GQuark gdict_client_context_error_quark (void) { return g_quark_from_static_string ("gdict-client-context-error-quark"); } static void gdict_client_context_iface_init (GdictContextIface *iface) { iface->get_databases = gdict_client_context_get_databases; iface->get_strategies = gdict_client_context_get_strategies; iface->match_word = gdict_client_context_match_word; iface->define_word = gdict_client_context_define_word; } static void gdict_client_context_class_init (GdictClientContextClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gobject_class->set_property = gdict_client_context_set_property; gobject_class->get_property = gdict_client_context_get_property; gobject_class->finalize = gdict_client_context_finalize; g_object_class_override_property (gobject_class, GDICT_CONTEXT_PROP_LOCAL_ONLY, "local-only"); /** * GdictClientContext:client-name * * The name of the client using this context; it will be advertised when * connecting to the dictionary server. * * Since: 1.0 */ g_object_class_install_property (gobject_class, PROP_CLIENT_NAME, g_param_spec_string ("client-name", _("Client Name"), _("The name of the client of the context object"), NULL, (G_PARAM_READABLE | G_PARAM_WRITABLE))); /** * GdictClientContext:hostname * * The hostname of the dictionary server to connect to. * * Since: 1.0 */ g_object_class_install_property (gobject_class, PROP_HOSTNAME, g_param_spec_string ("hostname", _("Hostname"), _("The hostname of the dictionary server to connect to"), NULL, (G_PARAM_READABLE | G_PARAM_WRITABLE))); /** * GdictClientContext:port * * The port of the dictionary server to connect to. * * Since: 1.0 */ g_object_class_install_property (gobject_class, PROP_PORT, g_param_spec_uint ("port", _("Port"), _("The port of the dictionary server to connect to"), 0, 65535, GDICT_DEFAULT_PORT, (G_PARAM_READABLE | G_PARAM_WRITABLE))); /** * GdictClientContext:status * * The status code as returned by the dictionary server. * * Since: 1.0 */ g_object_class_install_property (gobject_class, PROP_STATUS, g_param_spec_enum ("status", _("Status"), _("The status code as returned by the dictionary server"), GDICT_TYPE_STATUS_CODE, GDICT_STATUS_INVALID, G_PARAM_READABLE)); /** * GdictClientContext::connected * @client: the object which received the signal * * Emitted when a #GdictClientContext has successfully established a * connection with a dictionary server. * * Since: 1.0 */ gdict_client_context_signals[CONNECTED] = g_signal_new ("connected", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GdictClientContextClass, connected), NULL, NULL, gdict_marshal_VOID__VOID, G_TYPE_NONE, 0); /** * GdictClientContext::disconnected * @client: the object which received the signal * * Emitted when a #GdictClientContext has disconnected from a dictionary * server. * * Since: 1.0 */ gdict_client_context_signals[DISCONNECTED] = g_signal_new ("disconnected", G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GdictClientContextClass, disconnected), NULL, NULL, gdict_marshal_VOID__VOID, G_TYPE_NONE, 0); klass->connected = gdict_client_context_real_connected; klass->disconnected = gdict_client_context_real_disconnected; } static void gdict_client_context_init (GdictClientContext *context) { GdictClientContextPrivate *priv; priv = gdict_client_context_get_instance_private (context); context->priv = priv; priv->hostname = NULL; priv->port = 0; priv->hostinfo = NULL; #ifdef ENABLE_IPV6 priv->host6info = NULL; #endif priv->last_lookup = (time_t) -1; priv->is_connecting = FALSE; priv->local_only = FALSE; priv->status_code = GDICT_STATUS_INVALID; priv->client_name = NULL; priv->command = NULL; priv->commands_queue = g_queue_new (); } static void gdict_client_context_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GdictClientContextPrivate *priv = gdict_client_context_get_instance_private (GDICT_CLIENT_CONTEXT (object)); switch (prop_id) { case PROP_HOSTNAME: g_free (priv->hostname); priv->hostname = g_strdup (g_value_get_string (value)); gdict_client_context_clear_hostinfo (GDICT_CLIENT_CONTEXT (object)); break; case PROP_PORT: priv->port = g_value_get_uint (value); break; case PROP_CLIENT_NAME: g_free (priv->client_name); priv->client_name = g_strdup (g_value_get_string (value)); break; case GDICT_CONTEXT_PROP_LOCAL_ONLY: priv->local_only = g_value_get_boolean (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gdict_client_context_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GdictClientContextPrivate *priv = gdict_client_context_get_instance_private (GDICT_CLIENT_CONTEXT (object)); switch (prop_id) { case PROP_STATUS: g_value_set_enum (value, priv->status_code); break; case PROP_HOSTNAME: g_value_set_string (value, priv->hostname); break; case PROP_PORT: g_value_set_uint (value, priv->port); break; case PROP_CLIENT_NAME: g_value_set_string (value, priv->client_name); break; case GDICT_CONTEXT_PROP_LOCAL_ONLY: g_value_set_boolean (value, priv->local_only); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gdict_client_context_finalize (GObject *object) { GdictClientContext *context = GDICT_CLIENT_CONTEXT (object); GdictClientContextPrivate *priv = context->priv; /* force disconnection */ gdict_client_context_force_disconnect (context); gdict_command_free (priv->command); if (priv->commands_queue) { g_queue_free_full (priv->commands_queue, (GDestroyNotify) gdict_command_free); priv->commands_queue = NULL; } g_free (priv->client_name); g_free (priv->hostname); #ifdef ENABLE_IPV6 if (priv->host6info) freeaddrinfo (priv->host6info); #endif /* chain up parent's finalize method */ G_OBJECT_CLASS (gdict_client_context_parent_class)->finalize (object); } /** * gdict_client_context_new: * @hostname: the hostname of a dictionary server, or %NULL for the * default server * @port: port to be used when connecting to the dictionary server, * or -1 for the default port * * Creates a new #GdictClientContext object for @hostname. Use this * object to connect and query the dictionary server using the Dictionary * Protocol as defined by RFC 2229. * * Return value: the newly created #GdictClientContext object. You should * free it using g_object_unref(). */ GdictContext * gdict_client_context_new (const gchar *hostname, gint port) { return g_object_new (GDICT_TYPE_CLIENT_CONTEXT, "hostname", (hostname != NULL ? hostname : GDICT_DEFAULT_HOSTNAME), "port", (port != -1 ? port : GDICT_DEFAULT_PORT), "client-name", GDICT_DEFAULT_CLIENT, NULL); } /** * gdict_client_context_set_hostname: * @context: a #GdictClientContext * @hostname: the hostname of a Dictionary server, or %NULL * * Sets @hostname as the hostname of the dictionary server to be used. * If @hostname is %NULL, the default dictionary server will be used. */ void gdict_client_context_set_hostname (GdictClientContext *context, const gchar *hostname) { g_return_if_fail (GDICT_IS_CLIENT_CONTEXT (context)); g_object_set (G_OBJECT (context), "hostname", (hostname != NULL ? hostname : GDICT_DEFAULT_HOSTNAME), NULL); } /** * gdict_client_context_get_hostname: * @context: a #GdictClientContext * * Gets the hostname of the dictionary server used by @context. * * Return value: the hostname of a dictionary server. The returned string is * owned by the #GdictClientContext object and should never be modified or * freed. */ const gchar * gdict_client_context_get_hostname (GdictClientContext *context) { gchar *hostname; g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), NULL); g_object_get (G_OBJECT (context), "hostname", &hostname, NULL); return hostname; } /** * gdict_client_context_set_port: * @context: a #GdictClientContext * @port: port of the dictionary server to be used, or -1 * * Sets the port of the dictionary server to be used when connecting. * * If @port is -1, the default port will be used. */ void gdict_client_context_set_port (GdictClientContext *context, gint port) { g_return_if_fail (GDICT_IS_CLIENT_CONTEXT (context)); g_object_set (G_OBJECT (context), "port", (port != -1 ? port : GDICT_DEFAULT_PORT), NULL); } /** * gdict_client_context_get_port: * @context: a #GdictClientContext * * Gets the port of the dictionary server used by @context. * * Return value: the number of the port. */ guint gdict_client_context_get_port (GdictClientContext *context) { guint port; g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), -1); g_object_get (G_OBJECT (context), "port", &port, NULL); return port; } /** * gdict_client_context_set_client: * @context: a #GdictClientContext * @client: the client name to use, or %NULL * * Sets @client as the client name to be used when advertising ourselves when * a connection the the dictionary server has been established. * If @client is %NULL, the default client name will be used. */ void gdict_client_context_set_client (GdictClientContext *context, const gchar *client) { g_return_if_fail (GDICT_IS_CLIENT_CONTEXT (context)); g_object_set (G_OBJECT (context), "client-name", (client != NULL ? client : GDICT_DEFAULT_CLIENT), NULL); } /** * gdict_client_context_get_client: * @context: a #GdictClientContext * * Gets the client name used by @context. See gdict_client_context_set_client(). * * Return value: the client name. The returned string is owned by the * #GdictClientContext object and should never be modified or freed. */ const gchar * gdict_client_context_get_client (GdictClientContext *context) { gchar *client_name; g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), NULL); g_object_get (G_OBJECT (context), "client-name", &client_name, NULL); return client_name; } /* creates a new command to be sent to the dictionary server */ static GdictCommand * gdict_command_new (GdictCommandType cmd_type) { GdictCommand *retval; g_assert (IS_VALID_CMD (cmd_type)); retval = g_slice_new0 (GdictCommand); retval->cmd_type = cmd_type; retval->state = S_START; return retval; } static void gdict_command_free (GdictCommand *cmd) { if (!cmd) return; g_free (cmd->cmd_string); switch (cmd->cmd_type) { case CMD_CLIENT: case CMD_QUIT: break; case CMD_SHOW_DB: case CMD_SHOW_STRAT: break; case CMD_MATCH: g_free (cmd->database); g_free (cmd->strategy); g_free (cmd->word); break; case CMD_DEFINE: g_free (cmd->database); g_free (cmd->word); break; default: break; } if (cmd->buffer) g_string_free (cmd->buffer, TRUE); if (cmd->data_destroy) cmd->data_destroy (cmd->data); g_slice_free (GdictCommand, cmd); } /* push @command into the head of the commands queue; the command queue is * a FIFO-like pipe: commands go into the head and are retrieved from the * tail. */ static gboolean gdict_client_context_push_command (GdictClientContext *context, GdictCommand *command) { GdictClientContextPrivate *priv; g_assert (GDICT_IS_CLIENT_CONTEXT (context)); g_assert (command != NULL); priv = context->priv; /* avoid pushing a command twice */ if (g_queue_find (priv->commands_queue, command)) { g_warning ("gdict_client_context_push_command() called on a command already in queue\n"); return FALSE; } GDICT_NOTE (DICT, "Pushing command ('%s') into the queue...", dict_command_strings[command->cmd_type]); g_queue_push_head (priv->commands_queue, command); return TRUE; } static GdictCommand * gdict_client_context_pop_command (GdictClientContext *context) { GdictClientContextPrivate *priv; GdictCommand *retval; g_assert (GDICT_IS_CLIENT_CONTEXT (context)); priv = context->priv; retval = (GdictCommand *) g_queue_pop_tail (priv->commands_queue); if (!retval) return NULL; GDICT_NOTE (DICT, "Getting command ('%s') from the queue...", dict_command_strings[retval->cmd_type]); return retval; } /* send @command on the wire */ static gboolean gdict_client_context_send_command (GdictClientContext *context, GdictCommand *command, GError **error) { GdictClientContextPrivate *priv; GError *write_error; gsize written_bytes; GIOStatus res; g_assert (GDICT_IS_CLIENT_CONTEXT (context)); g_assert (command != NULL && command->cmd_string != NULL); priv = context->priv; if (!priv->channel) { GDICT_NOTE (DICT, "No connection established"); g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_NO_CONNECTION, _("No connection to the dictionary server at '%s:%d'"), priv->hostname, priv->port); return FALSE; } write_error = NULL; res = g_io_channel_write_chars (priv->channel, command->cmd_string, -1, &written_bytes, &write_error); if (res != G_IO_STATUS_NORMAL) { g_propagate_error (error, write_error); return FALSE; } /* force flushing of the write buffer */ g_io_channel_flush (priv->channel, NULL); GDICT_NOTE (DICT, "Wrote %"G_GSIZE_FORMAT" bytes to the channel", written_bytes); return TRUE; } /* gdict_client_context_run_command: runs @command inside @context; this * function builds the command string and then passes it to the dictionary * server. */ static gboolean gdict_client_context_run_command (GdictClientContext *context, GdictCommand *command, GError **error) { GdictClientContextPrivate *priv; gchar *payload; GError *send_error; gboolean res; g_assert (GDICT_IS_CLIENT_CONTEXT (context)); g_assert (command != NULL); g_assert (IS_VALID_CMD (command->cmd_type)); GDICT_NOTE (DICT, "GdictCommand command =\n" "{\n" " .cmd_type = '%02d' ('%s');\n" " .database = '%s';\n" " .strategy = '%s';\n" " .word = '%s';\n" "}\n", command->cmd_type, dict_command_strings[command->cmd_type], command->database ? command->database : "", command->strategy ? command->strategy : "", command->word ? command->word : ""); priv = context->priv; g_assert (priv->command == NULL); priv->command = command; /* build the command string to be sent to the server */ switch (command->cmd_type) { case CMD_CLIENT: payload = g_shell_quote (priv->client_name); command->cmd_string = g_strdup_printf ("%s %s\r\n", dict_command_strings[CMD_CLIENT], payload); g_free (payload); break; case CMD_QUIT: command->cmd_string = g_strdup_printf ("%s\r\n", dict_command_strings[CMD_QUIT]); break; case CMD_SHOW_DB: command->cmd_string = g_strdup_printf ("%s\r\n", dict_command_strings[CMD_SHOW_DB]); break; case CMD_SHOW_STRAT: command->cmd_string = g_strdup_printf ("%s\r\n", dict_command_strings[CMD_SHOW_STRAT]); break; case CMD_MATCH: g_assert (command->word); payload = g_shell_quote (command->word); command->cmd_string = g_strdup_printf ("%s %s %s %s\r\n", dict_command_strings[CMD_MATCH], (command->database != NULL ? command->database : "!"), (command->strategy != NULL ? command->strategy : "*"), payload); g_free (payload); break; case CMD_DEFINE: g_assert (command->word); payload = g_shell_quote (command->word); command->cmd_string = g_strdup_printf ("%s %s %s\r\n", dict_command_strings[CMD_DEFINE], (command->database != NULL ? command->database : "!"), payload); g_free (payload); break; default: g_assert_not_reached (); break; } g_assert (command->cmd_string); GDICT_NOTE (DICT, "Sending command ('%s') to the server", dict_command_strings[command->cmd_type]); send_error = NULL; res = gdict_client_context_send_command (context, command, &send_error); if (!res) { g_propagate_error (error, send_error); return FALSE; } return TRUE; } /* we use this signal to advertise ourselves to the dictionary server */ static void gdict_client_context_real_connected (GdictClientContext *context) { GdictCommand *cmd; cmd = gdict_command_new (CMD_CLIENT); cmd->state = S_FINISH; /* the CLIENT command should be the first one in our queue, so we place * it above all other commands the user might have issued between the * first and the emission of the "connected" signal, by calling it * directely. */ gdict_client_context_run_command (context, cmd, NULL); } static void clear_command_queue (GdictClientContext *context) { GdictClientContextPrivate *priv = context->priv; if (priv->commands_queue) { g_queue_free_full (priv->commands_queue, (GDestroyNotify) gdict_command_free); } /* renew */ priv->commands_queue = g_queue_new (); } /* force a disconnection from the server */ static void gdict_client_context_force_disconnect (GdictClientContext *context) { GdictClientContextPrivate *priv = context->priv; if (priv->timeout_id) { g_source_remove (priv->timeout_id); priv->timeout_id = 0; } if (priv->source_id) { g_source_remove (priv->source_id); priv->source_id = 0; } if (priv->channel) { g_io_channel_shutdown (priv->channel, TRUE, NULL); g_io_channel_unref (priv->channel); priv->channel = NULL; } if (priv->command) { gdict_command_free (priv->command); priv->command = NULL; } clear_command_queue (context); } static void gdict_client_context_real_disconnected (GdictClientContext *context) { gdict_client_context_force_disconnect (context); } /* clear the lookup data */ static void gdict_client_context_clear_hostinfo (GdictClientContext *context) { GdictClientContextPrivate *priv; g_assert (GDICT_IS_CLIENT_CONTEXT (context)); priv = context->priv; #ifdef ENABLE_IPV6 if (!priv->host6info) return; #endif if (!priv->hostinfo) return; #ifdef ENABLE_IPV6 freeaddrinfo (priv->host6info); #endif priv->hostinfo = NULL; } /* gdict_client_context_lookup_server: perform an hostname lookup in order to * connect to the dictionary server */ static gboolean gdict_client_context_lookup_server (GdictClientContext *context, GError **error) { GdictClientContextPrivate *priv; gboolean is_expired = FALSE; time_t now; g_assert (GDICT_IS_CLIENT_CONTEXT (context)); priv = context->priv; /* we need the hostname, at this point */ g_assert (priv->hostname != NULL); time (&now); if (now >= (priv->last_lookup + HOSTNAME_LOOKUP_EXPIRE)) is_expired = TRUE; /* we already have resolved the hostname */ #ifdef ENABLE_IPV6 if (priv->host6info && !is_expired) return TRUE; #endif if (priv->hostinfo && !is_expired) return TRUE; /* clear any previously acquired lookup data */ gdict_client_context_clear_hostinfo (context); GDICT_NOTE (DICT, "Looking up hostname '%s'", priv->hostname); #ifdef ENABLE_IPV6 if (_gdict_has_ipv6 ()) { struct addrinfo hints, *res; GDICT_NOTE (DICT, "Hostname '%s' look-up (using IPv6)", priv->hostname); memset (&hints, 0, sizeof (hints)); hints.ai_socktype = SOCK_STREAM; if (getaddrinfo (priv->hostname, NULL, &hints, &(priv->host6info)) == 0) { for (res = priv->host6info; res; res = res->ai_next) if (res->ai_family == AF_INET6 || res->ai_family == AF_INET) break; if (!res) { g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_LOOKUP, _("Lookup failed for hostname '%s': no suitable resources found"), priv->hostname); return FALSE; } else { if (res->ai_family == AF_INET6) memcpy (&((struct sockaddr_in6 *) &priv->sockaddr)->sin6_addr, &((struct sockaddr_in6 *) res->ai_addr)->sin6_addr, sizeof (struct in6_addr)); if (res->ai_family == AF_INET) memcpy (&((struct sockaddr_in *) &priv->sockaddr)->sin_addr, &((struct sockaddr_in *) res->ai_addr)->sin_addr, sizeof (struct in_addr)); priv->sockaddr.ss_family = res->ai_family; GDICT_NOTE (DICT, "Hostname '%s' found (using IPv6)", priv->hostname); priv->last_lookup = time (NULL); return TRUE; } } else { g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_LOOKUP, _("Lookup failed for host '%s': %s"), priv->hostname, gai_strerror (errno)); return FALSE; } } else { #endif /* ENABLE_IPV6 */ /* if we don't support IPv6, fallback to usual IPv4 lookup */ GDICT_NOTE (DICT, "Hostname '%s' look-up (using IPv4)", priv->hostname); ((struct sockaddr_in *) &priv->sockaddr)->sin_family = AF_INET; priv->hostinfo = gethostbyname (priv->hostname); if (priv->hostinfo) { memcpy (&((struct sockaddr_in *) &(priv->sockaddr))->sin_addr, priv->hostinfo->h_addr, priv->hostinfo->h_length); GDICT_NOTE (DICT, "Hostname '%s' found (using IPv4)", priv->hostname); priv->last_lookup = time (NULL); return TRUE; } else { g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_LOOKUP, _("Lookup failed for host '%s': host not found"), priv->hostname); return FALSE; } #ifdef ENABLE_IPV6 } #endif g_assert_not_reached (); return FALSE; } /* gdict_client_context_parse_line: parses a line from the dictionary server * this is the core of the RFC2229 protocol implementation, here's where * the magic happens. */ static gboolean gdict_client_context_parse_line (GdictClientContext *context, const gchar *buffer) { GdictClientContextPrivate *priv; GError *server_error = NULL; g_assert (GDICT_IS_CLIENT_CONTEXT (context)); g_assert (buffer != NULL); priv = context->priv; GDICT_NOTE (DICT, "parse buffer: '%s'", buffer); /* connection is a special case: we don't have a command, so we just * make sure that the server replied with the correct code. WARNING: * the server might be shutting down or not available, so we must * take into account those responses too! */ if (!priv->command) { if (priv->status_code == GDICT_STATUS_CONNECT) { /* the server accepts our connection */ g_signal_emit (context, gdict_client_context_signals[CONNECTED], 0); return TRUE; } else if ((priv->status_code == GDICT_STATUS_SERVER_DOWN) || (priv->status_code == GDICT_STATUS_SHUTDOWN)) { /* the server is shutting down or is not available */ g_set_error (&server_error, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_SERVER_DOWN, _("Unable to connect to the dictionary server " "at '%s:%d'. The server replied with " "code %d (server down)"), priv->hostname, priv->port, priv->status_code); g_signal_emit_by_name (context, "error", server_error); g_error_free (server_error); return TRUE; } else { GError *parse_error = NULL; g_set_error (&parse_error, GDICT_CONTEXT_ERROR, GDICT_CONTEXT_ERROR_PARSE, _("Unable to parse the dictionary server reply\n: '%s'"), buffer); g_signal_emit_by_name (context, "error", parse_error); g_error_free (parse_error); return FALSE; } } /* disconnection is another special case: the server replies with code * 221, and closes the connection; we emit the "disconnected" signal * and close the connection on our side. */ if (priv->status_code == GDICT_STATUS_QUIT) { g_signal_emit (context, gdict_client_context_signals[DISCONNECTED], 0); return TRUE; } /* here we catch all the errors codes that the server might give us */ server_error = NULL; switch (priv->status_code) { case GDICT_STATUS_NO_MATCH: g_set_error (&server_error, GDICT_CONTEXT_ERROR, GDICT_CONTEXT_ERROR_NO_MATCH, _("No definitions found for '%s'"), priv->command->word); GDICT_NOTE (DICT, "No match: %s", server_error->message); g_signal_emit_by_name (context, "error", server_error); g_error_free (server_error); server_error = NULL; priv->command->state = S_FINISH; break; case GDICT_STATUS_BAD_DATABASE: g_set_error (&server_error, GDICT_CONTEXT_ERROR, GDICT_CONTEXT_ERROR_INVALID_DATABASE, _("Invalid database '%s'"), priv->command->database); GDICT_NOTE (DICT, "Bad DB: %s", server_error->message); g_signal_emit_by_name (context, "error", server_error); g_error_free (server_error); server_error = NULL; priv->command->state = S_FINISH; break; case GDICT_STATUS_BAD_STRATEGY: g_set_error (&server_error, GDICT_CONTEXT_ERROR, GDICT_CONTEXT_ERROR_INVALID_STRATEGY, _("Invalid strategy '%s'"), priv->command->strategy); GDICT_NOTE (DICT, "Bad strategy: %s", server_error->message); g_signal_emit_by_name (context, "error", server_error); g_error_free (server_error); server_error = NULL; priv->command->state = S_FINISH; break; case GDICT_STATUS_BAD_COMMAND: g_set_error (&server_error, GDICT_CONTEXT_ERROR, GDICT_CONTEXT_ERROR_INVALID_COMMAND, _("Bad command '%s'"), dict_command_strings[priv->command->cmd_type]); GDICT_NOTE (DICT, "Bad command: %s", server_error->message); g_signal_emit_by_name (context, "error", server_error); g_error_free (server_error); server_error = NULL; priv->command->state = S_FINISH; break; case GDICT_STATUS_BAD_PARAMETERS: g_set_error (&server_error, GDICT_CONTEXT_ERROR, GDICT_CONTEXT_ERROR_INVALID_COMMAND, _("Bad parameters for command '%s'"), dict_command_strings[priv->command->cmd_type]); GDICT_NOTE (DICT, "Bad params: %s", server_error->message); g_signal_emit_by_name (context, "error", server_error); g_error_free (server_error); server_error = NULL; priv->command->state = S_FINISH; break; case GDICT_STATUS_NO_DATABASES_PRESENT: g_set_error (&server_error, GDICT_CONTEXT_ERROR, GDICT_CONTEXT_ERROR_NO_DATABASES, _("No databases found on dictionary server at '%s'"), priv->hostname); GDICT_NOTE (DICT, "No DB: %s", server_error->message); g_signal_emit_by_name (context, "error", server_error); g_error_free (server_error); server_error = NULL; priv->command->state = S_FINISH; break; case GDICT_STATUS_NO_STRATEGIES_PRESENT: g_set_error (&server_error, GDICT_CONTEXT_ERROR, GDICT_CONTEXT_ERROR_NO_STRATEGIES, _("No strategies found on dictionary server at '%s'"), priv->hostname); GDICT_NOTE (DICT, "No strategies: %s", server_error->message); g_signal_emit_by_name (context, "error", server_error); g_error_free (server_error); server_error = NULL; priv->command->state = S_FINISH; break; default: GDICT_NOTE (DICT, "non-error code: %d", priv->status_code); break; } /* server replied with 'ok' or the command has reached its FINISH state, * so now we are clear for destroying the current command and check if * there are other commands on the queue, and run them. */ if ((priv->status_code == GDICT_STATUS_OK) || (priv->command->state == S_FINISH)) { GdictCommand *new_command; GError *run_error; GdictCommandType last_cmd; last_cmd = priv->command->cmd_type; gdict_command_free (priv->command); priv->command = NULL; /* notify the end of a command - ignore CLIENT and QUIT commands, as * we issue them ourselves */ if ((last_cmd != CMD_CLIENT) && (last_cmd != CMD_QUIT)) g_signal_emit_by_name (context, "lookup-end"); /* pop the next command from the queue */ new_command = gdict_client_context_pop_command (context); if (!new_command) { /* if the queue is empty, quit */ gdict_client_context_disconnect (context); new_command = gdict_client_context_pop_command (context); } run_error = NULL; gdict_client_context_run_command (context, new_command, &run_error); if (run_error) { g_signal_emit_by_name (context, "error", run_error); g_error_free (run_error); } return TRUE; } GDICT_NOTE (DICT, "check command %d ('%s')[state:%d]", priv->command->cmd_type, dict_command_strings[priv->command->cmd_type], priv->command->state); /* check command type */ switch (priv->command->cmd_type) { case CMD_CLIENT: case CMD_QUIT: break; case CMD_SHOW_DB: if (priv->status_code == GDICT_STATUS_N_DATABASES_PRESENT) { gchar *p; priv->command->state = S_DATA; p = g_utf8_strchr (buffer, -1, ' '); if (p) p = g_utf8_next_char (p); GDICT_NOTE (DICT, "server replied: %d databases found", atoi (p)); } else if (0 == strcmp (buffer, ".")) priv->command->state = S_FINISH; else { GdictDatabase *db; gchar *name, *full, *p; g_assert (priv->command->state == S_DATA); /* first token: database name; * second token: database description; */ name = (gchar *) buffer; if (!name) break; p = g_utf8_strchr (name, -1, ' '); if (p) *p = '\0'; full = g_utf8_next_char (p); if (full[0] == '\"') full = g_utf8_next_char (full); p = g_utf8_strchr (full, -1, '\"'); if (p) *p = '\0'; db = _gdict_database_new (name); db->full_name = g_strdup (full); g_signal_emit_by_name (context, "database-found", db); gdict_database_unref (db); } break; case CMD_SHOW_STRAT: if (priv->status_code == GDICT_STATUS_N_STRATEGIES_PRESENT) { gchar *p; priv->command->state = S_DATA; p = g_utf8_strchr (buffer, -1, ' '); if (p) p = g_utf8_next_char (p); GDICT_NOTE (DICT, "server replied: %d strategies found", atoi (p)); } else if (0 == strcmp (buffer, ".")) priv->command->state = S_FINISH; else { GdictStrategy *strat; gchar *name, *desc, *p; g_assert (priv->command->state == S_DATA); name = (gchar *) buffer; if (!name) break; p = g_utf8_strchr (name, -1, ' '); if (p) *p = '\0'; desc = g_utf8_next_char (p); if (desc[0] == '\"') desc = g_utf8_next_char (desc); p = g_utf8_strchr (desc, -1, '\"'); if (p) *p = '\0'; strat = _gdict_strategy_new (name); strat->description = g_strdup (desc); g_signal_emit_by_name (context, "strategy-found", strat); gdict_strategy_unref (strat); } break; case CMD_DEFINE: if (priv->status_code == GDICT_STATUS_N_DEFINITIONS_RETRIEVED) { GdictDefinition *def; gchar *p; priv->command->state = S_STATUS; p = g_utf8_strchr (buffer, -1, ' '); if (p) p = g_utf8_next_char (p); GDICT_NOTE (DICT, "server replied: %d definitions found", atoi (p)); def = _gdict_definition_new (atoi (p)); priv->command->data = def; priv->command->data_destroy = (GDestroyNotify) gdict_definition_unref; } else if (priv->status_code == GDICT_STATUS_WORD_DB_NAME) { GdictDefinition *def; gchar *word, *db_name, *db_full, *p; word = (gchar *) buffer; /* skip the status code */ word = g_utf8_strchr (word, -1, ' '); word = g_utf8_next_char (word); if (word[0] == '\"') word = g_utf8_next_char (word); p = g_utf8_strchr (word, -1, '\"'); if (p) *p = '\0'; p = g_utf8_next_char (p); /* the database name is not protected by "" */ db_name = g_utf8_next_char (p); if (!db_name) break; p = g_utf8_strchr (db_name, -1, ' '); if (p) *p = '\0'; p = g_utf8_next_char (p); db_full = g_utf8_next_char (p); if (!db_full) break; if (db_full[0] == '\"') db_full = g_utf8_next_char (db_full); p = g_utf8_strchr (db_full, -1, '\"'); if (p) *p = '\0'; def = (GdictDefinition *) priv->command->data; GDICT_NOTE (DICT, "{ word = '%s', db_name = '%s', db_full = '%s' }", word, db_name, db_full); def->word = g_strdup (word); def->database_name = g_strdup (db_name); def->database_full = g_strdup (db_full); def->definition = NULL; priv->command->state = S_DATA; } else if (strcmp (buffer, ".") == 0) { GdictDefinition *def; gint num; g_assert (priv->command->state == S_DATA); def = (GdictDefinition *) priv->command->data; if (!def) break; def->definition = g_string_free (priv->command->buffer, FALSE); /* store the numer of definitions */ num = def->total; g_signal_emit_by_name (context, "definition-found", def); gdict_definition_unref (def); priv->command->buffer = NULL; priv->command->data = _gdict_definition_new (num); priv->command->state = S_STATUS; } else { g_assert (priv->command->state == S_DATA); if (!priv->command->buffer) priv->command->buffer = g_string_new (NULL); GDICT_NOTE (DICT, "appending to buffer:\n %s", buffer); /* TODO - collapse '..' to '.' */ g_string_append_printf (priv->command->buffer, "%s\n", buffer); } break; case CMD_MATCH: if (priv->status_code == GDICT_STATUS_N_MATCHES_FOUND) { gchar *p; priv->command->state = S_DATA; p = g_utf8_strchr (buffer, -1, ' '); if (p) p = g_utf8_next_char (p); GDICT_NOTE (DICT, "server replied: %d matches found", atoi (p)); } else if (0 == strcmp (buffer, ".")) priv->command->state = S_FINISH; else { GdictMatch *match; gchar *word, *db_name, *p; g_assert (priv->command->state == S_DATA); db_name = (gchar *) buffer; if (!db_name) break; p = g_utf8_strchr (db_name, -1, ' '); if (p) *p = '\0'; word = g_utf8_next_char (p); if (word[0] == '\"') word = g_utf8_next_char (word); p = g_utf8_strchr (word, -1, '\"'); if (p) *p = '\0'; match = _gdict_match_new (word); match->database = g_strdup (db_name); g_signal_emit_by_name (context, "match-found", match); gdict_match_unref (match); } break; default: g_assert_not_reached (); break; } return TRUE; } /* retrieve the status code from the server response line */ static gint get_status_code (const gchar *line, gint old_status) { gchar *status; gint possible_status, retval; if (strlen (line) < 3) return 0; if (!g_unichar_isdigit (line[0]) || !g_unichar_isdigit (line[1]) || !g_unichar_isdigit (line[2])) return 0; if (!g_unichar_isspace (line[3])) return 0; status = g_strndup (line, 3); possible_status = atoi (status); g_free (status); /* status whitelisting: sometimes, a database *cough* moby-thes *cough* * might return a number as first word; we do a small check here for * invalid status codes based on the previously set status; we don't check * the whole line, as we need only to be sure that the status code is * consistent with what we expect. */ switch (old_status) { case GDICT_STATUS_WORD_DB_NAME: case GDICT_STATUS_N_MATCHES_FOUND: if (possible_status == GDICT_STATUS_OK) retval = possible_status; else retval = 0; break; case GDICT_STATUS_N_DEFINITIONS_RETRIEVED: if (possible_status == GDICT_STATUS_WORD_DB_NAME) retval = possible_status; else retval = 0; break; default: retval = possible_status; break; } return retval; } static gboolean gdict_client_context_io_watch_cb (GIOChannel *channel, GIOCondition condition, GdictClientContext *context) { GdictClientContextPrivate *priv; g_assert (GDICT_IS_CLIENT_CONTEXT (context)); priv = context->priv; /* since this is an asynchronous channel, we might end up here * even though the channel has been shut down. */ if (!priv->channel) { g_warning ("No channel available\n"); return FALSE; } if (priv->is_connecting) { priv->is_connecting = FALSE; if (priv->timeout_id) { g_source_remove (priv->timeout_id); priv->timeout_id = 0; } } if (condition & G_IO_ERR) { GError *err = NULL; g_set_error (&err, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_SOCKET, _("Connection failed to the dictionary server at %s:%d"), priv->hostname, priv->port); g_signal_emit_by_name (context, "error", err); g_error_free (err); return FALSE; } while (1) { GIOStatus res; guint status_code; GError *read_err; gsize len, term; gchar *line; gboolean parse_res; /* we might sever the connection while still inside the read loop, * so we must check the state of the channel before actually doing * the line reading, otherwise we'll end up with death, destruction * and chaos on all earth. oh, and an assertion failed inside * g_io_channel_read_line(). */ if (!priv->channel) break; read_err = NULL; res = g_io_channel_read_line (priv->channel, &line, &len, &term, &read_err); if (res == G_IO_STATUS_ERROR) { if (read_err) { GError *err = NULL; g_set_error (&err, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_SOCKET, _("Error while reading reply from server:\n%s"), read_err->message); g_signal_emit_by_name (context, "error", err); g_error_free (err); g_error_free (read_err); } gdict_client_context_force_disconnect (context); return FALSE; } if (len == 0) break; /* truncate the line terminator before parsing */ line[term] = '\0'; status_code = get_status_code (line, priv->status_code); if ((status_code == 0) || (GDICT_IS_VALID_STATUS_CODE (status_code))) { priv->status_code = status_code; GDICT_NOTE (DICT, "new status = '%d'", priv->status_code); } else priv->status_code = GDICT_STATUS_INVALID; /* notify changes only for valid status codes */ if (priv->status_code != GDICT_STATUS_INVALID) g_object_notify (G_OBJECT (context), "status"); parse_res = gdict_client_context_parse_line (context, line); if (!parse_res) { g_free (line); g_warning ("Parsing failed"); gdict_client_context_force_disconnect (context); return FALSE; } g_free (line); } return TRUE; } static gboolean check_for_connection (gpointer data) { GdictClientContext *context = data; #if 0 g_debug (G_STRLOC ": checking for connection (is connecting:%s)", context->priv->is_connecting ? "true" : "false"); #endif if (context == NULL) return FALSE; if (context->priv->is_connecting) { GError *err = NULL; GDICT_NOTE (DICT, "Forcing a disconnection due to timeout"); g_set_error (&err, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_SOCKET, _("Connection timeout for the dictionary server at '%s:%d'"), context->priv->hostname, context->priv->port); g_signal_emit_by_name (context, "error", err); g_error_free (err); gdict_client_context_force_disconnect (context); } /* this is a one-off operation */ return FALSE; } static gboolean gdict_client_context_connect (GdictClientContext *context, GError **error) { GdictClientContextPrivate *priv; GError *lookup_error, *flags_error; gboolean res; gint sock_fd, sock_res; gsize addrlen; GIOFlags flags; g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), FALSE); priv = context->priv; if (!priv->hostname) { g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_LOOKUP, _("No hostname defined for the dictionary server")); return FALSE; } /* forgive the absence of a port */ if (!priv->port) priv->port = GDICT_DEFAULT_PORT; priv->is_connecting = TRUE; lookup_error = NULL; res = gdict_client_context_lookup_server (context, &lookup_error); if (!res) { g_propagate_error (error, lookup_error); return FALSE; } #ifdef ENABLE_IPV6 if (priv->sockaddr.ss_family == AF_INET6) ((struct sockaddr_in6 *) &priv->sockaddr)->sin6_port = g_htons (priv->port); else #endif ((struct sockaddr_in *) &priv->sockaddr)->sin_port = g_htons (priv->port); #ifdef ENABLE_IPV6 if (priv->sockaddr.ss_family == AF_INET6) { sock_fd = socket (AF_INET6, SOCK_STREAM, 0); if (sock_fd < 0) { g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_SOCKET, _("Unable to create socket")); return FALSE; } addrlen = sizeof (struct sockaddr_in6); } else { #endif /* ENABLE_IPV6 */ sock_fd = socket (AF_INET, SOCK_STREAM, 0); if (sock_fd < 0) { g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_SOCKET, _("Unable to create socket")); return FALSE; } addrlen = sizeof (struct sockaddr_in); #ifdef ENABLE_IPV6 } #endif priv->channel = g_io_channel_unix_new (sock_fd); /* RFC2229 mandates the usage of UTF-8, so we force this encoding */ g_io_channel_set_encoding (priv->channel, "UTF-8", NULL); g_io_channel_set_line_term (priv->channel, "\r\n", 2); /* make sure that the channel is non-blocking */ flags = g_io_channel_get_flags (priv->channel); flags |= G_IO_FLAG_NONBLOCK; flags_error = NULL; g_io_channel_set_flags (priv->channel, flags, &flags_error); if (flags_error) { g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_SOCKET, _("Unable to set the channel as non-blocking: %s"), flags_error->message); g_error_free (flags_error); g_io_channel_unref (priv->channel); return FALSE; } /* let the magic begin */ sock_res = connect (sock_fd, (struct sockaddr *) &priv->sockaddr, addrlen); if ((sock_res != 0) && (errno != EINPROGRESS)) { g_set_error (error, GDICT_CLIENT_CONTEXT_ERROR, GDICT_CLIENT_CONTEXT_ERROR_SOCKET, _("Unable to connect to the dictionary server at '%s:%d'"), priv->hostname, priv->port); return FALSE; } priv->timeout_id = g_timeout_add_seconds (CONNECTION_TIMEOUT_SEC, check_for_connection, context); /* XXX - remember that g_io_add_watch() increases the reference count * of the GIOChannel we are using. */ priv->source_id = g_io_add_watch (priv->channel, (G_IO_IN | G_IO_ERR), (GIOFunc) gdict_client_context_io_watch_cb, context); return TRUE; } static void gdict_client_context_disconnect (GdictClientContext *context) { GdictCommand *cmd; g_return_if_fail (GDICT_IS_CLIENT_CONTEXT (context)); /* instead of just breaking the connection to the server, we push * a QUIT command on the queue, and wait for every other scheduled * command to perform; this allows the creation of a batch of * commands. */ cmd = gdict_command_new (CMD_QUIT); cmd->state = S_FINISH; gdict_client_context_push_command (context, cmd); } static gboolean gdict_client_context_is_connected (GdictClientContext *context) { g_assert (GDICT_IS_CLIENT_CONTEXT (context)); /* we are in the middle of a connection attempt */ if (context->priv->is_connecting) return TRUE; return (context->priv->channel != NULL && context->priv->source_id != 0); } static gboolean gdict_client_context_get_databases (GdictContext *context, GError **error) { GdictClientContext *client_ctx; GdictCommand *cmd; g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), FALSE); client_ctx = GDICT_CLIENT_CONTEXT (context); g_signal_emit_by_name (context, "lookup-start"); if (!gdict_client_context_is_connected (client_ctx)) { GError *connect_error = NULL; gdict_client_context_connect (client_ctx, &connect_error); if (connect_error) { g_signal_emit_by_name (context, "lookup-end"); g_propagate_error (error, connect_error); return FALSE; } } cmd = gdict_command_new (CMD_SHOW_DB); return gdict_client_context_push_command (client_ctx, cmd); } static gboolean gdict_client_context_get_strategies (GdictContext *context, GError **error) { GdictClientContext *client_ctx; GdictCommand *cmd; g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), FALSE); client_ctx = GDICT_CLIENT_CONTEXT (context); g_signal_emit_by_name (context, "lookup-start"); if (!gdict_client_context_is_connected (client_ctx)) { GError *connect_error = NULL; gdict_client_context_connect (client_ctx, &connect_error); if (connect_error) { g_signal_emit_by_name (context, "lookup-end"); g_propagate_error (error, connect_error); return FALSE; } } cmd = gdict_command_new (CMD_SHOW_STRAT); return gdict_client_context_push_command (client_ctx, cmd); } static gboolean gdict_client_context_define_word (GdictContext *context, const gchar *database, const gchar *word, GError **error) { GdictClientContext *client_ctx; GdictCommand *cmd; g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), FALSE); client_ctx = GDICT_CLIENT_CONTEXT (context); g_signal_emit_by_name (context, "lookup-start"); if (!gdict_client_context_is_connected (client_ctx)) { GError *connect_error = NULL; gdict_client_context_connect (client_ctx, &connect_error); if (connect_error) { g_signal_emit_by_name (context, "lookup-end"); g_propagate_error (error, connect_error); return FALSE; } } cmd = gdict_command_new (CMD_DEFINE); cmd->database = g_strdup ((database != NULL ? database : GDICT_DEFAULT_DATABASE)); cmd->word = g_utf8_normalize (word, -1, G_NORMALIZE_NFC); return gdict_client_context_push_command (client_ctx, cmd); } static gboolean gdict_client_context_match_word (GdictContext *context, const gchar *database, const gchar *strategy, const gchar *word, GError **error) { GdictClientContext *client_ctx; GdictCommand *cmd; g_return_val_if_fail (GDICT_IS_CLIENT_CONTEXT (context), FALSE); client_ctx = GDICT_CLIENT_CONTEXT (context); g_signal_emit_by_name (context, "lookup-start"); if (!gdict_client_context_is_connected (client_ctx)) { GError *connect_error = NULL; gdict_client_context_connect (client_ctx, &connect_error); if (connect_error) { g_signal_emit_by_name (context, "lookup-end"); g_propagate_error (error, connect_error); return FALSE; } } cmd = gdict_command_new (CMD_MATCH); cmd->database = g_strdup ((database != NULL ? database : GDICT_DEFAULT_DATABASE)); cmd->strategy = g_strdup ((strategy != NULL ? strategy : GDICT_DEFAULT_STRATEGY)); cmd->word = g_utf8_normalize (word, -1, G_NORMALIZE_NFC); return gdict_client_context_push_command (client_ctx, cmd); }