1 /*
2 * Copyright (C) 2004, 2005 Jean-Yves Lefort
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of Jean-Yves Lefort nor the names of its contributors
14 * may be used to endorse or promote products derived from this software
15 * without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
18 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
19 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
20 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
22 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 * POSSIBILITY OF SUCH DAMAGE.
30 */
31
32 #include "config.h"
33 #include <string.h>
34 #include <stdarg.h>
35 #include <stdlib.h>
36 #include <glib/gi18n-lib.h>
37 #include <libsoup/soup.h>
38 #ifdef HAVE_LIBSOUP22
39 #include <libsoup/soup-message-filter.h>
40 #endif
41 #include <libxml/HTMLparser.h>
42 #include "translate.h"
43 #include "translate-generic-service.h"
44 #include "translate-generic-main.h"
45 #include "translate-generic-parser.h"
46 #include "translate-generic-soup-cookie-jar.h"
47
48 #ifdef HAVE_LIBSOUP22
49 #define soup_message_headers_get soup_message_get_header
50 #define soup_message_headers_append soup_message_add_header
51 #define SoupURI SoupUri
52 #define trans_SOUP_MESSAGE_RESPONSE_BODY(msg) ((msg)->response.body)
53 #define trans_SOUP_MESSAGE_RESPONSE_LENGTH(msg) ((msg)->response.length)
54 #else
55 #define trans_SOUP_MESSAGE_RESPONSE_BODY(msg) ((msg)->response_body->data)
56 #define trans_SOUP_MESSAGE_RESPONSE_LENGTH(msg) ((msg)->response_body->length)
57 #endif
58
59 #define MAKE_WARNING_PREFIX(service, group_pos, attribute, element) \
60 g_strdup_printf(_("in %s, group %i, \"%s\" attribute of \"%s\" element"), \
61 translate_service_get_name((service)), \
62 (group_pos), (attribute), (element))
63
64 enum {
65 PROP_0,
66 PROP_GROUPS
67 };
68
69 struct _TranslateGenericServicePrivate
70 {
71 GSList *groups;
72 };
73
74 typedef struct
75 {
76 GSList **pairs;
77 TranslatePairFlags flags;
78 } GetPairsInfo;
79
80 typedef struct
81 {
82 gboolean found;
83 const char *from;
84 const char *to;
85 } GetGroupInfo;
86
87 typedef enum
88 {
89 TRANSFER_FOLLOW_REFRESH = 1 << 0,
90 TRANSFER_CONVERT = 1 << 1
91 } TransferFlags;
92
93 typedef enum
94 {
95 HTML_CONTEXT_PRE_HEAD,
96 HTML_CONTEXT_HEAD,
97 HTML_CONTEXT_POST_HEAD
98 } HTMLContext;
99
100 typedef struct
101 {
102 SoupSession *session;
103 TranslateProgressFunc progress_func;
104 gpointer user_data;
105 unsigned int length;
106 unsigned int received;
107
108 /* HTML parser */
109 gboolean parse_html;
110 HTMLContext html_context;
111 GHashTable *html_http_equiv;
112 } TransferInfo;
113
114 static GObjectClass *parent_class = NULL;
115
116 static void translate_generic_service_register_type (GType *type);
117 static void translate_generic_service_class_init (TranslateGenericServiceClass *class);
118 static void translate_generic_service_init (TranslateGenericService *service);
119 static void translate_generic_service_finalize (GObject *object);
120 static void translate_generic_service_set_property (GObject *object,
121 unsigned int prop_id,
122 const GValue *value,
123 GParamSpec *pspec);
124
125 static gboolean translate_generic_service_get_pairs (TranslateService *service,
126 GSList **pairs,
127 TranslateProgressFunc progress_func,
128 gpointer user_data,
129 GError **err);
130 static gboolean translate_generic_service_get_pairs_cb (const char *from,
131 const char *to,
132 gpointer user_data);
133
134 static TranslateGenericGroup *translate_generic_service_get_group (TranslateGenericService *service,
135 const char *from,
136 const char *to,
137 int *pos);
138 static gboolean translate_generic_service_get_group_cb (const char *from,
139 const char *to,
140 gpointer user_data);
141
142 static char *translate_generic_service_get (const char *uri,
143 const char *post,
144 const char *post_content_type,
145 const GSList *headers,
146 TransferFlags flags,
147 TranslateProgressFunc progress_func,
148 gpointer user_data,
149 GError **err);
150
151 static const char *translate_generic_service_get_header (SoupMessage *message,
152 TransferInfo *info,
153 const char *name);
154
155 static void translate_generic_service_log_connect (SoupMessage *message);
156 #ifdef HAVE_LIBSOUP22
157 static void translate_generic_service_log_wrote_headers_h (SoupMessage *message,
158 gpointer user_data);
159 static void translate_generic_service_log_wrote_body_h (SoupMessage *message,
160 gpointer user_data);
161 static void translate_generic_service_log_got_headers_h (SoupMessage *message,
162 gpointer user_data);
163 static void translate_generic_service_log_got_body_h (SoupMessage *message,
164 gpointer user_data);
165 static void translate_generic_service_log_headers_cb (const char *key,
166 const char *value,
167 gpointer user_data);
168 #else
169 static void translate_generic_service_log_printer (SoupLogger *logger,
170 SoupLoggerLogLevel level,
171 char direction,
172 const char *data,
173 gpointer user_data);
174 #endif
175
176 static void translate_generic_service_progress_got_headers_h (SoupMessage *message,
177 gpointer user_data);
178 static void translate_generic_service_progress_got_chunk_h (SoupMessage *message,
179 #ifdef HAVE_LIBSOUP24
180 SoupBuffer *chunk,
181 #endif
182 gpointer user_data);
183
184 static void translate_generic_service_html_got_headers_h (SoupMessage *message,
185 gpointer user_data);
186 static void translate_generic_service_html_got_body_h (SoupMessage *message,
187 gpointer user_data);
188 static void translate_generic_service_html_start_element_cb (gpointer user_data,
189 const xmlChar *name,
190 const xmlChar **atts);
191 static void translate_generic_service_html_end_element_cb (gpointer user_data,
192 const xmlChar *name);
193
194 static void translate_generic_service_refresh_got_body_h (SoupMessage *message,
195 gpointer user_data);
196
197 #ifdef HAVE_LIBSOUP22
198 static void translate_generic_service_redirect_handler (SoupMessage *message,
199 gpointer user_data);
200 #endif
201
202 static char *translate_generic_service_translate_text (TranslateService *service,
203 const char *text,
204 const char *from,
205 const char *to,
206 TranslateProgressFunc progress_func,
207 gpointer user_data,
208 GError **err);
209
210 char *translate_generic_service_expand (const char *warning_prefix,
211 const char *str,
212 ...);
213 static char *translate_generic_service_expand_variable (const char *warning_prefix,
214 const char *variable,
215 GHashTable *subs);
216 static char *translate_generic_service_modify_value (const char *warning_prefix,
217 const char *value,
218 const char *modifier_name,
219 const char *modifier_value);
220
221 static char *translate_generic_service_translate_web_page (TranslateService *service,
222 const char *url,
223 const char *from,
224 const char *to,
225 TranslateProgressFunc progress_func,
226 gpointer user_data,
227 GError **err);
228
229 static SoupSession *translate_generic_service_soup_session_sync_new (void);
230
231 GType
translate_generic_service_get_type(void)232 translate_generic_service_get_type (void)
233 {
234 static GType type;
235 static GOnce once = G_ONCE_INIT;
236
237 g_once(&once, (GThreadFunc) translate_generic_service_register_type, &type);
238
239 return type;
240 }
241
242 static void
translate_generic_service_register_type(GType * type)243 translate_generic_service_register_type (GType *type)
244 {
245 static const GTypeInfo info = {
246 sizeof(TranslateGenericServiceClass),
247 NULL,
248 NULL,
249 (GClassInitFunc) translate_generic_service_class_init,
250 NULL,
251 NULL,
252 sizeof(TranslateGenericService),
253 0,
254 (GInstanceInitFunc) translate_generic_service_init
255 };
256
257 *type = g_type_register_static(TRANSLATE_TYPE_SERVICE,
258 "TranslateGenericService",
259 &info,
260 0);
261 }
262
263 static void
translate_generic_service_class_init(TranslateGenericServiceClass * class)264 translate_generic_service_class_init (TranslateGenericServiceClass *class)
265 {
266 GObjectClass *object_class = G_OBJECT_CLASS(class);
267 TranslateServiceClass *service_class = TRANSLATE_SERVICE_CLASS(class);
268
269 g_type_class_add_private(class, sizeof(TranslateGenericServicePrivate));
270 parent_class = g_type_class_peek_parent(class);
271
272 object_class->finalize = translate_generic_service_finalize;
273 object_class->set_property = translate_generic_service_set_property;
274
275 service_class->get_pairs = translate_generic_service_get_pairs;
276 service_class->translate_text = translate_generic_service_translate_text;
277 service_class->translate_web_page = translate_generic_service_translate_web_page;
278
279 g_object_class_install_property(object_class,
280 PROP_GROUPS,
281 g_param_spec_pointer("groups",
282 NULL,
283 NULL,
284 G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
285 }
286
287 static void
translate_generic_service_init(TranslateGenericService * service)288 translate_generic_service_init (TranslateGenericService *service)
289 {
290 service->priv = G_TYPE_INSTANCE_GET_PRIVATE(service,
291 TRANSLATE_GENERIC_TYPE_SERVICE,
292 TranslateGenericServicePrivate);
293 }
294
295 static void
translate_generic_service_finalize(GObject * object)296 translate_generic_service_finalize (GObject *object)
297 {
298 TranslateGenericService *service = TRANSLATE_GENERIC_SERVICE(object);
299
300 g_slist_foreach(service->priv->groups, (GFunc) translate_generic_group_unref, NULL);
301 g_slist_free(service->priv->groups);
302
303 parent_class->finalize(object);
304 }
305
306 static void
translate_generic_service_set_property(GObject * object,unsigned int prop_id,const GValue * value,GParamSpec * pspec)307 translate_generic_service_set_property (GObject *object,
308 unsigned int prop_id,
309 const GValue *value,
310 GParamSpec *pspec)
311 {
312 TranslateGenericService *service = TRANSLATE_GENERIC_SERVICE(object);
313
314 switch (prop_id)
315 {
316 case PROP_GROUPS:
317 service->priv->groups = g_slist_copy(g_value_get_pointer(value));
318 g_slist_foreach(service->priv->groups, (GFunc) translate_generic_group_ref, NULL);
319 break;
320
321 default:
322 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
323 break;
324 }
325 }
326
327 static gboolean
translate_generic_service_get_pairs(TranslateService * service,GSList ** pairs,TranslateProgressFunc progress_func,gpointer user_data,GError ** err)328 translate_generic_service_get_pairs (TranslateService *service,
329 GSList **pairs,
330 TranslateProgressFunc progress_func,
331 gpointer user_data,
332 GError **err)
333 {
334 TranslateGenericService *generic = TRANSLATE_GENERIC_SERVICE(service);
335 GetPairsInfo info;
336 GSList *l;
337
338 info.pairs = pairs;
339 *info.pairs = NULL;
340
341 for (l = generic->priv->groups; l != NULL; l = l ->next)
342 {
343 TranslateGenericGroup *group = l->data;
344
345 info.flags = 0;
346 if (group->text_location)
347 info.flags |= TRANSLATE_PAIR_TEXT;
348 if (group->web_page_location)
349 info.flags |= TRANSLATE_PAIR_WEB_PAGE;
350
351 translate_generic_group_foreach_pair(group, translate_generic_service_get_pairs_cb, &info);
352 }
353
354 return TRUE;
355 }
356
357 static gboolean
translate_generic_service_get_pairs_cb(const char * from,const char * to,gpointer user_data)358 translate_generic_service_get_pairs_cb (const char *from,
359 const char *to,
360 gpointer user_data)
361 {
362 GetPairsInfo *info = user_data;
363
364 *info->pairs = g_slist_append(*info->pairs, translate_pair_new(info->flags, from, to));
365
366 return TRUE; /* continue */
367 }
368
369 static TranslateGenericGroup *
translate_generic_service_get_group(TranslateGenericService * service,const char * from,const char * to,int * pos)370 translate_generic_service_get_group (TranslateGenericService *service,
371 const char *from,
372 const char *to,
373 int *pos)
374 {
375 GSList *l;
376 int _pos;
377 GetGroupInfo info = { FALSE, from, to };
378
379 g_return_val_if_fail(TRANSLATE_GENERIC_IS_SERVICE(service), NULL);
380 g_return_val_if_fail(from != NULL, NULL);
381 g_return_val_if_fail(to != NULL, NULL);
382 g_return_val_if_fail(pos != NULL, NULL);
383
384 for (l = service->priv->groups, _pos = 1; l != NULL; l = l ->next, _pos++)
385 {
386 TranslateGenericGroup *group = l->data;
387
388 translate_generic_group_foreach_pair(group, translate_generic_service_get_group_cb, &info);
389 if (info.found)
390 {
391 *pos = _pos;
392 return group;
393 }
394 }
395
396 *pos = -1;
397 return NULL;
398 }
399
400 static gboolean
translate_generic_service_get_group_cb(const char * from,const char * to,gpointer user_data)401 translate_generic_service_get_group_cb (const char *from,
402 const char *to,
403 gpointer user_data)
404 {
405 GetGroupInfo *info = user_data;
406
407 if (! g_ascii_strcasecmp(from, info->from) && ! g_ascii_strcasecmp(to, info->to))
408 {
409 info->found = TRUE;
410 return FALSE; /* abort */
411 }
412 else
413 return TRUE; /* continue */
414 }
415
416 static char *
translate_generic_service_get(const char * uri,const char * post,const char * post_content_type,const GSList * headers,TransferFlags flags,TranslateProgressFunc progress_func,gpointer user_data,GError ** err)417 translate_generic_service_get (const char *uri,
418 const char *post,
419 const char *post_content_type,
420 const GSList *headers,
421 TransferFlags flags,
422 TranslateProgressFunc progress_func,
423 gpointer user_data,
424 GError **err)
425 {
426 TransferInfo info;
427 SoupMessage *message;
428 const GSList *l;
429 char *response = NULL;
430
431 g_return_val_if_fail(uri != NULL, FALSE);
432
433 message = soup_message_new(post ? SOUP_METHOD_POST : SOUP_METHOD_GET, uri);
434 if (! message)
435 {
436 g_set_error(err,
437 TRANSLATE_GENERIC_SERVICE_ERROR,
438 TRANSLATE_GENERIC_SERVICE_ERROR_TRANSFER,
439 _("unable to parse URI \"%s\""), uri);
440 return NULL;
441 }
442
443 if (post)
444 {
445 g_return_val_if_fail(post_content_type != NULL, NULL);
446 soup_message_set_request(message,
447 post_content_type,
448 #ifdef HAVE_LIBSOUP22
449 SOUP_BUFFER_USER_OWNED,
450 #else
451 SOUP_MEMORY_TEMPORARY,
452 #endif
453 (char *) post,
454 strlen(post));
455 }
456
457 for (l = headers; l != NULL; l = l->next)
458 {
459 TranslateGenericHttpHeader *header = l->data;
460 soup_message_headers_append(message->request_headers, header->name, header->value);
461 }
462
463 info.session = translate_generic_service_soup_session_sync_new();
464 info.parse_html = FALSE;
465 info.html_http_equiv = NULL;
466
467 if (translate_generic_debug_flags & TRANSLATE_GENERIC_DEBUG_LOG_TRANSFERS)
468 {
469 #ifdef HAVE_LIBSOUP22
470 g_object_connect(message,
471 "signal::wrote-headers", translate_generic_service_log_wrote_headers_h, &info,
472 "signal::wrote-body", translate_generic_service_log_wrote_body_h, &info,
473 "signal::got-headers", translate_generic_service_log_got_headers_h, &info,
474 "signal::got-body", translate_generic_service_log_got_body_h, &info,
475 NULL);
476 #else
477 SoupLogger *logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, -1);
478 soup_logger_set_printer (logger, translate_generic_service_log_printer, NULL, NULL);
479 soup_logger_attach (logger, info.session);
480 g_object_unref (logger);
481 #endif
482 }
483
484 if (progress_func)
485 {
486 info.progress_func = progress_func;
487 info.user_data = user_data;
488 info.length = -1;
489 info.received = 0;
490
491 g_object_connect(message,
492 "signal::got-headers", translate_generic_service_progress_got_headers_h, &info,
493 "signal::got-chunk", translate_generic_service_progress_got_chunk_h, &info,
494 NULL);
495 }
496
497 /*
498 * We parse the HTML to retrieve the http-equiv meta tags, and we
499 * only need them if we convert or follow refresh.
500 */
501 if ((flags & TRANSFER_FOLLOW_REFRESH) || (flags & TRANSFER_CONVERT))
502 g_object_connect(message,
503 "signal::got-headers", translate_generic_service_html_got_headers_h, &info,
504 "signal::got-body", translate_generic_service_html_got_body_h, &info,
505 NULL);
506
507 if (flags & TRANSFER_FOLLOW_REFRESH)
508 g_signal_connect(message, "got-body", G_CALLBACK(translate_generic_service_refresh_got_body_h), &info);
509
510 #ifdef HAVE_LIBSOUP22
511 /* http://bugzilla.ximian.com/show_bug.cgi?id=70688 */
512 soup_message_set_flags(message, SOUP_MESSAGE_NO_REDIRECT);
513 soup_message_add_status_class_handler(message,
514 SOUP_STATUS_CLASS_REDIRECT,
515 SOUP_HANDLER_POST_BODY,
516 translate_generic_service_redirect_handler,
517 info.session);
518 #endif
519
520 if (translate_generic_debug_flags & TRANSLATE_GENERIC_DEBUG_LOG_TRANSFERS)
521 translate_generic_service_log_connect(message);
522
523 soup_session_send_message(info.session, message);
524 g_object_unref(info.session);
525
526 if (SOUP_STATUS_IS_SUCCESSFUL(message->status_code))
527 {
528 char *charset = NULL;
529
530 if (flags & TRANSFER_CONVERT)
531 {
532 const char *content_type;
533
534 content_type = translate_generic_service_get_header(message, &info, "Content-Type");
535 if (content_type)
536 {
537 const char *tmp;
538
539 tmp = translate_ascii_strcasestr(content_type, "charset=");
540 if (tmp)
541 {
542 int len;
543
544 tmp += 8;
545 if (*tmp == '\'' || *tmp == '"')
546 tmp++;
547
548 len = strlen(tmp);
549 if (len > 0 && (tmp[len - 1] == '\'' || tmp[len - 1] == '"'))
550 len--;
551
552 charset = g_strndup(tmp, len);
553 }
554 }
555 }
556
557 if (charset)
558 {
559 response = g_convert(trans_SOUP_MESSAGE_RESPONSE_BODY (message), trans_SOUP_MESSAGE_RESPONSE_LENGTH (message), "UTF-8", charset, NULL, NULL, err);
560 g_free(charset);
561 }
562 else
563 {
564 if ((flags & TRANSFER_CONVERT) && ! g_utf8_validate(trans_SOUP_MESSAGE_RESPONSE_BODY (message), trans_SOUP_MESSAGE_RESPONSE_LENGTH (message), NULL))
565 g_set_error(err,
566 TRANSLATE_GENERIC_SERVICE_ERROR,
567 TRANSLATE_GENERIC_SERVICE_ERROR_TRANSFER,
568 _("invalid UTF-8"));
569 else
570 response = g_strndup(trans_SOUP_MESSAGE_RESPONSE_BODY (message), trans_SOUP_MESSAGE_RESPONSE_LENGTH (message));
571 }
572 }
573 else
574 {
575 if (message->status_code == SOUP_STATUS_CANCELLED)
576 g_set_error(err,
577 TRANSLATE_ERROR,
578 TRANSLATE_ERROR_CANCELLED,
579 "%s", message->reason_phrase);
580 else
581 g_set_error(err,
582 TRANSLATE_GENERIC_SERVICE_ERROR,
583 TRANSLATE_GENERIC_SERVICE_ERROR_TRANSFER,
584 "%s", message->reason_phrase);
585 }
586
587 if (info.html_http_equiv)
588 g_hash_table_destroy(info.html_http_equiv);
589
590 g_object_unref(message);
591
592 return response;
593 }
594
595 static const char *
translate_generic_service_get_header(SoupMessage * message,TransferInfo * info,const char * name)596 translate_generic_service_get_header (SoupMessage *message,
597 TransferInfo *info,
598 const char *name)
599 {
600 const char *value;
601
602 g_return_val_if_fail(SOUP_IS_MESSAGE(message), NULL);
603 g_return_val_if_fail(info != NULL, NULL);
604 g_return_val_if_fail(name != NULL, NULL);
605
606 value = info->html_http_equiv
607 ? g_hash_table_lookup(info->html_http_equiv, name)
608 : NULL;
609
610 if (! value)
611 value = soup_message_headers_get(message->response_headers, name);
612
613 return value;
614 }
615
616 static void
translate_generic_service_log_connect(SoupMessage * message)617 translate_generic_service_log_connect (SoupMessage *message)
618 {
619 const SoupURI *uri;
620
621 uri = soup_message_get_uri(message);
622 g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, _("connecting to %s:%i"), uri->host, uri->port);
623 }
624
625 #ifdef HAVE_LIBSOUP22
626
627 static void
translate_generic_service_log_wrote_headers_h(SoupMessage * message,gpointer user_data)628 translate_generic_service_log_wrote_headers_h (SoupMessage *message,
629 gpointer user_data)
630 {
631 const SoupUri *suri;
632 char *uri;
633
634 suri = soup_message_get_uri(message);
635 uri = soup_uri_to_string(suri, FALSE);
636
637 g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "> %s %s", message->method, uri);
638 g_free(uri);
639
640 soup_message_foreach_header(message->request_headers, (GHFunc) translate_generic_service_log_headers_cb, ">");
641 }
642
643 static void
translate_generic_service_log_wrote_body_h(SoupMessage * message,gpointer user_data)644 translate_generic_service_log_wrote_body_h (SoupMessage *message,
645 gpointer user_data)
646 {
647 if (message->request.body)
648 g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "> %s", message->request.body);
649 }
650
651 static void
translate_generic_service_log_got_headers_h(SoupMessage * message,gpointer user_data)652 translate_generic_service_log_got_headers_h (SoupMessage *message,
653 gpointer user_data)
654 {
655 g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "< %s", soup_status_get_phrase(message->status_code));
656
657 soup_message_foreach_header(message->response_headers, (GHFunc) translate_generic_service_log_headers_cb, "<");
658
659 if (message->response.body)
660 g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "< %s", message->response.body);
661 }
662
663 static void
translate_generic_service_log_got_body_h(SoupMessage * message,gpointer user_data)664 translate_generic_service_log_got_body_h (SoupMessage *message,
665 gpointer user_data)
666 {
667 if (message->response.body)
668 g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "< %s", message->response.body);
669 }
670
671 static void
translate_generic_service_log_headers_cb(const char * key,const char * value,gpointer user_data)672 translate_generic_service_log_headers_cb (const char *key,
673 const char *value,
674 gpointer user_data)
675 {
676 const char *prefix = user_data;
677
678 g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "%s %s: %s", prefix, key, value);
679 }
680
681 #else /* !HAVE_LIBSOUP22 */
682
683 static void
translate_generic_service_log_printer(SoupLogger * logger,SoupLoggerLogLevel level,char direction,const char * data,gpointer user_data)684 translate_generic_service_log_printer (SoupLogger *logger,
685 SoupLoggerLogLevel level,
686 char direction,
687 const char *data,
688 gpointer user_data)
689 {
690 g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, "%c %s", direction, data);
691 }
692
693 #endif /* HAVE_LIBSOUP22 */
694
695 static void
translate_generic_service_progress_got_headers_h(SoupMessage * message,gpointer user_data)696 translate_generic_service_progress_got_headers_h (SoupMessage *message,
697 gpointer user_data)
698 {
699 TransferInfo *info = user_data;
700 const char *content_length;
701
702 content_length = soup_message_headers_get(message->response_headers, "Content-Length");
703 info->length = (content_length
704 && *content_length
705 && strspn(content_length, "0123456789") == strlen(content_length))
706 ? atoi(content_length) : -1;
707 info->received = 0;
708 }
709
710 static void
translate_generic_service_progress_got_chunk_h(SoupMessage * message,SoupBuffer * chunk,gpointer user_data)711 translate_generic_service_progress_got_chunk_h (SoupMessage *message,
712 #ifdef HAVE_LIBSOUP24
713 SoupBuffer *chunk,
714 #endif
715 gpointer user_data)
716 {
717 TransferInfo *info = user_data;
718 double progress;
719
720 if (info->length == -1)
721 progress = -1;
722 else
723 {
724 #ifdef HAVE_LIBSOUP22
725 info->received += message->response.length;
726 #else
727 info->received += chunk->length;
728 #endif
729 progress = (double) info->received / info->length;
730 progress = CLAMP(progress, 0.0, 1.0);
731 }
732
733 if (! info->progress_func(progress, info->user_data))
734 soup_session_abort(info->session);
735 }
736
737 static void
translate_generic_service_html_got_headers_h(SoupMessage * message,gpointer user_data)738 translate_generic_service_html_got_headers_h (SoupMessage *message,
739 gpointer user_data)
740 {
741 TransferInfo *info = user_data;
742 const char *content_type;
743
744 content_type = soup_message_headers_get(message->response_headers, "Content-Type");
745 info->parse_html = content_type
746 && (g_str_has_prefix(content_type, "text/html")
747 || g_str_has_prefix(content_type, "application/xhtml+xml")
748 || g_str_has_prefix(content_type, "application/xml")
749 || g_str_has_prefix(content_type, "text/xml"));
750 }
751
752 static void
translate_generic_service_html_got_body_h(SoupMessage * message,gpointer user_data)753 translate_generic_service_html_got_body_h (SoupMessage *message,
754 gpointer user_data)
755 {
756 TransferInfo *info = user_data;
757
758 if (info->html_http_equiv)
759 {
760 g_hash_table_destroy(info->html_http_equiv);
761 info->html_http_equiv = NULL;
762 }
763
764 if (info->parse_html && trans_SOUP_MESSAGE_RESPONSE_LENGTH (message) > 0)
765 {
766 char *body;
767 xmlSAXHandler sax_handler = { NULL };
768
769 info->html_context = HTML_CONTEXT_PRE_HEAD;
770 info->html_http_equiv = g_hash_table_new_full(translate_ascii_strcase_hash,
771 translate_ascii_strcase_equal,
772 g_free,
773 g_free);
774
775 sax_handler.startElement = translate_generic_service_html_start_element_cb;
776 sax_handler.endElement = translate_generic_service_html_end_element_cb;
777
778 body = g_strndup(trans_SOUP_MESSAGE_RESPONSE_BODY (message), trans_SOUP_MESSAGE_RESPONSE_LENGTH (message));
779 htmlSAXParseDoc(body, NULL, &sax_handler, user_data);
780 g_free(body);
781 }
782 }
783
784 static void
translate_generic_service_html_start_element_cb(gpointer user_data,const xmlChar * name,const xmlChar ** atts)785 translate_generic_service_html_start_element_cb (gpointer user_data,
786 const xmlChar *name,
787 const xmlChar **atts)
788 {
789 TransferInfo *info = user_data;
790
791 if (info->html_context == HTML_CONTEXT_PRE_HEAD)
792 {
793 if (! g_ascii_strcasecmp(name, "head"))
794 info->html_context = HTML_CONTEXT_HEAD;
795 }
796 else if (info->html_context == HTML_CONTEXT_HEAD)
797 {
798 if (! g_ascii_strcasecmp(name, "meta"))
799 {
800 int i;
801 const char *http_equiv = NULL;
802
803 for (i = 0; atts[i] && atts[i + 1]; i += 2)
804 if (! g_ascii_strcasecmp(atts[i], "http-equiv"))
805 {
806 http_equiv = atts[i + 1];
807 break;
808 }
809
810 if (http_equiv)
811 {
812 const char *content = NULL;
813
814 for (i = 0; atts[i] && atts[i + 1]; i += 2)
815 if (! g_ascii_strcasecmp(atts[i], "content"))
816 {
817 content = atts[i + 1];
818 break;
819 }
820
821 if (content)
822 g_hash_table_insert(info->html_http_equiv, g_strdup(http_equiv), g_strdup(content));
823 }
824 }
825 }
826 }
827
828 static void
translate_generic_service_html_end_element_cb(gpointer user_data,const xmlChar * name)829 translate_generic_service_html_end_element_cb (gpointer user_data,
830 const xmlChar *name)
831 {
832 TransferInfo *info = user_data;
833
834 if (info->html_context == HTML_CONTEXT_HEAD
835 && ! g_ascii_strcasecmp(name, "head"))
836 info->html_context = HTML_CONTEXT_POST_HEAD;
837 }
838
839 static void
translate_generic_service_refresh_got_body_h(SoupMessage * message,gpointer user_data)840 translate_generic_service_refresh_got_body_h (SoupMessage *message,
841 gpointer user_data)
842 {
843 TransferInfo *info = user_data;
844 const char *refresh_uri;
845 SoupURI *new_uri = NULL;
846
847 refresh_uri = translate_generic_service_get_header(message, info, "Refresh");
848 if (refresh_uri)
849 {
850 refresh_uri = translate_ascii_strcasestr(refresh_uri, "url=");
851 if (refresh_uri)
852 refresh_uri += 4;
853 }
854
855 if (refresh_uri)
856 {
857 new_uri = soup_uri_new(refresh_uri);
858 if (! new_uri)
859 {
860 SoupURI *base_uri;
861
862 base_uri = (SoupURI *)soup_message_get_uri(message);
863 new_uri = soup_uri_new_with_base(base_uri, refresh_uri);
864 }
865 }
866
867 if (new_uri)
868 {
869 soup_message_set_uri(message, new_uri);
870 soup_uri_free(new_uri);
871
872 if (translate_generic_debug_flags & TRANSLATE_GENERIC_DEBUG_LOG_TRANSFERS)
873 translate_generic_service_log_connect(message);
874
875 soup_session_requeue_message(info->session, message);
876 }
877 }
878
879 #ifdef HAVE_LIBSOUP22
880 static void
translate_generic_service_redirect_handler(SoupMessage * message,gpointer user_data)881 translate_generic_service_redirect_handler (SoupMessage *message,
882 gpointer user_data)
883 {
884 const char *new_location;
885
886 new_location = soup_message_get_header(message->response_headers, "Location");
887 if (new_location)
888 {
889 SoupSession *session = user_data;
890 SoupUri *new_uri;
891
892 new_uri = soup_uri_new(new_location);
893 if (! new_uri)
894 {
895 const SoupUri *base_uri;
896
897 base_uri = soup_message_get_uri(message);
898 new_uri = soup_uri_new_with_base(base_uri, new_location);
899
900 if (! new_uri)
901 {
902 soup_message_set_status_full(message, SOUP_STATUS_MALFORMED, _("invalid redirect URL"));
903 return;
904 }
905 }
906
907 soup_message_set_uri(message, new_uri);
908 soup_uri_free(new_uri);
909
910 if (translate_generic_debug_flags & TRANSLATE_GENERIC_DEBUG_LOG_TRANSFERS)
911 translate_generic_service_log_connect(message);
912
913 soup_session_requeue_message(session, message);
914 }
915 }
916 #endif
917
918 static char *
translate_generic_service_translate_text(TranslateService * service,const char * text,const char * from,const char * to,TranslateProgressFunc progress_func,gpointer user_data,GError ** err)919 translate_generic_service_translate_text (TranslateService *service,
920 const char *text,
921 const char *from,
922 const char *to,
923 TranslateProgressFunc progress_func,
924 gpointer user_data,
925 GError **err)
926 {
927 TranslateGenericService *generic = TRANSLATE_GENERIC_SERVICE(service);
928 TranslateGenericGroup *group;
929 int group_pos;
930 const char *service_from;
931 const char *service_to;
932 char *warning_prefix;
933 char *url;
934 char *post = NULL;
935 GSList *headers;
936 char *response;
937 GString *answer = NULL;
938
939 group = translate_generic_service_get_group(generic, from, to, &group_pos);
940 g_return_val_if_fail(group != NULL, NULL);
941
942 service_from = translate_generic_group_get_service_tag(group, from);
943 service_to = translate_generic_group_get_service_tag(group, to);
944
945 warning_prefix = MAKE_WARNING_PREFIX(service, group_pos, "url", "text-translation");
946 url = translate_generic_service_expand(warning_prefix,
947 group->text_location->url,
948 "text", text,
949 "from", service_from,
950 "to", service_to,
951 NULL);
952 g_free(warning_prefix);
953
954 if (group->text_location->post)
955 {
956 warning_prefix = MAKE_WARNING_PREFIX(service, group_pos, "post", "text-translation");
957 post = translate_generic_service_expand(warning_prefix,
958 group->text_location->post,
959 "text", text,
960 "from", service_from,
961 "to", service_to,
962 NULL);
963 g_free(warning_prefix);
964 }
965
966 headers = g_slist_copy(group->http_headers);
967 headers = g_slist_concat(headers, g_slist_copy(group->text_location->http_headers));
968
969 response = translate_generic_service_get(url,
970 post,
971 group->text_location->content_type,
972 headers,
973 TRANSFER_FOLLOW_REFRESH | TRANSFER_CONVERT,
974 progress_func,
975 user_data,
976 err);
977
978 g_free(url);
979 g_free(post);
980 g_slist_free(headers);
981
982 if (response)
983 {
984 const char *work = response;
985 char *error = NULL;
986 GSList *l;
987
988 for (l = group->text_error_markers; l != NULL && ! error; l = l->next)
989 {
990 const char *marker = l->data;
991
992 error = strstr(work, marker);
993 }
994
995 if (error)
996 g_set_error(err,
997 TRANSLATE_GENERIC_SERVICE_ERROR,
998 TRANSLATE_GENERIC_SERVICE_ERROR_FAILED,
999 _("server failure"));
1000 else
1001 {
1002 char *raw = NULL;
1003
1004 for (l = group->text_pre_markers; l != NULL && work; l = l->next)
1005 {
1006 const char *marker = l->data;
1007
1008 work = strstr(work, marker);
1009 if (work)
1010 work += strlen(marker);
1011 }
1012
1013 if (work)
1014 {
1015 if (group->text_post_marker)
1016 {
1017 char *s;
1018
1019 s = strstr(work, group->text_post_marker);
1020 if (s)
1021 raw = g_strndup(work, s - work);
1022 }
1023 else
1024 raw = g_strdup(work);
1025 }
1026
1027 if (raw)
1028 {
1029 char *expanded;
1030 const char *s;
1031 int len;
1032
1033 expanded = translate_sgml_ref_expand(raw);
1034 g_free(raw);
1035
1036 /*
1037 * If the service has removed the leading and/or
1038 * trailing whitespace, restore it.
1039 */
1040
1041 answer = g_string_new(NULL);
1042
1043 for (s = text;
1044 *s && g_unichar_isspace(g_utf8_get_char(s));
1045 s = g_utf8_next_char(s));
1046
1047 len = s - text;
1048 if (len > 0 && strncmp(expanded, text, len))
1049 g_string_append_len(answer, text, len);
1050
1051 g_string_append(answer, expanded);
1052
1053 if (*s)
1054 { /* there was a middle block, handle trailing spaces */
1055 for (s = g_utf8_find_prev_char(text, strchr(text, 0));
1056 s && g_unichar_isspace(g_utf8_get_char(s));
1057 s = g_utf8_find_prev_char(text, s));
1058 s = s ? g_utf8_next_char(s) : text;
1059
1060 if (! g_str_has_suffix(expanded, s))
1061 g_string_append(answer, s);
1062 }
1063 }
1064 else
1065 g_set_error(err,
1066 TRANSLATE_GENERIC_SERVICE_ERROR,
1067 TRANSLATE_GENERIC_SERVICE_ERROR_PARSE,
1068 _("unable to parse server data"));
1069 }
1070
1071 g_free(response);
1072 }
1073
1074 return answer ? g_string_free(answer, FALSE) : NULL;
1075 }
1076
1077 char *
translate_generic_service_expand(const char * warning_prefix,const char * str,...)1078 translate_generic_service_expand (const char *warning_prefix,
1079 const char *str,
1080 ...)
1081 {
1082 GHashTable *subs;
1083 va_list args;
1084 const char *name;
1085 GString *result;
1086 int i;
1087 int dollar = -1;
1088
1089 g_return_val_if_fail(warning_prefix != NULL, NULL);
1090 g_return_val_if_fail(str != NULL, NULL);
1091
1092 va_start(args, str);
1093 subs = g_hash_table_new(g_str_hash, g_str_equal);
1094
1095 while ((name = va_arg(args, const char *)))
1096 {
1097 const char *value;
1098
1099 value = va_arg(args, const char *);
1100 g_return_val_if_fail(value != NULL, NULL);
1101
1102 g_hash_table_insert(subs, (gpointer) name, (gpointer) value);
1103 }
1104
1105 result = g_string_new(NULL);
1106
1107 for (i = 0; str[i]; i++)
1108 if (dollar >= 0)
1109 {
1110 if (dollar == i - 1)
1111 {
1112 if (str[i] == '$')
1113 {
1114 g_string_append_c(result, '$');
1115 dollar = -1;
1116 }
1117 else if (str[i] != '{')
1118 {
1119 g_string_append_len(result, str + (i - 1), 2);
1120 dollar = -1;
1121 }
1122 }
1123 else
1124 {
1125 if (str[i] == '}')
1126 {
1127 int start;
1128 char *variable;
1129 char *expanded;
1130
1131 start = dollar + 2;
1132
1133 variable = g_strndup(str + start, i - start);
1134 expanded = translate_generic_service_expand_variable(warning_prefix,
1135 variable,
1136 subs);
1137 g_free(variable);
1138
1139 if (expanded)
1140 {
1141 g_string_append(result, expanded);
1142 g_free(expanded);
1143 }
1144
1145 dollar = -1;
1146 }
1147 }
1148 }
1149 else
1150 {
1151 if (str[i] == '$')
1152 dollar = i;
1153 else
1154 g_string_append_c(result, str[i]);
1155 }
1156
1157 g_hash_table_destroy(subs);
1158 va_end(args);
1159
1160 return g_string_free(result, FALSE);
1161 }
1162
1163 static char *
translate_generic_service_expand_variable(const char * warning_prefix,const char * variable,GHashTable * subs)1164 translate_generic_service_expand_variable (const char *warning_prefix,
1165 const char *variable,
1166 GHashTable *subs)
1167 {
1168 char *s;
1169 char *varname;
1170 char **modifiers = NULL;
1171 char *expanded = NULL;
1172
1173 g_return_val_if_fail(warning_prefix != NULL, NULL);
1174 g_return_val_if_fail(variable != NULL, NULL);
1175 g_return_val_if_fail(subs != NULL, NULL);
1176
1177 s = strchr(variable, ':');
1178 if (s)
1179 {
1180 varname = g_strndup(variable, s - variable);
1181 modifiers = g_strsplit(s + 1, ",", 0);
1182 }
1183 else
1184 varname = g_strdup(variable);
1185
1186 if (! strcmp(varname, "time"))
1187 {
1188 time_t now;
1189
1190 now = translate_time();
1191 expanded = g_strdup_printf("%ul", now);
1192 }
1193 else
1194 expanded = g_strdup(g_hash_table_lookup(subs, varname));
1195
1196 if (expanded)
1197 {
1198 if (modifiers)
1199 {
1200 int i;
1201
1202 for (i = 0; modifiers[i]; i++)
1203 {
1204 char *mod_name;
1205 char *mod_value;
1206 char *modified;
1207
1208 s = strchr(modifiers[i], '=');
1209 if (s)
1210 {
1211 mod_name = g_strndup(modifiers[i], s - modifiers[i]);
1212 mod_value = g_strdup(s + 1);
1213 }
1214 else
1215 {
1216 mod_name = g_strdup(modifiers[i]);
1217 mod_value = NULL;
1218 }
1219
1220 modified = translate_generic_service_modify_value(warning_prefix,
1221 expanded,
1222 mod_name,
1223 mod_value);
1224 g_free(mod_name);
1225 g_free(mod_value);
1226
1227 g_free(expanded);
1228 expanded = modified;
1229 }
1230 }
1231 }
1232 else
1233 g_warning(_("%s: unknown variable \"%s\""), warning_prefix, varname);
1234
1235 g_free(varname);
1236 g_strfreev(modifiers);
1237
1238 return expanded;
1239 }
1240
1241 static char *
translate_generic_service_modify_value(const char * warning_prefix,const char * value,const char * modifier_name,const char * modifier_value)1242 translate_generic_service_modify_value (const char *warning_prefix,
1243 const char *value,
1244 const char *modifier_name,
1245 const char *modifier_value)
1246 {
1247 char *modified = NULL;
1248
1249 g_return_val_if_fail(warning_prefix != NULL, NULL);
1250 g_return_val_if_fail(value != NULL, NULL);
1251 g_return_val_if_fail(modifier_name != NULL, NULL);
1252
1253 if (! strcmp(modifier_name, "escape"))
1254 {
1255 if (modifier_value)
1256 g_warning(_("%s: value specified for \"escape\" modifier"), warning_prefix);
1257 modified = soup_uri_encode(value, NULL);
1258 }
1259 else if (! strcmp(modifier_name, "charset"))
1260 {
1261 if (modifier_value)
1262 {
1263 GError *err = NULL;
1264
1265 modified = g_convert(value, -1, modifier_value, "UTF-8", NULL, NULL, &err);
1266 if (! modified)
1267 {
1268 g_warning(_("%s: unable to convert to \"%s\": %s"), warning_prefix, modifier_value, err->message);
1269 g_error_free(err);
1270 }
1271 }
1272 else
1273 g_warning(_("%s: value of \"charset\" modifier missing"), warning_prefix);
1274 }
1275 else
1276 g_warning(_("%s: unknown modifier \"%s\""), warning_prefix, modifier_name);
1277
1278 return modified ? modified : g_strdup(value);
1279 }
1280
1281 static char *
translate_generic_service_translate_web_page(TranslateService * service,const char * url,const char * from,const char * to,TranslateProgressFunc progress_func,gpointer user_data,GError ** err)1282 translate_generic_service_translate_web_page (TranslateService *service,
1283 const char *url,
1284 const char *from,
1285 const char *to,
1286 TranslateProgressFunc progress_func,
1287 gpointer user_data,
1288 GError **err)
1289 {
1290 TranslateGenericService *generic = TRANSLATE_GENERIC_SERVICE(service);
1291 TranslateGenericGroup *group;
1292 int group_pos;
1293 const char *service_from;
1294 const char *service_to;
1295 char *warning_prefix;
1296 char *translation_url;
1297 char *post = NULL;
1298 GSList *headers;
1299 char *response;
1300
1301 group = translate_generic_service_get_group(generic, from, to, &group_pos);
1302 g_return_val_if_fail(group != NULL, NULL);
1303
1304 service_from = translate_generic_group_get_service_tag(group, from);
1305 service_to = translate_generic_group_get_service_tag(group, to);
1306
1307 warning_prefix = MAKE_WARNING_PREFIX(service, group_pos, "url", "web-page-translation");
1308 translation_url = translate_generic_service_expand(warning_prefix,
1309 group->web_page_location->url,
1310 "url", url,
1311 "from", service_from,
1312 "to", service_to,
1313 NULL);
1314 g_free(warning_prefix);
1315
1316 headers = g_slist_copy(group->http_headers);
1317 headers = g_slist_concat(headers, g_slist_copy(group->web_page_location->http_headers));
1318
1319 if (group->web_page_location->post)
1320 {
1321 warning_prefix = MAKE_WARNING_PREFIX(service, group_pos, "post", "web-page-translation");
1322 post = translate_generic_service_expand(warning_prefix,
1323 group->web_page_location->post,
1324 "url", url,
1325 "from", service_from,
1326 "to", service_to,
1327 NULL);
1328 g_free(warning_prefix);
1329 }
1330 else if (! headers)
1331 return translation_url;
1332
1333 response = translate_generic_service_get(translation_url,
1334 post,
1335 group->web_page_location->content_type,
1336 headers,
1337 0,
1338 progress_func,
1339 user_data,
1340 err);
1341
1342 g_free(translation_url);
1343 translation_url = NULL;
1344
1345 g_free(post);
1346 g_slist_free(headers);
1347
1348 if (response)
1349 {
1350 int fd;
1351 char *filename;
1352
1353 fd = g_file_open_tmp("libtranslate.XXXXXX", &filename, err);
1354 if (fd >= 0)
1355 {
1356 GIOChannel *channel;
1357
1358 channel = g_io_channel_unix_new(fd);
1359
1360 if (g_io_channel_set_encoding(channel, NULL, err)
1361 && g_io_channel_write_chars(channel, response, -1, NULL, err))
1362 {
1363 if (g_io_channel_shutdown(channel, TRUE, err))
1364 translation_url = g_strconcat("file://", filename, NULL);
1365 }
1366 else
1367 g_io_channel_shutdown(channel, FALSE, NULL);
1368
1369 g_io_channel_unref(channel);
1370 g_free(filename);
1371 }
1372
1373 g_free(response);
1374 }
1375
1376 return translation_url;
1377 }
1378
1379 static SoupSession *
translate_generic_service_soup_session_sync_new(void)1380 translate_generic_service_soup_session_sync_new (void)
1381 {
1382 char *proxy_text_uri;
1383 SoupURI *proxy_uri = NULL;
1384 SoupSession *session;
1385 TranslateGenericSoupCookieJar *cookie_jar;
1386
1387 proxy_text_uri = translate_get_proxy();
1388 if (proxy_text_uri)
1389 {
1390 proxy_uri = soup_uri_new(proxy_text_uri);
1391 if (! proxy_uri)
1392 g_warning(_("unable to parse proxy URI \"%s\""), proxy_text_uri);
1393
1394 g_free(proxy_text_uri);
1395 }
1396
1397 session = soup_session_sync_new_with_options(SOUP_SESSION_PROXY_URI, proxy_uri, NULL);
1398
1399 if (proxy_uri)
1400 soup_uri_free(proxy_uri);
1401
1402 cookie_jar = translate_generic_soup_cookie_jar_new();
1403 translate_generic_soup_cookie_jar_attach(cookie_jar, session);
1404 g_object_unref(cookie_jar);
1405
1406 return session;
1407 }
1408
1409 TranslateService *
translate_generic_service_new(const char * name,const char * nick,unsigned int max_chunk_len,const GSList * groups)1410 translate_generic_service_new (const char *name,
1411 const char *nick,
1412 unsigned int max_chunk_len,
1413 const GSList *groups)
1414 {
1415 g_return_val_if_fail(name != NULL, NULL);
1416 g_return_val_if_fail(nick != NULL, NULL);
1417
1418 return g_object_new(TRANSLATE_GENERIC_TYPE_SERVICE,
1419 "name", name,
1420 "nick", nick,
1421 "max-chunk-len", max_chunk_len,
1422 "groups", groups,
1423 NULL);
1424 }
1425
1426 GQuark
translate_generic_service_error_quark(void)1427 translate_generic_service_error_quark (void)
1428 {
1429 return g_quark_from_static_string("translate-service-generic-error");
1430 }
1431