1 /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2  *
3  * Copyright (C) 2013-2015 Richard Hughes <richard@hughsie.com>
4  *
5  * Licensed under the GNU Lesser General Public License Version 2.1
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or(at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
20  */
21 
22 /**
23  * SECTION:dnf-repo-loader
24  * @short_description: An object for loading several #DnfRepo objects.
25  * @include: libdnf.h
26  * @stability: Stable
27  *
28  * This object can create #DnfRepo objects from either DVD media or from .repo
29  * files in the repodir.
30  *
31  * See also: #DnfRepo
32  */
33 
34 #include <strings.h>
35 
36 #include <gio/gunixmounts.h>
37 #include <librepo/util.h>
38 #include <string.h>
39 
40 #include "catch-error.hpp"
41 #include "dnf-package.h"
42 #include "dnf-repo-loader.h"
43 #include "dnf-utils.h"
44 
45 typedef struct
46 {
47     GPtrArray       *monitor_repos;
48     DnfContext      *context;    /* weak reference */
49     GPtrArray       *repos;
50     GVolumeMonitor  *volume_monitor;
51     gboolean         loaded;
52 } DnfRepoLoaderPrivate;
53 
54 enum {
55     SIGNAL_CHANGED,
56     SIGNAL_LAST
57 };
58 
59 static guint signals[SIGNAL_LAST] = { 0 };
60 
G_DEFINE_TYPE_WITH_PRIVATE(DnfRepoLoader,dnf_repo_loader,G_TYPE_OBJECT)61 G_DEFINE_TYPE_WITH_PRIVATE(DnfRepoLoader, dnf_repo_loader, G_TYPE_OBJECT)
62 #define GET_PRIVATE(o) (static_cast<DnfRepoLoaderPrivate *>(dnf_repo_loader_get_instance_private (o)))
63 
64 /**
65  * dnf_repo_loader_finalize:
66  **/
67 static void
68 dnf_repo_loader_finalize(GObject *object)
69 {
70     DnfRepoLoader *self = DNF_REPO_LOADER(object);
71     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
72 
73     if (priv->context != NULL)
74         g_object_remove_weak_pointer(G_OBJECT(priv->context),
75                                      (void **) &priv->context);
76     g_ptr_array_unref(priv->monitor_repos);
77     g_object_unref(priv->volume_monitor);
78     g_ptr_array_unref(priv->repos);
79 
80     G_OBJECT_CLASS(dnf_repo_loader_parent_class)->finalize(object);
81 }
82 
83 /**
84  * dnf_repo_loader_invalidate:
85  */
86 static void
dnf_repo_loader_invalidate(DnfRepoLoader * self)87 dnf_repo_loader_invalidate(DnfRepoLoader *self)
88 {
89     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
90     priv->loaded = FALSE;
91     dnf_context_invalidate_full(priv->context, "repos.d invalidated",
92                      DNF_CONTEXT_INVALIDATE_FLAG_ENROLLMENT);
93 }
94 
95 /**
96  * dnf_repo_loader_mount_changed_cb:
97  */
98 static void
dnf_repo_loader_mount_changed_cb(GVolumeMonitor * vm,GMount * mount,DnfRepoLoader * self)99 dnf_repo_loader_mount_changed_cb(GVolumeMonitor *vm, GMount *mount, DnfRepoLoader *self)
100 {
101     /* invalidate all repos if a CD is inserted / removed */
102     g_debug("emit changed(mounts changed)");
103     g_signal_emit(self, signals[SIGNAL_CHANGED], 0);
104     dnf_repo_loader_invalidate(self);
105 }
106 
107 /**
108  * dnf_repo_loader_init:
109  **/
110 static void
dnf_repo_loader_init(DnfRepoLoader * self)111 dnf_repo_loader_init(DnfRepoLoader *self)
112 {
113     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
114     priv->monitor_repos = g_ptr_array_new_with_free_func((GDestroyNotify) g_object_unref);
115     priv->repos = g_ptr_array_new_with_free_func((GDestroyNotify) g_object_unref);
116     priv->volume_monitor = g_volume_monitor_get();
117     g_signal_connect(priv->volume_monitor, "mount-added",
118                      G_CALLBACK(dnf_repo_loader_mount_changed_cb), self);
119     g_signal_connect(priv->volume_monitor, "mount-removed",
120                      G_CALLBACK(dnf_repo_loader_mount_changed_cb), self);
121 }
122 
123 /**
124  * dnf_repo_loader_class_init:
125  **/
126 static void
dnf_repo_loader_class_init(DnfRepoLoaderClass * klass)127 dnf_repo_loader_class_init(DnfRepoLoaderClass *klass)
128 {
129     GObjectClass *object_class = G_OBJECT_CLASS(klass);
130 
131     /**
132      * DnfRepoLoader::changed:
133      **/
134     signals[SIGNAL_CHANGED] =
135         g_signal_new("changed",
136                   G_TYPE_FROM_CLASS(object_class), G_SIGNAL_RUN_LAST,
137                   G_STRUCT_OFFSET(DnfRepoLoaderClass, changed),
138                   NULL, NULL, g_cclosure_marshal_VOID__VOID,
139                   G_TYPE_NONE, 0);
140 
141     object_class->finalize = dnf_repo_loader_finalize;
142 }
143 
144 /**
145  * dnf_repo_loader_add_media:
146  **/
147 static gboolean
dnf_repo_loader_add_media(DnfRepoLoader * self,const gchar * mount_point,guint idx,GError ** error)148 dnf_repo_loader_add_media(DnfRepoLoader *self,
149              const gchar *mount_point,
150              guint idx,
151              GError **error)
152 {
153     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
154     DnfRepo *repo;
155     g_autofree gchar *treeinfo_fn = NULL;
156     g_autoptr(GKeyFile) treeinfo = NULL;
157 
158     /* get common things */
159     treeinfo_fn = g_build_filename(mount_point, ".treeinfo", NULL);
160     treeinfo = g_key_file_new();
161     if (!g_key_file_load_from_file(treeinfo, treeinfo_fn, G_KEY_FILE_NONE, error))
162         return FALSE;
163 
164     /* create read-only location */
165     repo = dnf_repo_new(priv->context);
166     dnf_repo_set_enabled(repo, DNF_REPO_ENABLED_PACKAGES);
167     dnf_repo_set_gpgcheck(repo, TRUE);
168     dnf_repo_set_kind(repo, DNF_REPO_KIND_MEDIA);
169     dnf_repo_set_skip_if_unavailable(repo, TRUE);
170     dnf_repo_set_cost(repo, 100);
171     dnf_repo_set_keyfile(repo, treeinfo);
172     if (idx == 0) {
173         dnf_repo_set_id(repo, "media");
174     } else {
175         g_autofree gchar *tmp = NULL;
176         tmp = g_strdup_printf("media-%i", idx);
177         dnf_repo_set_id(repo, tmp);
178     }
179     dnf_repo_set_location(repo, mount_point);
180     if (!dnf_repo_setup(repo, error))
181         return FALSE;
182 
183     g_debug("added repo %s", dnf_repo_get_id(repo));
184     g_ptr_array_add(priv->repos, repo);
185     return TRUE;
186 }
187 
188 /**
189  * dnf_repo_loader_add_sack_from_mount_point:
190  */
191 static gboolean
dnf_repo_loader_add_sack_from_mount_point(DnfRepoLoader * self,const gchar * root,guint * idx,GError ** error)192 dnf_repo_loader_add_sack_from_mount_point(DnfRepoLoader *self,
193                                           const gchar *root,
194                                           guint *idx,
195                                           GError **error)
196 {
197     const gchar *id = ".treeinfo";
198     gboolean exists;
199     g_autofree gchar *treeinfo_fn = NULL;
200 
201     /* check if any installed media is an install disk */
202     treeinfo_fn = g_build_filename(root, id, NULL);
203     exists = g_file_test(treeinfo_fn, G_FILE_TEST_EXISTS);
204     g_debug("checking %s for %s: %s", root, id, exists ? "yes" : "no");
205     if (!exists)
206         return TRUE;
207 
208     /* add the repodata/repomd.xml as a repo */
209     if (!dnf_repo_loader_add_media(self, root, *idx, error))
210         return FALSE;
211    (*idx)++;
212     return TRUE;
213 }
214 
215 /**
216  * dnf_repo_loader_get_repos_removable:
217  */
218 static gboolean
dnf_repo_loader_get_repos_removable(DnfRepoLoader * self,GError ** error)219 dnf_repo_loader_get_repos_removable(DnfRepoLoader *self, GError **error)
220 {
221     GList *mounts;
222     GList *l;
223     gboolean ret = TRUE;
224     guint idx = 0;
225 
226     /* coldplug the mounts */
227     mounts = g_unix_mounts_get(NULL);
228     for (l = mounts; l != NULL; l = l->next) {
229         GUnixMountEntry *e =(GUnixMountEntry *) l->data;
230         if (!g_unix_mount_is_readonly(e))
231             continue;
232         if (g_strcmp0(g_unix_mount_get_fs_type(e), "iso9660") != 0)
233             continue;
234         ret = dnf_repo_loader_add_sack_from_mount_point(self,
235                                                   g_unix_mount_get_mount_path(e),
236                                                   &idx,
237                                                   error);
238         if (!ret)
239             goto out;
240     }
241 out:
242     g_list_foreach(mounts,(GFunc) g_unix_mount_free, NULL);
243     g_list_free(mounts);
244     return ret;
245 }
246 
247 /**
248  * dnf_repo_loader_repo_cost_fn:
249  */
250 static gint
dnf_repo_loader_repo_cost_fn(gconstpointer a,gconstpointer b)251 dnf_repo_loader_repo_cost_fn(gconstpointer a, gconstpointer b)
252 {
253     DnfRepo *repo_a = *((DnfRepo **) a);
254     DnfRepo *repo_b = *((DnfRepo **) b);
255     if (dnf_repo_get_cost(repo_a) < dnf_repo_get_cost(repo_b))
256         return -1;
257     if (dnf_repo_get_cost(repo_a) > dnf_repo_get_cost(repo_b))
258         return 1;
259     return 0;
260 }
261 
262 /**
263  * dnf_repo_loader_load_multiline_key_file:
264  **/
265 static GKeyFile *
dnf_repo_loader_load_multiline_key_file(const gchar * filename,GError ** error)266 dnf_repo_loader_load_multiline_key_file(const gchar *filename, GError **error)
267 {
268     GKeyFile *file = NULL;
269     gboolean ret;
270     gsize len;
271     guint i;
272     g_autofree gchar *data = NULL;
273     g_autoptr(GString) string = NULL;
274     g_auto(GStrv) lines = NULL;
275 
276     /* load file */
277     if (!g_file_get_contents(filename, &data, &len, error))
278         return NULL;
279 
280     /* split into lines */
281     string = g_string_new("");
282     lines = g_strsplit(data, "\n", -1);
283     for (i = 0; lines[i] != NULL; i++) {
284 
285         /* convert tabs to spaces */
286         g_strdelimit(lines[i], "\t", ' ');
287 
288         /* if a line starts with whitespace, then append it on
289          * the previous line */
290         if (lines[i][0] == ' ' && string->len > 0) {
291 
292             /* whitespace strip this new line */
293             g_strstrip(lines[i]);
294 
295             /* skip over the line if it was only whitespace */
296             if (strlen(lines[i]) == 0)
297                 continue;
298 
299             /* remove old newline from previous line */
300             g_string_set_size(string, string->len - 1);
301 
302             /* only add a ';' if we have anything after the '=' */
303             if (string->str[string->len - 1] == '=') {
304                 g_string_append_printf(string, "%s\n", lines[i]);
305             } else {
306                 g_string_append_printf(string, ";%s\n", lines[i]);
307             }
308         } else {
309             g_string_append_printf(string, "%s\n", lines[i]);
310         }
311     }
312 
313     /* remove final newline */
314     if (string->len > 0)
315         g_string_set_size(string, string->len - 1);
316 
317     /* load modified lines */
318     file = g_key_file_new();
319     ret = g_key_file_load_from_data(file,
320                                     string->str,
321                                     -1,
322                                     G_KEY_FILE_KEEP_COMMENTS,
323                                     error);
324     if (!ret) {
325         g_key_file_free(file);
326         return NULL;
327     }
328     return file;
329 }
330 
331 /**
332  * dnf_repo_loader_repo_parse_id:
333  **/
334 static gboolean
dnf_repo_loader_repo_parse_id(DnfRepoLoader * self,const gchar * id,const gchar * filename,GKeyFile * keyfile,GError ** error)335 dnf_repo_loader_repo_parse_id(DnfRepoLoader *self,
336                               const gchar *id,
337                               const gchar *filename,
338                               GKeyFile *keyfile,
339                               GError **error)
340 {
341     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
342     g_autoptr(DnfRepo) repo = NULL;
343 
344     repo = dnf_repo_new(priv->context);
345     dnf_repo_set_kind(repo, DNF_REPO_KIND_REMOTE);
346     dnf_repo_set_keyfile(repo, keyfile);
347     dnf_repo_set_filename(repo, filename);
348     dnf_repo_set_id(repo, id);
349 
350     /* set up the repo ready for use */
351     if (!dnf_repo_setup(repo, error))
352         return FALSE;
353 
354     g_debug("added repo %s\t%s", filename, id);
355     g_ptr_array_add(priv->repos, g_object_ref(repo));
356     return TRUE;
357 }
358 
359 /**
360  * dnf_repo_loader_repo_parse:
361  **/
362 static gboolean
dnf_repo_loader_repo_parse(DnfRepoLoader * self,const gchar * filename,GError ** error)363 dnf_repo_loader_repo_parse(DnfRepoLoader *self,
364                            const gchar *filename,
365                            GError **error)
366 {
367     gboolean ret = TRUE;
368     guint i;
369     g_auto(GStrv) groups = NULL;
370     g_autoptr(GKeyFile) keyfile = NULL;
371 
372     /* load non-standard keyfile */
373     keyfile = dnf_repo_loader_load_multiline_key_file(filename, error);
374     if (keyfile == NULL) {
375         g_prefix_error(error, "Failed to load %s: ", filename);
376         return FALSE;
377     }
378 
379     /* save all the repos listed in the file, "main" section is skipped - repoid can't be "main" */
380     groups = g_key_file_get_groups(keyfile, NULL);
381     for (i = 0; groups[i] != NULL; i++) {
382         if (strcmp(groups[i], "main") == 0) {
383             continue;
384         }
385         ret = dnf_repo_loader_repo_parse_id(self, groups[i], filename, keyfile, error);
386         if (!ret)
387             return FALSE;
388     }
389     return TRUE;
390 }
391 
392 /**
393  * dnf_repo_loader_refresh:
394  */
395 static gboolean
dnf_repo_loader_refresh(DnfRepoLoader * self,GError ** error)396 dnf_repo_loader_refresh(DnfRepoLoader *self, GError **error)
397 {
398     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
399 
400     if (!dnf_context_plugin_hook(priv->context, PLUGIN_HOOK_ID_CONTEXT_PRE_REPOS_RELOAD, nullptr, nullptr))
401         return FALSE;
402 
403     /* no longer loaded */
404     dnf_repo_loader_invalidate(self);
405     g_ptr_array_set_size(priv->repos, 0);
406 
407     /* re-populate redhat.repo */
408     if (!dnf_context_setup_enrollments(priv->context, error))
409         return FALSE;
410 
411     /* load repos defined in main configuration */
412     auto cfg_file_path = dnf_context_get_config_file_path();
413     if (cfg_file_path[0] != '\0' &&
414         (dnf_context_is_set_config_file_path() || g_file_test(cfg_file_path, G_FILE_TEST_IS_REGULAR))) {
415         if (!dnf_repo_loader_repo_parse(self, cfg_file_path, error)) {
416             return FALSE;
417         }
418     }
419 
420     /* open dir */
421     auto repos_dir = dnf_context_get_repos_dir(priv->context);
422     for (auto item = repos_dir; *item; ++item) {
423         auto repo_path = *item;
424         g_autoptr(GDir) dir = g_dir_open(repo_path, 0, NULL);
425         // existence of repos directories is not mandatory
426         if (dir == NULL)
427             continue;
428 
429         /* find all the .repo files */
430         while (auto file = g_dir_read_name(dir)) {
431             g_autofree gchar *path_tmp = NULL;
432             if (!g_str_has_suffix(file, ".repo"))
433                 continue;
434             path_tmp = g_build_filename(repo_path, file, NULL);
435             if (!dnf_repo_loader_repo_parse(self, path_tmp, error))
436                 return FALSE;
437         }
438     }
439 
440     /* add any DVD repos */
441     if (!dnf_repo_loader_get_repos_removable(self, error))
442         return FALSE;
443 
444     /* all okay */
445     priv->loaded = TRUE;
446 
447     /* sort these in order of cost */
448     g_ptr_array_sort(priv->repos, dnf_repo_loader_repo_cost_fn);
449     return TRUE;
450 }
451 
452 /**
453  * dnf_repo_loader_has_removable_repos:
454  */
455 gboolean
dnf_repo_loader_has_removable_repos(DnfRepoLoader * self)456 dnf_repo_loader_has_removable_repos(DnfRepoLoader *self)
457 {
458     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
459     guint i;
460 
461     g_return_val_if_fail(DNF_IS_REPO_LOADER(self), FALSE);
462 
463     /* are there any media repos */
464     for (i = 0; i < priv->repos->len; i++) {
465         auto repo = static_cast<DnfRepo *>(g_ptr_array_index(priv->repos, i));
466         if (dnf_repo_get_kind(repo) == DNF_REPO_KIND_MEDIA)
467             return TRUE;
468     }
469     return FALSE;
470 }
471 
472 /**
473  * dnf_repo_loader_get_repos:
474  *
475  * Returns:(transfer container)(element-type DnfRepo): Array of repos
476  */
477 GPtrArray *
dnf_repo_loader_get_repos(DnfRepoLoader * self,GError ** error)478 dnf_repo_loader_get_repos(DnfRepoLoader *self, GError **error) try
479 {
480     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
481 
482     g_return_val_if_fail(DNF_IS_REPO_LOADER(self), NULL);
483     g_return_val_if_fail(error == NULL || *error == NULL, NULL);
484 
485     /* nothing set yet */
486     if (!priv->loaded) {
487         if (!dnf_repo_loader_refresh(self, error))
488             return NULL;
489     }
490 
491     /* all okay */
492     return g_ptr_array_ref(priv->repos);
493 } CATCH_TO_GERROR(NULL)
494 
495 /**
496  * dnf_repo_loader_get_repo_by_id:
497  */
498 DnfRepo *
499 dnf_repo_loader_get_repo_by_id(DnfRepoLoader *self, const gchar *id, GError **error) try
500 {
501     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
502     guint i;
503 
504     g_return_val_if_fail(DNF_IS_REPO_LOADER(self), NULL);
505     g_return_val_if_fail(id != NULL, NULL);
506     g_return_val_if_fail(error == NULL || *error == NULL, NULL);
507 
508     /* nothing set yet */
509     if (!priv->loaded) {
510         if (!dnf_repo_loader_refresh(self, error))
511             return NULL;
512     }
513 
514     for (i = 0; i < priv->repos->len; i++) {
515         auto tmp = static_cast<DnfRepo *>(g_ptr_array_index(priv->repos, i));
516         if (g_strcmp0(dnf_repo_get_id(tmp), id) == 0)
517             return tmp;
518     }
519 
520     /* we didn't find anything */
521     g_set_error(error,
522                 DNF_ERROR,
523                 DNF_ERROR_REPO_NOT_FOUND,
524                 "failed to find %s", id);
525     return NULL;
CATCH_TO_GERROR(NULL)526 } CATCH_TO_GERROR(NULL)
527 
528 /**
529  * dnf_repo_loader_directory_changed_cb:
530  **/
531 static void
532 dnf_repo_loader_directory_changed_cb(GFileMonitor *monitor_,
533                                      GFile *file, GFile *other_file,
534                                      GFileMonitorEvent event_type,
535                                      DnfRepoLoader *self)
536 {
537     g_debug("emit changed(ReposDir changed)");
538     g_signal_emit(self, signals[SIGNAL_CHANGED], 0);
539     dnf_repo_loader_invalidate(self);
540 }
541 
542 /**
543  * dnf_repo_loader_setup_monitor:
544  */
545 static void
dnf_repo_loader_setup_monitor(DnfRepoLoader * self,const char * path,bool is_dir)546 dnf_repo_loader_setup_monitor(DnfRepoLoader *self, const char * path, bool is_dir)
547 {
548     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
549     g_autoptr(GFile) file_path = g_file_new_for_path(path);
550     g_autoptr(GError) error = NULL;
551     GFileMonitor * monitor = NULL;
552     if (is_dir) {
553         monitor = g_file_monitor_directory(file_path, G_FILE_MONITOR_NONE, NULL, &error);
554     } else {
555         monitor = g_file_monitor_file(file_path, G_FILE_MONITOR_NONE, NULL, &error);
556     }
557     if (monitor) {
558         g_ptr_array_add(priv->monitor_repos, monitor);
559         g_signal_connect(monitor, "changed",
560                             G_CALLBACK(dnf_repo_loader_directory_changed_cb), self);
561     } else {
562         g_warning("failed to setup monitor: %s",
563                 error->message);
564     }
565 }
566 
567 /**
568  * dnf_repo_loader_setup_watch:
569  */
570 static void
dnf_repo_loader_setup_watch(DnfRepoLoader * self)571 dnf_repo_loader_setup_watch(DnfRepoLoader *self)
572 {
573     DnfRepoLoaderPrivate *priv = GET_PRIVATE(self);
574 
575     /* setup a file monitor on the main configuration file */
576     auto cfg_file_path = dnf_context_get_config_file_path();
577     if (cfg_file_path[0] != '\0' && g_file_test(cfg_file_path, G_FILE_TEST_IS_REGULAR)) {
578         dnf_repo_loader_setup_monitor(self, cfg_file_path, false);
579     }
580 
581     /* setup a file monitor on the repos directory */
582     auto repos_dir = dnf_context_get_repos_dir(priv->context);
583     if (!repos_dir[0]) {
584         g_warning("no repodir set");
585         return;
586     }
587     for (auto iter = repos_dir; *iter; ++iter) {
588         auto repo_dir = *iter;
589         dnf_repo_loader_setup_monitor(self, repo_dir, true);
590     }
591 }
592 
593 /**
594  * dnf_repo_loader_new:
595  * @context: A #DnfContext instance
596  *
597  * Creates a new #DnfRepoLoader.
598  *
599  * Returns:(transfer full): a #DnfRepoLoader
600  *
601  * Since: 0.1.0
602  **/
603 DnfRepoLoader *
dnf_repo_loader_new(DnfContext * context)604 dnf_repo_loader_new(DnfContext *context)
605 {
606     DnfRepoLoaderPrivate *priv;
607     auto self = DNF_REPO_LOADER(g_object_new(DNF_TYPE_REPO_LOADER, NULL));
608     priv = GET_PRIVATE(self);
609     priv->context = context;
610     g_object_add_weak_pointer(G_OBJECT(priv->context),(void **) &priv->context);
611     dnf_repo_loader_setup_watch(self);
612     return DNF_REPO_LOADER(self);
613 }
614