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