1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  * Copyright (C) 2007 Don Scorgie <dscorgie@svn.gnome.org>
4  * Copyright (C) 2010-2020 Shaun McCance <shaunm@gnome.org>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of the
9  * License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public
17  * License along with this program; if not, see <http://www.gnu.org/licenses/>.
18  *
19  * Author: Don Scorgie <dscorgie@svn.gnome.org>
20  */
21 
22 #ifdef HAVE_CONFIG_H
23 #include <config.h>
24 #endif
25 
26 #include <glib.h>
27 #include <glib/gi18n.h>
28 #include <gtk/gtk.h>
29 #include <libxml/tree.h>
30 
31 #include "yelp-error.h"
32 #include "yelp-info-document.h"
33 #include "yelp-info-parser.h"
34 #include "yelp-transform.h"
35 #include "yelp-debug.h"
36 #include "yelp-settings.h"
37 
38 #define STYLESHEET DATADIR"/yelp/xslt/info2html.xsl"
39 
40 typedef enum {
41     INFO_STATE_BLANK,   /* Brand new, run transform as needed */
42     INFO_STATE_PARSING, /* Parsing/transforming document, please wait */
43     INFO_STATE_PARSED,  /* All done, if we ain't got it, it ain't here */
44     INFO_STATE_STOP     /* Stop everything now, object to be disposed */
45 } InfoState;
46 
47 typedef struct _YelpInfoDocumentPrivate  YelpInfoDocumentPrivate;
48 struct _YelpInfoDocumentPrivate {
49     InfoState    state;
50 
51     GMutex      mutex;
52     GThread    *thread;
53 
54     xmlDocPtr   xmldoc;
55     GtkTreeModel  *sections;
56 
57     gboolean    process_running;
58     gboolean    transform_running;
59 
60     YelpTransform *transform;
61     guint          chunk_ready;
62     guint          finished;
63     guint          error;
64 
65     gchar   *root_id;
66     gchar   *visit_prev_id;
67 };
68 
69 G_DEFINE_TYPE_WITH_PRIVATE (YelpInfoDocument, yelp_info_document, YELP_TYPE_DOCUMENT)
70 
71 static void           yelp_info_document_dispose          (GObject                *object);
72 static void           yelp_info_document_finalize         (GObject                *object);
73 
74 /* YelpDocument */
75 static gboolean       info_request_page                   (YelpDocument         *document,
76                                                            const gchar          *page_id,
77                                                            GCancellable         *cancellable,
78                                                            YelpDocumentCallback  callback,
79                                                            gpointer              user_data,
80                                                            GDestroyNotify        notify);
81 
82 /* YelpTransform */
83 static void           transform_chunk_ready     (YelpTransform        *transform,
84                                                  gchar                *chunk_id,
85                                                  YelpInfoDocument     *info);
86 static void           transform_finished        (YelpTransform        *transform,
87                                                  YelpInfoDocument     *info);
88 static void           transform_error           (YelpTransform        *transform,
89                                                  YelpInfoDocument     *info);
90 static void           transform_finalized       (YelpInfoDocument     *info,
91                                                  gpointer              transform);
92 
93 static void           info_document_process     (YelpInfoDocument     *info);
94 static gboolean       info_sections_visit       (GtkTreeModel         *model,
95                                                  GtkTreePath          *path,
96                                                  GtkTreeIter          *iter,
97                                                  YelpInfoDocument     *info);
98 static void           info_document_disconnect  (YelpInfoDocument     *info);
99 
100 
101 static void
yelp_info_document_class_init(YelpInfoDocumentClass * klass)102 yelp_info_document_class_init (YelpInfoDocumentClass *klass)
103 {
104     GObjectClass      *object_class   = G_OBJECT_CLASS (klass);
105     YelpDocumentClass *document_class = YELP_DOCUMENT_CLASS (klass);
106 
107     object_class->dispose = yelp_info_document_dispose;
108     object_class->finalize = yelp_info_document_finalize;
109 
110     document_class->request_page = info_request_page;
111 }
112 
113 static void
yelp_info_document_init(YelpInfoDocument * info)114 yelp_info_document_init (YelpInfoDocument *info)
115 {
116     YelpInfoDocumentPrivate *priv = yelp_info_document_get_instance_private (info);
117 
118     priv->state = INFO_STATE_BLANK;
119     priv->xmldoc = NULL;
120     g_mutex_init (&priv->mutex);
121 }
122 
123 static void
yelp_info_document_dispose(GObject * object)124 yelp_info_document_dispose (GObject *object)
125 {
126     YelpInfoDocumentPrivate *priv =
127         yelp_info_document_get_instance_private (YELP_INFO_DOCUMENT (object));
128 
129     if (priv->sections) {
130         g_object_unref (priv->sections);
131         priv->sections = NULL;
132     }
133 
134     if (priv->transform) {
135         g_object_unref (priv->transform);
136         priv->transform = NULL;
137     }
138 
139     G_OBJECT_CLASS (yelp_info_document_parent_class)->dispose (object);
140 }
141 
142 static void
yelp_info_document_finalize(GObject * object)143 yelp_info_document_finalize (GObject *object)
144 {
145     YelpInfoDocumentPrivate *priv =
146         yelp_info_document_get_instance_private (YELP_INFO_DOCUMENT (object));
147 
148     if (priv->xmldoc)
149         xmlFreeDoc (priv->xmldoc);
150 
151     g_free (priv->root_id);
152     g_free (priv->visit_prev_id);
153 
154     g_mutex_clear (&priv->mutex);
155 
156     G_OBJECT_CLASS (yelp_info_document_parent_class)->finalize (object);
157 }
158 
159 /******************************************************************************/
160 
161 YelpDocument *
yelp_info_document_new(YelpUri * uri)162 yelp_info_document_new (YelpUri *uri)
163 {
164     g_return_val_if_fail (uri != NULL, NULL);
165 
166     return (YelpDocument *) g_object_new (YELP_TYPE_INFO_DOCUMENT,
167                                           "document-uri", uri,
168                                           NULL);
169 }
170 
171 
172 /******************************************************************************/
173 /** YelpDocument **************************************************************/
174 
175 static gboolean
info_request_page(YelpDocument * document,const gchar * page_id,GCancellable * cancellable,YelpDocumentCallback callback,gpointer user_data,GDestroyNotify notify)176 info_request_page (YelpDocument         *document,
177                    const gchar          *page_id,
178                    GCancellable         *cancellable,
179                    YelpDocumentCallback  callback,
180                    gpointer              user_data,
181                    GDestroyNotify        notify)
182 {
183     YelpInfoDocumentPrivate *priv =
184         yelp_info_document_get_instance_private (YELP_INFO_DOCUMENT (document));
185     gchar *docuri;
186     GError *error;
187     gboolean handled;
188 
189     if (page_id == NULL)
190         page_id = priv->root_id;
191 
192     handled =
193         YELP_DOCUMENT_CLASS (yelp_info_document_parent_class)->request_page (document,
194                                                                              page_id,
195                                                                              cancellable,
196                                                                              callback,
197                                                                              user_data,
198                                                                              notify);
199     if (handled) {
200         return TRUE;
201     }
202 
203     g_mutex_lock (&priv->mutex);
204 
205     switch (priv->state) {
206     case INFO_STATE_BLANK:
207 	priv->state = INFO_STATE_PARSING;
208 	priv->process_running = TRUE;
209         g_object_ref (document);
210 	priv->thread = g_thread_new ("info-page",
211                                      (GThreadFunc)(GCallback) info_document_process,
212                                      document);
213 	break;
214     case INFO_STATE_PARSING:
215 	break;
216     case INFO_STATE_PARSED:
217     case INFO_STATE_STOP:
218         docuri = yelp_uri_get_document_uri (yelp_document_get_uri (document));
219         error = g_error_new (YELP_ERROR, YELP_ERROR_NOT_FOUND,
220                              _("The page ‘%s’ was not found in the document ‘%s’."),
221                              page_id, docuri);
222         g_free (docuri);
223         yelp_document_signal (document, page_id,
224                               YELP_DOCUMENT_SIGNAL_ERROR,
225                               error);
226         g_error_free (error);
227         break;
228     default:
229         g_assert_not_reached ();
230         break;
231     }
232 
233     g_mutex_unlock (&priv->mutex);
234     return TRUE;
235 }
236 
237 
238 /******************************************************************************/
239 /** YelpTransform *************************************************************/
240 
241 static void
transform_chunk_ready(YelpTransform * transform,gchar * chunk_id,YelpInfoDocument * info)242 transform_chunk_ready (YelpTransform    *transform,
243                        gchar            *chunk_id,
244                        YelpInfoDocument *info)
245 {
246     YelpInfoDocumentPrivate *priv = yelp_info_document_get_instance_private (info);
247     gchar *content;
248 
249     g_assert (transform == priv->transform);
250 
251     if (priv->state == INFO_STATE_STOP) {
252         info_document_disconnect (info);
253         return;
254     }
255 
256     content = yelp_transform_take_chunk (transform, chunk_id);
257     yelp_document_give_contents (YELP_DOCUMENT (info),
258                                  chunk_id,
259                                  content,
260                                  "application/xhtml+xml");
261 
262     yelp_document_signal (YELP_DOCUMENT (info),
263                           chunk_id,
264                           YELP_DOCUMENT_SIGNAL_INFO,
265                           NULL);
266     yelp_document_signal (YELP_DOCUMENT (info),
267                           chunk_id,
268                           YELP_DOCUMENT_SIGNAL_CONTENTS,
269                           NULL);
270 }
271 
272 static void
transform_finished(YelpTransform * transform,YelpInfoDocument * info)273 transform_finished (YelpTransform    *transform,
274                     YelpInfoDocument *info)
275 {
276     YelpInfoDocumentPrivate *priv = yelp_info_document_get_instance_private (info);
277     gchar *docuri;
278     GError *error;
279 
280     g_assert (transform == priv->transform);
281 
282     if (priv->state == INFO_STATE_STOP) {
283         info_document_disconnect (info);
284         return;
285     }
286 
287     info_document_disconnect (info);
288     priv->state = INFO_STATE_PARSED;
289 
290     /* We want to free priv->xmldoc, but we can't free it before transform
291        is finalized.   Otherwise, we could crash when YelpTransform frees
292        its libxslt resources.
293      */
294     g_object_weak_ref ((GObject *) transform,
295                        (GWeakNotify) transform_finalized,
296                        info);
297 
298     docuri = yelp_uri_get_document_uri (yelp_document_get_uri ((YelpDocument *) info));
299     error = g_error_new (YELP_ERROR, YELP_ERROR_NOT_FOUND,
300                          _("The requested page was not found in the document ‘%s’."),
301                          docuri);
302     g_free (docuri);
303     yelp_document_error_pending ((YelpDocument *) info, error);
304     g_error_free (error);
305 }
306 
307 static void
transform_error(YelpTransform * transform,YelpInfoDocument * info)308 transform_error (YelpTransform    *transform,
309                  YelpInfoDocument *info)
310 {
311     YelpInfoDocumentPrivate *priv = yelp_info_document_get_instance_private (info);
312     GError *error;
313 
314     g_assert (transform == priv->transform);
315 
316     if (priv->state == INFO_STATE_STOP) {
317         info_document_disconnect (info);
318         return;
319     }
320 
321     error = yelp_transform_get_error (transform);
322     yelp_document_error_pending ((YelpDocument *) info, error);
323     g_error_free (error);
324 
325     info_document_disconnect (info);
326 }
327 
328 static void
transform_finalized(YelpInfoDocument * info,gpointer transform)329 transform_finalized (YelpInfoDocument *info,
330                      gpointer          transform)
331 {
332     YelpInfoDocumentPrivate *priv = yelp_info_document_get_instance_private (info);
333 
334     if (priv->xmldoc)
335 	xmlFreeDoc (priv->xmldoc);
336     priv->xmldoc = NULL;
337 }
338 
339 
340 
341 /******************************************************************************/
342 /** Threaded ******************************************************************/
343 
344 static void
info_document_process(YelpInfoDocument * info)345 info_document_process (YelpInfoDocument *info)
346 {
347     YelpInfoDocumentPrivate *priv = yelp_info_document_get_instance_private (info);
348     GFile *file = NULL;
349     gchar *filepath = NULL;
350     GError *error;
351     gint  params_i = 0;
352     gchar **params = NULL;
353 
354     file = yelp_uri_get_file (yelp_document_get_uri ((YelpDocument *) info));
355     if (file == NULL) {
356         error = g_error_new (YELP_ERROR, YELP_ERROR_NOT_FOUND,
357                              _("The file does not exist."));
358         yelp_document_error_pending ((YelpDocument *) info, error);
359         g_error_free (error);
360         goto done;
361     }
362 
363     filepath = g_file_get_path (file);
364     g_object_unref (file);
365     if (!g_file_test (filepath, G_FILE_TEST_IS_REGULAR)) {
366         error = g_error_new (YELP_ERROR, YELP_ERROR_NOT_FOUND,
367                              _("The file ‘%s’ does not exist."),
368                              filepath);
369         yelp_document_error_pending ((YelpDocument *) info, error);
370         g_error_free (error);
371         goto done;
372     }
373 
374     priv->sections = (GtkTreeModel *) yelp_info_parser_parse_file (filepath);
375     gtk_tree_model_foreach (priv->sections,
376                             (GtkTreeModelForeachFunc) info_sections_visit,
377                             info);
378     priv->xmldoc = yelp_info_parser_parse_tree ((GtkTreeStore *) priv->sections);
379 
380     if (priv->xmldoc == NULL) {
381 	error = g_error_new (YELP_ERROR, YELP_ERROR_PROCESSING,
382                              _("The file ‘%s’ could not be parsed because it is"
383                                " not a well-formed info page."),
384                              filepath);
385 	yelp_document_error_pending ((YelpDocument *) info, error);
386         goto done;
387     }
388 
389     g_mutex_lock (&priv->mutex);
390     if (priv->state == INFO_STATE_STOP) {
391 	g_mutex_unlock (&priv->mutex);
392 	goto done;
393     }
394 
395     priv->transform = yelp_transform_new (STYLESHEET);
396     priv->chunk_ready =
397         g_signal_connect (priv->transform, "chunk-ready",
398                           (GCallback) transform_chunk_ready,
399                           info);
400     priv->finished =
401         g_signal_connect (priv->transform, "finished",
402                           (GCallback) transform_finished,
403                           info);
404     priv->error =
405         g_signal_connect (priv->transform, "error",
406                           (GCallback) transform_error,
407                           info);
408 
409     params = yelp_settings_get_all_params (yelp_settings_get_default (), 0, &params_i);
410 
411     priv->transform_running = TRUE;
412     yelp_transform_start (priv->transform,
413                           priv->xmldoc,
414                           NULL,
415 			  (const gchar * const *) params);
416     g_strfreev (params);
417     g_mutex_unlock (&priv->mutex);
418 
419  done:
420     g_free (filepath);
421     priv->process_running = FALSE;
422     g_object_unref (info);
423 }
424 
425 static gboolean
info_sections_visit(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,YelpInfoDocument * info)426 info_sections_visit (GtkTreeModel     *model,
427                      GtkTreePath      *path,
428                      GtkTreeIter      *iter,
429                      YelpInfoDocument *info)
430 {
431     YelpInfoDocumentPrivate *priv = yelp_info_document_get_instance_private (info);
432     gchar *page_id, *title;
433 
434     gtk_tree_model_get (model, iter,
435                         INFO_PARSER_COLUMN_PAGE_NO, &page_id,
436                         INFO_PARSER_COLUMN_PAGE_NAME, &title,
437                         -1);
438     yelp_document_set_page_id ((YelpDocument *) info, page_id, page_id);
439     yelp_document_set_page_title ((YelpDocument *) info, page_id, title);
440 
441     if (priv->root_id == NULL) {
442         priv->root_id = g_strdup (page_id);
443         yelp_document_set_page_id ((YelpDocument *) info, NULL, page_id);
444     }
445     yelp_document_set_root_id ((YelpDocument *) info, page_id, priv->root_id);
446 
447     if (priv->visit_prev_id != NULL) {
448         yelp_document_set_prev_id ((YelpDocument *) info, page_id, priv->visit_prev_id);
449         yelp_document_set_next_id ((YelpDocument *) info, priv->visit_prev_id, page_id);
450         g_free (priv->visit_prev_id);
451     }
452     priv->visit_prev_id = page_id;
453     g_free (title);
454     return FALSE;
455 }
456 
457 static void
info_document_disconnect(YelpInfoDocument * info)458 info_document_disconnect (YelpInfoDocument *info)
459 {
460     YelpInfoDocumentPrivate *priv = yelp_info_document_get_instance_private (info);
461     if (priv->chunk_ready) {
462         g_signal_handler_disconnect (priv->transform, priv->chunk_ready);
463         priv->chunk_ready = 0;
464     }
465     if (priv->finished) {
466         g_signal_handler_disconnect (priv->transform, priv->finished);
467         priv->finished = 0;
468     }
469     if (priv->error) {
470         g_signal_handler_disconnect (priv->transform, priv->error);
471         priv->error = 0;
472     }
473     yelp_transform_cancel (priv->transform);
474     g_object_unref (priv->transform);
475     priv->transform = NULL;
476     priv->transform_running = FALSE;
477 }
478