1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * Copyright (C) 2003-2020 Shaun McCance <shaunm@gnome.org>
4 *
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License as
7 * published by the Free Software Foundation; either version 2 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public
16 * License along with this program; if not, see <http://www.gnu.org/licenses/>.
17 *
18 * Author: Shaun McCance <shaunm@gnome.org>
19 */
20
21 #ifdef HAVE_CONFIG_H
22 #include <config.h>
23 #endif
24
25 #include <glib.h>
26 #include <glib/gi18n.h>
27 #include <glib-object.h>
28 #include <libxml/parser.h>
29 #include <libxml/parserInternals.h>
30 #include <libxml/xinclude.h>
31 #include <libxml/xpathInternals.h>
32 #include <libxslt/documents.h>
33 #include <libxslt/xslt.h>
34 #include <libexslt/exslt.h>
35 #include <libxslt/templates.h>
36 #include <libxslt/transform.h>
37 #include <libxslt/extensions.h>
38 #include <libxslt/xsltInternals.h>
39 #include <libxslt/xsltutils.h>
40
41 #include "yelp-debug.h"
42 #include "yelp-error.h"
43 #include "yelp-transform.h"
44
45 #define YELP_NAMESPACE "http://www.gnome.org/yelp/ns"
46
47 static void yelp_transform_dispose (GObject *object);
48 static void yelp_transform_finalize (GObject *object);
49 static void yelp_transform_get_property (GObject *object,
50 guint prop_id,
51 GValue *value,
52 GParamSpec *pspec);
53 static void yelp_transform_set_property (GObject *object,
54 guint prop_id,
55 const GValue *value,
56 GParamSpec *pspec);
57
58 static void transform_run (YelpTransform *transform);
59
60 static gboolean transform_chunk (YelpTransform *transform);
61 static gboolean transform_error (YelpTransform *transform);
62 static gboolean transform_final (YelpTransform *transform);
63
64 static void xslt_yelp_document (xsltTransformContextPtr ctxt,
65 xmlNodePtr node,
66 xmlNodePtr inst,
67 xsltStylePreCompPtr comp);
68 static void xslt_yelp_cache (xsltTransformContextPtr ctxt,
69 xmlNodePtr node,
70 xmlNodePtr inst,
71 xsltStylePreCompPtr comp);
72 static void xslt_yelp_aux (xmlXPathParserContextPtr ctxt,
73 int nargs);
74
75 enum {
76 PROP_0,
77 PROP_STYLESHEET
78 };
79
80 enum {
81 CHUNK_READY,
82 FINISHED,
83 ERROR,
84 LAST_SIGNAL
85 };
86 static gint signals[LAST_SIGNAL] = { 0 };
87
88 typedef struct _YelpTransformPrivate YelpTransformPrivate;
89 struct _YelpTransformPrivate {
90 xmlDocPtr input;
91 xmlDocPtr output;
92 gchar *stylesheet_file;
93 xsltStylesheetPtr stylesheet;
94 xsltTransformContextPtr context;
95
96 xmlDocPtr aux;
97 xsltDocumentPtr aux_xslt;
98
99 gchar **params;
100
101 GThread *thread;
102 GMutex mutex;
103 GAsyncQueue *queue;
104 GHashTable *chunks;
105
106 gboolean running;
107 gboolean cancelled;
108
109 GError *error;
110 };
111
G_DEFINE_TYPE_WITH_PRIVATE(YelpTransform,yelp_transform,G_TYPE_OBJECT)112 G_DEFINE_TYPE_WITH_PRIVATE (YelpTransform, yelp_transform, G_TYPE_OBJECT)
113
114 /******************************************************************************/
115
116 static void
117 yelp_transform_init (YelpTransform *transform)
118 {
119 YelpTransformPrivate *priv = yelp_transform_get_instance_private (transform);
120 priv->queue = g_async_queue_new_full (g_free);
121 priv->chunks = g_hash_table_new_full (g_str_hash,
122 g_str_equal,
123 g_free,
124 NULL);
125 }
126
127 static void
yelp_transform_class_init(YelpTransformClass * klass)128 yelp_transform_class_init (YelpTransformClass *klass)
129 {
130 GObjectClass *object_class = G_OBJECT_CLASS (klass);
131
132 exsltRegisterAll ();
133
134 object_class->dispose = yelp_transform_dispose;
135 object_class->finalize = yelp_transform_finalize;
136 object_class->get_property = yelp_transform_get_property;
137 object_class->set_property = yelp_transform_set_property;
138
139 signals[CHUNK_READY] = g_signal_new ("chunk-ready",
140 G_TYPE_FROM_CLASS (klass),
141 G_SIGNAL_RUN_LAST,
142 0, NULL, NULL,
143 g_cclosure_marshal_VOID__STRING,
144 G_TYPE_NONE, 1, G_TYPE_STRING);
145 signals[FINISHED] = g_signal_new ("finished",
146 G_TYPE_FROM_CLASS (klass),
147 G_SIGNAL_RUN_LAST,
148 0, NULL, NULL,
149 g_cclosure_marshal_VOID__VOID,
150 G_TYPE_NONE, 0);
151 signals[ERROR] = g_signal_new ("error",
152 G_TYPE_FROM_CLASS (klass),
153 G_SIGNAL_RUN_LAST,
154 0, NULL, NULL,
155 g_cclosure_marshal_VOID__VOID,
156 G_TYPE_NONE, 0);
157
158 g_object_class_install_property (object_class,
159 PROP_STYLESHEET,
160 g_param_spec_string ("stylesheet",
161 "XSLT Stylesheet",
162 "The location of the XSLT stylesheet",
163 NULL,
164 G_PARAM_CONSTRUCT_ONLY |
165 G_PARAM_READWRITE | G_PARAM_STATIC_NAME |
166 G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB));
167 }
168
169 static void
yelp_transform_dispose(GObject * object)170 yelp_transform_dispose (GObject *object)
171 {
172 YelpTransformPrivate *priv =
173 yelp_transform_get_instance_private (YELP_TRANSFORM (object));
174
175 debug_print (DB_FUNCTION, "entering\n");
176
177 if (priv->queue) {
178 g_async_queue_unref (priv->queue);
179 priv->queue = NULL;
180 }
181
182 /* We do not free input or aux. They belong to the caller, which
183 must ensure they exist for the lifetime of the transform. We
184 have to set priv->aux_xslt->doc (which is priv->aux) to NULL
185 before xsltFreeTransformContext. Otherwise it will be freed,
186 which we don't want.
187 */
188 if (priv->aux_xslt)
189 priv->aux_xslt->doc = NULL;
190
191 /* We free these in dispose to make absolutely certain that they're
192 freed by the time any weak notify callbacks are called. These
193 may be used elsewhere to free resources like the input document.
194 */
195 if (priv->context) {
196 xsltFreeTransformContext (priv->context);
197 priv->context = NULL;
198 }
199 if (priv->stylesheet) {
200 xsltFreeStylesheet (priv->stylesheet);
201 priv->stylesheet = NULL;
202 }
203 if (priv->output) {
204 xmlFreeDoc (priv->output);
205 priv->output = NULL;
206 }
207 g_clear_pointer (&priv->stylesheet_file, g_free);
208
209 G_OBJECT_CLASS (yelp_transform_parent_class)->dispose (object);
210 }
211
212 static void
yelp_transform_finalize(GObject * object)213 yelp_transform_finalize (GObject *object)
214 {
215 YelpTransformPrivate *priv =
216 yelp_transform_get_instance_private (YELP_TRANSFORM (object));
217 GHashTableIter iter;
218 gpointer chunk;
219
220 debug_print (DB_FUNCTION, "entering\n");
221
222 if (priv->error)
223 g_error_free (priv->error);
224
225 g_hash_table_iter_init (&iter, priv->chunks);
226 while (g_hash_table_iter_next (&iter, NULL, &chunk))
227 g_free (chunk);
228 g_hash_table_destroy (priv->chunks);
229
230 g_strfreev (priv->params);
231 g_mutex_clear (&priv->mutex);
232
233 G_OBJECT_CLASS (yelp_transform_parent_class)->finalize (object);
234 }
235
236 static void
yelp_transform_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)237 yelp_transform_get_property (GObject *object,
238 guint prop_id,
239 GValue *value,
240 GParamSpec *pspec)
241 {
242 YelpTransformPrivate *priv =
243 yelp_transform_get_instance_private (YELP_TRANSFORM (object));
244
245 switch (prop_id)
246 {
247 case PROP_STYLESHEET:
248 g_value_set_string (value, priv->stylesheet_file);
249 break;
250 default:
251 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
252 break;
253 }
254 }
255
256 static void
yelp_transform_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)257 yelp_transform_set_property (GObject *object,
258 guint prop_id,
259 const GValue *value,
260 GParamSpec *pspec)
261 {
262 YelpTransformPrivate *priv =
263 yelp_transform_get_instance_private (YELP_TRANSFORM (object));
264
265 switch (prop_id)
266 {
267 case PROP_STYLESHEET:
268 priv->stylesheet_file = g_value_dup_string (value);
269 break;
270 default:
271 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
272 break;
273 }
274 }
275
276 /******************************************************************************/
277
278 YelpTransform *
yelp_transform_new(const gchar * stylesheet)279 yelp_transform_new (const gchar *stylesheet)
280 {
281 return (YelpTransform *) g_object_new (YELP_TYPE_TRANSFORM,
282 "stylesheet", stylesheet,
283 NULL);
284 }
285
286 gboolean
yelp_transform_start(YelpTransform * transform,xmlDocPtr document,xmlDocPtr auxiliary,const gchar * const * params)287 yelp_transform_start (YelpTransform *transform,
288 xmlDocPtr document,
289 xmlDocPtr auxiliary,
290 const gchar * const *params)
291 {
292 YelpTransformPrivate *priv = yelp_transform_get_instance_private (transform);
293
294 priv->input = document;
295 priv->aux = auxiliary;
296 priv->params = g_strdupv ((gchar **) params);
297
298 g_mutex_init (&priv->mutex);
299 g_mutex_lock (&priv->mutex);
300 priv->running = TRUE;
301 g_object_ref (transform);
302 priv->thread = g_thread_new ("transform-run",
303 (GThreadFunc)(GCallback) transform_run,
304 transform);
305 g_mutex_unlock (&priv->mutex);
306
307 return TRUE;
308 }
309
310 gchar *
yelp_transform_take_chunk(YelpTransform * transform,const gchar * chunk_id)311 yelp_transform_take_chunk (YelpTransform *transform,
312 const gchar *chunk_id)
313 {
314 YelpTransformPrivate *priv = yelp_transform_get_instance_private (transform);
315 gchar *buf;
316
317 g_mutex_lock (&priv->mutex);
318
319 buf = g_hash_table_lookup (priv->chunks, chunk_id);
320 if (buf)
321 g_hash_table_remove (priv->chunks, chunk_id);
322
323 g_mutex_unlock (&priv->mutex);
324
325 /* The caller assumes ownership of this memory. */
326 return buf;
327 }
328
329 void
yelp_transform_cancel(YelpTransform * transform)330 yelp_transform_cancel (YelpTransform *transform)
331 {
332 YelpTransformPrivate *priv = yelp_transform_get_instance_private (transform);
333 g_mutex_lock (&priv->mutex);
334 if (priv->running) {
335 priv->cancelled = TRUE;
336 if (priv->context)
337 priv->context->state = XSLT_STATE_STOPPED;
338 }
339 g_mutex_unlock (&priv->mutex);
340 }
341
342 GError *
yelp_transform_get_error(YelpTransform * transform)343 yelp_transform_get_error (YelpTransform *transform)
344 {
345 YelpTransformPrivate *priv = yelp_transform_get_instance_private (transform);
346 GError *ret = NULL;
347
348 g_mutex_lock (&priv->mutex);
349 if (priv->error)
350 ret = g_error_copy (priv->error);
351 g_mutex_unlock (&priv->mutex);
352
353 return ret;
354 }
355
356 /******************************************************************************/
357
358 static void
transform_run(YelpTransform * transform)359 transform_run (YelpTransform *transform)
360 {
361 YelpTransformPrivate *priv = yelp_transform_get_instance_private (transform);
362
363 debug_print (DB_FUNCTION, "entering\n");
364
365 priv->stylesheet = xsltParseStylesheetFile (BAD_CAST (priv->stylesheet_file));
366 if (priv->stylesheet == NULL) {
367 g_mutex_lock (&priv->mutex);
368 if (priv->error)
369 g_error_free (priv->error);
370 priv->error = g_error_new (YELP_ERROR, YELP_ERROR_PROCESSING,
371 _("The XSLT stylesheet ‘%s’ is either missing or not valid."),
372 priv->stylesheet_file);
373 g_object_ref (transform);
374 g_idle_add ((GSourceFunc) transform_error, transform);
375 g_mutex_unlock (&priv->mutex);
376 return;
377 }
378
379 priv->context = xsltNewTransformContext (priv->stylesheet,
380 priv->input);
381 if (priv->context == NULL) {
382 g_mutex_lock (&priv->mutex);
383 if (priv->error)
384 g_error_free (priv->error);
385 priv->error = g_error_new (YELP_ERROR, YELP_ERROR_PROCESSING,
386 _("The XSLT stylesheet ‘%s’ is either missing or not valid."),
387 priv->stylesheet_file);
388 g_object_ref (transform);
389 g_idle_add ((GSourceFunc) transform_error, transform);
390 g_mutex_unlock (&priv->mutex);
391 return;
392 }
393
394 priv->context->_private = transform;
395 xsltRegisterExtElement (priv->context,
396 BAD_CAST "document",
397 BAD_CAST YELP_NAMESPACE,
398 (xsltTransformFunction) xslt_yelp_document);
399 xsltRegisterExtElement (priv->context,
400 BAD_CAST "cache",
401 BAD_CAST YELP_NAMESPACE,
402 (xsltTransformFunction) xslt_yelp_cache);
403 xsltRegisterExtFunction (priv->context,
404 BAD_CAST "input",
405 BAD_CAST YELP_NAMESPACE,
406 (xmlXPathFunction) xslt_yelp_aux);
407
408 priv->output = xsltApplyStylesheetUser (priv->stylesheet,
409 priv->input,
410 (const char **) priv->params,
411 NULL, NULL,
412 priv->context);
413 g_mutex_lock (&priv->mutex);
414 priv->running = FALSE;
415 if (!priv->cancelled) {
416 g_idle_add ((GSourceFunc) transform_final, transform);
417 g_mutex_unlock (&priv->mutex);
418 }
419 else {
420 g_mutex_unlock (&priv->mutex);
421 g_object_unref (transform);
422 }
423 }
424
425 static gboolean
transform_chunk(YelpTransform * transform)426 transform_chunk (YelpTransform *transform)
427 {
428 YelpTransformPrivate *priv = yelp_transform_get_instance_private (transform);
429 gchar *chunk_id;
430
431 debug_print (DB_FUNCTION, "entering\n");
432
433 if (priv->cancelled)
434 goto done;
435
436 chunk_id = (gchar *) g_async_queue_try_pop (priv->queue);
437
438 g_signal_emit (transform, signals[CHUNK_READY], 0, chunk_id);
439
440 g_free (chunk_id);
441
442 done:
443 g_object_unref (transform);
444 return FALSE;
445 }
446
447 static gboolean
transform_error(YelpTransform * transform)448 transform_error (YelpTransform *transform)
449 {
450 YelpTransformPrivate *priv = yelp_transform_get_instance_private (transform);
451
452 debug_print (DB_FUNCTION, "entering\n");
453
454 if (priv->cancelled)
455 goto done;
456
457 g_signal_emit (transform, signals[ERROR], 0);
458
459 done:
460 g_object_unref (transform);
461 return FALSE;
462 }
463
464 static gboolean
transform_final(YelpTransform * transform)465 transform_final (YelpTransform *transform)
466 {
467 YelpTransformPrivate *priv = yelp_transform_get_instance_private (transform);
468
469 debug_print (DB_FUNCTION, "entering\n");
470
471 if (priv->cancelled)
472 goto done;
473
474 g_signal_emit (transform, signals[FINISHED], 0);
475
476 done:
477 g_object_unref (transform);
478 return FALSE;
479 }
480
481 /******************************************************************************/
482
483 static void
xslt_yelp_document(xsltTransformContextPtr ctxt,xmlNodePtr node,xmlNodePtr inst,xsltStylePreCompPtr comp)484 xslt_yelp_document (xsltTransformContextPtr ctxt,
485 xmlNodePtr node,
486 xmlNodePtr inst,
487 xsltStylePreCompPtr comp)
488 {
489 YelpTransform *transform;
490 YelpTransformPrivate *priv;
491 xmlChar *page_id = NULL;
492 gchar *temp;
493 xmlChar *page_buf;
494 gint buf_size;
495 xsltStylesheetPtr style = NULL;
496 const char *old_outfile;
497 xmlDocPtr new_doc = NULL;
498 xmlDocPtr old_doc;
499 xmlNodePtr old_insert;
500
501 debug_print (DB_FUNCTION, "entering\n");
502
503 if (!ctxt || !node || !inst || !comp)
504 return;
505
506 if (ctxt->state == XSLT_STATE_STOPPED)
507 return;
508
509 transform = YELP_TRANSFORM (ctxt->_private);
510 priv = yelp_transform_get_instance_private (transform);
511
512 page_id = xsltEvalAttrValueTemplate (ctxt, inst,
513 (const xmlChar *) "href",
514 NULL);
515 if (page_id == NULL || *page_id == '\0') {
516 if (page_id)
517 xmlFree (page_id);
518 else
519 xsltTransformError (ctxt, NULL, inst,
520 _("No href attribute found on "
521 "yelp:document\n"));
522 /* FIXME: put a real error here */
523 goto done;
524 }
525 debug_print (DB_ARG, " page_id = \"%s\"\n", page_id);
526
527 old_outfile = ctxt->outputFile;
528 old_doc = ctxt->output;
529 old_insert = ctxt->insert;
530 ctxt->outputFile = (const char *) page_id;
531
532 style = xsltNewStylesheet ();
533 if (style == NULL) {
534 xsltTransformError (ctxt, NULL, inst,
535 _("Out of memory"));
536 goto done;
537 }
538
539 style->omitXmlDeclaration = TRUE;
540
541 new_doc = xmlNewDoc (BAD_CAST "1.0");
542 new_doc->charset = XML_CHAR_ENCODING_UTF8;
543 new_doc->dict = ctxt->dict;
544 xmlDictReference (new_doc->dict);
545
546 ctxt->output = new_doc;
547 ctxt->insert = (xmlNodePtr) new_doc;
548
549 xsltApplyOneTemplate (ctxt, node, inst->children, NULL, NULL);
550 xsltSaveResultToString (&page_buf, &buf_size, new_doc, style);
551
552 ctxt->outputFile = old_outfile;
553 ctxt->output = old_doc;
554 ctxt->insert = old_insert;
555
556 g_mutex_lock (&priv->mutex);
557
558 temp = g_strdup ((gchar *) page_id);
559 xmlFree (page_id);
560
561 g_async_queue_push (priv->queue, g_strdup ((gchar *) temp));
562 g_hash_table_insert (priv->chunks, temp, page_buf);
563
564 g_object_ref (transform);
565 g_idle_add ((GSourceFunc) transform_chunk, transform);
566
567 g_mutex_unlock (&priv->mutex);
568
569 done:
570 if (new_doc)
571 xmlFreeDoc (new_doc);
572 if (style)
573 xsltFreeStylesheet (style);
574 }
575
576 static void
xslt_yelp_cache(xsltTransformContextPtr ctxt,xmlNodePtr node,xmlNodePtr inst,xsltStylePreCompPtr comp)577 xslt_yelp_cache (xsltTransformContextPtr ctxt,
578 xmlNodePtr node,
579 xmlNodePtr inst,
580 xsltStylePreCompPtr comp)
581 {
582 }
583
584 static void
xslt_yelp_aux(xmlXPathParserContextPtr ctxt,int nargs)585 xslt_yelp_aux (xmlXPathParserContextPtr ctxt, int nargs)
586 {
587 xsltTransformContextPtr tctxt;
588 xmlXPathObjectPtr ret;
589 YelpTransform *transform;
590 YelpTransformPrivate *priv;
591
592 tctxt = xsltXPathGetTransformContext (ctxt);
593 transform = YELP_TRANSFORM (tctxt->_private);
594 priv = yelp_transform_get_instance_private (transform);
595
596 priv->aux_xslt = xsltNewDocument (tctxt, priv->aux);
597
598 ret = xmlXPathNewNodeSet (xmlDocGetRootElement (priv->aux));
599 xsltExtensionInstructionResultRegister (tctxt, ret);
600 valuePush (ctxt, ret);
601 }
602