1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
2 /* SPDX-FileCopyrightText: 2002 CodeFactory AB
3  * SPDX-FileCopyrightText: 2002 Mikael Hallendal <micke@imendio.com>
4  * SPDX-FileCopyrightText: 2004-2008 Imendio AB
5  * SPDX-FileCopyrightText: 2010 Lanedo GmbH
6  * SPDX-FileCopyrightText: 2017, 2018 Sébastien Wilmet <swilmet@gnome.org>
7  * SPDX-License-Identifier: GPL-3.0-or-later
8  */
9 
10 #include "config.h"
11 #include "dh-book.h"
12 #include <glib/gi18n-lib.h>
13 #include "dh-link.h"
14 #include "dh-parser.h"
15 #include "dh-util-lib.h"
16 
17 /**
18  * SECTION:dh-book
19  * @Title: DhBook
20  * @Short_description: A book, usually the documentation for one library
21  *
22  * A #DhBook usually contains the documentation for one library (or
23  * application), for example GLib or GTK. A #DhBook corresponds to one index
24  * file. An index file is a file with the extension `*.devhelp`, `*.devhelp2`,
25  * `*.devhelp.gz` or `*.devhelp2.gz`.
26  *
27  * #DhBook creates a #GFileMonitor on the index file, and emits the
28  * #DhBook::updated or #DhBook::deleted signal in case the index file has
29  * changed on the filesystem. #DhBookListDirectory listens to those #DhBook
30  * signals, and emits in turn the #DhBookList #DhBookList::remove-book and
31  * #DhBookList::add-book signals.
32  */
33 
34 /* Timeout to wait for new events on the index file so that they are merged and
35  * we don't spam unneeded signals.
36  */
37 #define EVENT_MERGE_TIMEOUT_SECS (2)
38 
39 enum {
40         SIGNAL_UPDATED,
41         SIGNAL_DELETED,
42         N_SIGNALS
43 };
44 
45 typedef enum {
46         BOOK_MONITOR_EVENT_NONE,
47         BOOK_MONITOR_EVENT_UPDATED,
48         BOOK_MONITOR_EVENT_DELETED
49 } BookMonitorEvent;
50 
51 typedef struct {
52         GFile *index_file;
53 
54         gchar *id;
55         gchar *title;
56         gchar *language;
57 
58         /* The book tree of DhLink*. */
59         GNode *tree;
60 
61         /* List of DhLink*. */
62         GList *links;
63 
64         DhCompletion *completion;
65 
66         GFileMonitor *index_file_monitor;
67         BookMonitorEvent last_monitor_event;
68         guint monitor_event_timeout_id;
69 } DhBookPrivate;
70 
71 G_DEFINE_TYPE_WITH_PRIVATE (DhBook, dh_book, G_TYPE_OBJECT);
72 
73 static guint signals[N_SIGNALS] = { 0 };
74 
75 static void
dh_book_dispose(GObject * object)76 dh_book_dispose (GObject *object)
77 {
78         DhBookPrivate *priv;
79 
80         priv = dh_book_get_instance_private (DH_BOOK (object));
81 
82         g_clear_object (&priv->completion);
83         g_clear_object (&priv->index_file_monitor);
84 
85         if (priv->monitor_event_timeout_id != 0) {
86                 g_source_remove (priv->monitor_event_timeout_id);
87                 priv->monitor_event_timeout_id = 0;
88         }
89 
90         G_OBJECT_CLASS (dh_book_parent_class)->dispose (object);
91 }
92 
93 static void
dh_book_finalize(GObject * object)94 dh_book_finalize (GObject *object)
95 {
96         DhBookPrivate *priv;
97 
98         priv = dh_book_get_instance_private (DH_BOOK (object));
99 
100         g_clear_object (&priv->index_file);
101         g_free (priv->id);
102         g_free (priv->title);
103         g_free (priv->language);
104         _dh_util_free_book_tree (priv->tree);
105         g_list_free_full (priv->links, (GDestroyNotify)dh_link_unref);
106 
107         G_OBJECT_CLASS (dh_book_parent_class)->finalize (object);
108 }
109 
110 static void
dh_book_class_init(DhBookClass * klass)111 dh_book_class_init (DhBookClass *klass)
112 {
113         GObjectClass *object_class = G_OBJECT_CLASS (klass);
114 
115         object_class->dispose = dh_book_dispose;
116         object_class->finalize = dh_book_finalize;
117 
118         /**
119          * DhBook::updated:
120          * @book: the #DhBook emitting the signal.
121          *
122          * The ::updated signal is emitted when the index file has been
123          * modified (but the file still exists).
124          */
125         signals[SIGNAL_UPDATED] =
126                 g_signal_new ("updated",
127                               G_TYPE_FROM_CLASS (klass),
128                               G_SIGNAL_RUN_LAST,
129                               0,
130                               NULL, NULL, NULL,
131                               G_TYPE_NONE,
132                               0);
133 
134         /**
135          * DhBook::deleted:
136          * @book: the #DhBook emitting the signal.
137          *
138          * The ::deleted signal is emitted when the index file has been deleted
139          * from the filesystem.
140          */
141         signals[SIGNAL_DELETED] =
142                 g_signal_new ("deleted",
143                               G_TYPE_FROM_CLASS (klass),
144                               G_SIGNAL_RUN_LAST,
145                               0,
146                               NULL, NULL, NULL,
147                               G_TYPE_NONE,
148                               0);
149 }
150 
151 static void
dh_book_init(DhBook * book)152 dh_book_init (DhBook *book)
153 {
154         DhBookPrivate *priv = dh_book_get_instance_private (book);
155 
156         priv->last_monitor_event = BOOK_MONITOR_EVENT_NONE;
157 }
158 
159 static gboolean
monitor_event_timeout_cb(gpointer data)160 monitor_event_timeout_cb (gpointer data)
161 {
162         DhBook *book = DH_BOOK (data);
163         DhBookPrivate *priv = dh_book_get_instance_private (book);
164         BookMonitorEvent last_monitor_event = priv->last_monitor_event;
165 
166         /* Reset event */
167         priv->last_monitor_event = BOOK_MONITOR_EVENT_NONE;
168         priv->monitor_event_timeout_id = 0;
169 
170         /* We'll get either is_deleted OR is_updated, not possible to have both
171          * or none.
172          */
173         switch (last_monitor_event)
174         {
175         case BOOK_MONITOR_EVENT_DELETED:
176                 /* Emit the signal, but make sure we hold a reference while
177                  * doing it.
178                  */
179                 g_object_ref (book);
180                 g_signal_emit (book, signals[SIGNAL_DELETED], 0);
181                 g_object_unref (book);
182                 break;
183 
184         case BOOK_MONITOR_EVENT_UPDATED:
185                 /* Emit the signal, but make sure we hold a reference while
186                  * doing it.
187                  */
188                 g_object_ref (book);
189                 g_signal_emit (book, signals[SIGNAL_UPDATED], 0);
190                 g_object_unref (book);
191                 break;
192 
193         case BOOK_MONITOR_EVENT_NONE:
194         default:
195                 break;
196         }
197 
198         /* book can be destroyed here. */
199 
200         return G_SOURCE_REMOVE;
201 }
202 
203 static void
index_file_changed_cb(GFileMonitor * file_monitor,GFile * file,GFile * other_file,GFileMonitorEvent event_type,DhBook * book)204 index_file_changed_cb (GFileMonitor      *file_monitor,
205                        GFile             *file,
206                        GFile             *other_file,
207                        GFileMonitorEvent  event_type,
208                        DhBook            *book)
209 {
210         DhBookPrivate *priv = dh_book_get_instance_private (book);
211         gboolean reset_timeout = FALSE;
212 
213         /* CREATED may happen if the file is deleted and then created right
214          * away, as we're merging events.
215          */
216         if (event_type == G_FILE_MONITOR_EVENT_CHANGED ||
217             event_type == G_FILE_MONITOR_EVENT_CREATED) {
218                 priv->last_monitor_event = BOOK_MONITOR_EVENT_UPDATED;
219                 reset_timeout = TRUE;
220         } else if (event_type == G_FILE_MONITOR_EVENT_DELETED) {
221                 priv->last_monitor_event = BOOK_MONITOR_EVENT_DELETED;
222                 reset_timeout = TRUE;
223         }
224 
225         if (reset_timeout) {
226                 if (priv->monitor_event_timeout_id != 0)
227                         g_source_remove (priv->monitor_event_timeout_id);
228 
229                 priv->monitor_event_timeout_id = g_timeout_add_seconds (EVENT_MERGE_TIMEOUT_SECS,
230                                                                         monitor_event_timeout_cb,
231                                                                         book);
232         }
233 }
234 
235 /**
236  * dh_book_new:
237  * @index_file: the index file.
238  *
239  * Returns: (nullable): a new #DhBook object, or %NULL if parsing the index file
240  * failed.
241  */
242 DhBook *
dh_book_new(GFile * index_file)243 dh_book_new (GFile *index_file)
244 {
245         DhBookPrivate *priv;
246         DhBook *book;
247         gchar *language = NULL;
248         GError *error = NULL;
249 
250         g_return_val_if_fail (G_IS_FILE (index_file), NULL);
251 
252         book = g_object_new (DH_TYPE_BOOK, NULL);
253         priv = dh_book_get_instance_private (book);
254 
255         priv->index_file = g_object_ref (index_file);
256 
257         /* Parse file storing contents in the book struct. */
258         if (!_dh_parser_read_file (priv->index_file,
259                                    &priv->title,
260                                    &priv->id,
261                                    &language,
262                                    &priv->tree,
263                                    &priv->links,
264                                    &error)) {
265                 /* It's fine if the file doesn't exist, because
266                  * DhBookListDirectory tries to create a DhBook for each
267                  * possible index file in a certain book directory.
268                  */
269                 if (error != NULL &&
270                     !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
271                         gchar *parse_name;
272 
273                         parse_name = g_file_get_parse_name (priv->index_file);
274 
275                         g_warning ("Failed to read “%s”: %s",
276                                    parse_name,
277                                    error->message);
278 
279                         g_free (parse_name);
280                 }
281 
282                 g_clear_error (&error);
283 
284                 /* Deallocate the book, as we are not going to add it in the
285                  * manager.
286                  */
287                 g_object_unref (book);
288                 return NULL;
289         }
290 
291         /* Rewrite language, if any, including the prefix we want to use when
292          * seeing it, to standarize how the language group is shown.
293          * FIXME: maybe instead of a string, have a DhLanguage object which
294          * canonicalizes the string.
295          */
296         _dh_util_ascii_strtitle (language);
297         priv->language = (language != NULL ?
298                           g_strdup_printf (_("Language: %s"), language) :
299                           g_strdup (_("Language: Undefined")));
300         g_free (language);
301 
302         /* Setup monitor for changes */
303 
304         priv->index_file_monitor = g_file_monitor_file (priv->index_file,
305                                                         G_FILE_MONITOR_NONE,
306                                                         NULL,
307                                                         &error);
308 
309         if (error != NULL) {
310                 gchar *parse_name;
311 
312                 parse_name = g_file_get_parse_name (priv->index_file);
313 
314                 g_warning ("Failed to create file monitor for file “%s”: %s",
315                            parse_name,
316                            error->message);
317 
318                 g_free (parse_name);
319                 g_clear_error (&error);
320         }
321 
322         if (priv->index_file_monitor != NULL) {
323                 g_signal_connect_object (priv->index_file_monitor,
324                                          "changed",
325                                          G_CALLBACK (index_file_changed_cb),
326                                          book,
327                                          0);
328         }
329 
330         return book;
331 }
332 
333 /**
334  * dh_book_get_index_file:
335  * @book: a #DhBook.
336  *
337  * Returns: (transfer none): the index file.
338  */
339 GFile *
dh_book_get_index_file(DhBook * book)340 dh_book_get_index_file (DhBook *book)
341 {
342         DhBookPrivate *priv;
343 
344         g_return_val_if_fail (DH_IS_BOOK (book), NULL);
345 
346         priv = dh_book_get_instance_private (book);
347 
348         return priv->index_file;
349 }
350 
351 /**
352  * dh_book_get_id:
353  * @book: a #DhBook.
354  *
355  * Gets the book ID. In the Devhelp index file format version 2, it is actually
356  * the “name”, not the ID, but “book ID” is clearer, “book name” can be confused
357  * with the title.
358  *
359  * Returns: the book ID.
360  */
361 const gchar *
dh_book_get_id(DhBook * book)362 dh_book_get_id (DhBook *book)
363 {
364         DhBookPrivate *priv;
365 
366         g_return_val_if_fail (DH_IS_BOOK (book), NULL);
367 
368         priv = dh_book_get_instance_private (book);
369 
370         return priv->id;
371 }
372 
373 /**
374  * dh_book_get_title:
375  * @book: a #DhBook.
376  *
377  * Returns: the book title.
378  */
379 const gchar *
dh_book_get_title(DhBook * book)380 dh_book_get_title (DhBook *book)
381 {
382         DhBookPrivate *priv;
383 
384         g_return_val_if_fail (DH_IS_BOOK (book), NULL);
385 
386         priv = dh_book_get_instance_private (book);
387 
388         return priv->title;
389 }
390 
391 /**
392  * dh_book_get_language:
393  * @book: a #DhBook.
394  *
395  * Returns: the programming language used in @book.
396  */
397 const gchar *
dh_book_get_language(DhBook * book)398 dh_book_get_language (DhBook *book)
399 {
400         DhBookPrivate *priv;
401 
402         g_return_val_if_fail (DH_IS_BOOK (book), NULL);
403 
404         priv = dh_book_get_instance_private (book);
405 
406         return priv->language;
407 }
408 
409 /**
410  * dh_book_get_links:
411  * @book: a #DhBook.
412  *
413  * Returns: (element-type DhLink) (transfer none): the list of
414  * <emphasis>all</emphasis> #DhLink's part of @book.
415  */
416 GList *
dh_book_get_links(DhBook * book)417 dh_book_get_links (DhBook *book)
418 {
419         DhBookPrivate *priv;
420 
421         g_return_val_if_fail (DH_IS_BOOK (book), NULL);
422 
423         priv = dh_book_get_instance_private (book);
424 
425         return priv->links;
426 }
427 
428 /**
429  * dh_book_get_tree:
430  * @book: a #DhBook.
431  *
432  * Gets the general structure of the book, as a tree. The tree contains only
433  * #DhLink's of type %DH_LINK_TYPE_BOOK or %DH_LINK_TYPE_PAGE. The other
434  * #DhLink's are not contained in the tree. To have a list of
435  * <emphasis>all</emphasis> #DhLink's part of the book, you need to call
436  * dh_book_get_links().
437  *
438  * Returns: (transfer none): the tree of #DhLink's part of @book.
439  */
440 GNode *
dh_book_get_tree(DhBook * book)441 dh_book_get_tree (DhBook *book)
442 {
443         DhBookPrivate *priv;
444 
445         g_return_val_if_fail (DH_IS_BOOK (book), NULL);
446 
447         priv = dh_book_get_instance_private (book);
448 
449         return priv->tree;
450 }
451 
452 /**
453  * dh_book_get_completion:
454  * @book: a #DhBook.
455  *
456  * Returns: (transfer none): the #DhCompletion of @book.
457  * Since: 3.28
458  */
459 DhCompletion *
dh_book_get_completion(DhBook * book)460 dh_book_get_completion (DhBook *book)
461 {
462         DhBookPrivate *priv;
463 
464         g_return_val_if_fail (DH_IS_BOOK (book), NULL);
465 
466         priv = dh_book_get_instance_private (book);
467 
468         if (priv->completion == NULL) {
469                 GList *l;
470 
471                 priv->completion = dh_completion_new ();
472 
473                 for (l = priv->links; l != NULL; l = l->next) {
474                         DhLink *link = l->data;
475                         const gchar *str;
476 
477                         /* Do not provide completion for book titles. Normally
478                          * the user doesn't need it, it's more convenient to
479                          * choose a book with the DhBookTree.
480                          */
481                         if (dh_link_get_link_type (link) == DH_LINK_TYPE_BOOK)
482                                 continue;
483 
484                         str = dh_link_get_name (link);
485                         dh_completion_add_string (priv->completion, str);
486                 }
487 
488                 dh_completion_sort (priv->completion);
489         }
490 
491         return priv->completion;
492 }
493 
494 /**
495  * dh_book_cmp_by_id:
496  * @a: a #DhBook.
497  * @b: a #DhBook.
498  *
499  * Compares the #DhBook's by their IDs, with g_ascii_strcasecmp().
500  *
501  * Returns: an integer less than, equal to, or greater than zero, if @a is <, ==
502  * or > than @b.
503  */
504 gint
dh_book_cmp_by_id(DhBook * a,DhBook * b)505 dh_book_cmp_by_id (DhBook *a,
506                    DhBook *b)
507 {
508         DhBookPrivate *priv_a;
509         DhBookPrivate *priv_b;
510 
511         if (a == NULL || b == NULL)
512                 return -1;
513 
514         priv_a = dh_book_get_instance_private (a);
515         priv_b = dh_book_get_instance_private (b);
516 
517         if (priv_a->id == NULL || priv_b->id == NULL)
518                 return -1;
519 
520         return g_ascii_strcasecmp (priv_a->id, priv_b->id);
521 }
522 
523 /**
524  * dh_book_cmp_by_title:
525  * @a: a #DhBook.
526  * @b: a #DhBook.
527  *
528  * Compares the #DhBook's by their title.
529  *
530  * Returns: an integer less than, equal to, or greater than zero, if @a is <, ==
531  * or > than @b.
532  */
533 gint
dh_book_cmp_by_title(DhBook * a,DhBook * b)534 dh_book_cmp_by_title (DhBook *a,
535                       DhBook *b)
536 {
537         DhBookPrivate *priv_a;
538         DhBookPrivate *priv_b;
539 
540         if (a == NULL || b == NULL)
541                 return -1;
542 
543         priv_a = dh_book_get_instance_private (a);
544         priv_b = dh_book_get_instance_private (b);
545 
546         if (priv_a->title == NULL || priv_b->title == NULL)
547                 return -1;
548 
549         return g_utf8_collate (priv_a->title, priv_b->title);
550 }
551