gs-plugin-fedora-pkgdb-collections.c 14.6 KB
Newer Older
1 2
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
3
 * Copyright (C) 2016-2018 Kalev Lember <klember@redhat.com>
4
 * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
5
 *
6
 * SPDX-License-Identifier: GPL-2.0+
7 8 9 10
 */

#include <config.h>

11
#include <glib/gi18n.h>
12
#include <json-glib/json-glib.h>
13
#include <gnome-software.h>
14 15 16

#define FEDORA_PKGDB_COLLECTIONS_API_URI "https://admin.fedoraproject.org/pkgdb/api/collections/"

17
struct GsPluginData {
18
	gchar		*cachefn;
19
	GFileMonitor	*cachefn_monitor;
20 21
	gchar		*os_name;
	guint64		 os_version;
22
	GsApp		*cached_origin;
23
	GSettings	*settings;
24 25
	gboolean	 is_valid;
	GPtrArray	*distros;
26
	GMutex		 mutex;
27 28
};

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
typedef enum {
	PKGDB_ITEM_STATUS_ACTIVE,
	PKGDB_ITEM_STATUS_DEVEL,
	PKGDB_ITEM_STATUS_EOL,
	PKGDB_ITEM_STATUS_LAST
} PkgdbItemStatus;

typedef struct {
	gchar			*name;
	PkgdbItemStatus		 status;
	guint			 version;
} PkgdbItem;

static void
_pkgdb_item_free (PkgdbItem *item)
{
	g_free (item->name);
	g_slice_free (PkgdbItem, item);
}

49 50 51
void
gs_plugin_initialize (GsPlugin *plugin)
{
52
	GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData));
53

54 55
	g_mutex_init (&priv->mutex);

56 57 58
	/* check that we are running on Fedora */
	if (!gs_plugin_check_distro_id (plugin, "fedora")) {
		gs_plugin_set_enabled (plugin, FALSE);
59
		g_debug ("disabling '%s' as we're not Fedora", gs_plugin_get_name (plugin));
60 61
		return;
	}
62
	priv->distros = g_ptr_array_new_with_free_func ((GDestroyNotify) _pkgdb_item_free);
63
	priv->settings = g_settings_new ("org.gnome.software");
64 65 66 67 68 69

	/* require the GnomeSoftware::CpeName metadata */
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "os-release");

	/* old name */
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "fedora-distro-upgrades");
70 71 72 73 74
}

void
gs_plugin_destroy (GsPlugin *plugin)
{
75 76 77
	GsPluginData *priv = gs_plugin_get_data (plugin);
	if (priv->cachefn_monitor != NULL)
		g_object_unref (priv->cachefn_monitor);
78 79
	if (priv->cached_origin != NULL)
		g_object_unref (priv->cached_origin);
80 81
	if (priv->settings != NULL)
		g_object_unref (priv->settings);
82 83
	if (priv->distros != NULL)
		g_ptr_array_unref (priv->distros);
84 85
	g_free (priv->os_name);
	g_free (priv->cachefn);
86
	g_mutex_clear (&priv->mutex);
87 88
}

89
static void
90 91 92 93
_file_changed_cb (GFileMonitor *monitor,
		  GFile *file, GFile *other_file,
		  GFileMonitorEvent event_type,
		  gpointer user_data)
94 95
{
	GsPlugin *plugin = GS_PLUGIN (user_data);
96
	GsPluginData *priv = gs_plugin_get_data (plugin);
97 98 99

	g_debug ("cache file changed, so reloading upgrades list");
	gs_plugin_updates_changed (plugin);
100
	priv->is_valid = FALSE;
101 102
}

103 104 105
gboolean
gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
{
106
	GsPluginData *priv = gs_plugin_get_data (plugin);
107
	const gchar *verstr = NULL;
108
	gchar *endptr = NULL;
109
	g_autoptr(GFile) file = NULL;
110
	g_autoptr(GsOsRelease) os_release = NULL;
111
	g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
112

113
	/* get the file to cache */
114
	priv->cachefn = gs_utils_get_cache_filename ("fedora-pkgdb-collections",
115 116 117 118
						     "fedora.json",
						     GS_UTILS_CACHE_FLAG_WRITEABLE,
						     error);
	if (priv->cachefn == NULL)
119 120
		return FALSE;

121
	/* watch this in case it is changed by the user */
122 123
	file = g_file_new_for_path (priv->cachefn);
	priv->cachefn_monitor = g_file_monitor (file,
124 125 126
						G_FILE_MONITOR_NONE,
						cancellable,
						error);
127
	if (priv->cachefn_monitor == NULL)
128
		return FALSE;
129
	g_signal_connect (priv->cachefn_monitor, "changed",
130
			  G_CALLBACK (_file_changed_cb), plugin);
131

132
	/* read os-release for the current versions */
133 134 135 136
	os_release = gs_os_release_new (error);
	if (os_release == NULL)
		return FALSE;
	priv->os_name = g_strdup (gs_os_release_get_name (os_release));
137
	if (priv->os_name == NULL)
138
		return FALSE;
139
	verstr = gs_os_release_get_version_id (os_release);
140 141 142 143
	if (verstr == NULL)
		return FALSE;

	/* parse the version */
144 145
	priv->os_version = g_ascii_strtoull (verstr, &endptr, 10);
	if (endptr == verstr || priv->os_version > G_MAXUINT) {
146 147
		g_set_error (error,
			     GS_PLUGIN_ERROR,
148
			     GS_PLUGIN_ERROR_INVALID_FORMAT,
149 150 151 152
			     "Failed parse VERSION_ID: %s", verstr);
		return FALSE;
	}

153 154 155 156 157 158 159 160 161 162 163 164
	/* add source */
	priv->cached_origin = gs_app_new (gs_plugin_get_name (plugin));
	gs_app_set_kind (priv->cached_origin, AS_APP_KIND_SOURCE);
	gs_app_set_origin_hostname (priv->cached_origin,
				    FEDORA_PKGDB_COLLECTIONS_API_URI);

	/* add the source to the plugin cache which allows us to match the
	 * unique ID to a GsApp when creating an event */
	gs_plugin_cache_add (plugin,
			     gs_app_get_unique_id (priv->cached_origin),
			     priv->cached_origin);

165 166 167 168
	/* success */
	return TRUE;
}

169
static gboolean
170 171 172 173
_refresh_cache (GsPlugin *plugin,
		guint cache_age,
		GCancellable *cancellable,
		GError **error)
174
{
175
	GsPluginData *priv = gs_plugin_get_data (plugin);
176
	g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin));
177

178 179
	/* check cache age */
	if (cache_age > 0) {
180
		g_autoptr(GFile) file = g_file_new_for_path (priv->cachefn);
181
		guint tmp = gs_utils_get_file_age (file);
182
		if (tmp < cache_age) {
183
			g_debug ("%s is only %u seconds old",
184
				 priv->cachefn, tmp);
185 186 187 188 189
			return TRUE;
		}
	}

	/* download new file */
190 191 192 193
	gs_app_set_summary_missing (app_dl,
				    /* TRANSLATORS: status text when downloading */
				    _("Downloading upgrade information…"));
	if (!gs_plugin_download_file (plugin, app_dl,
194 195 196 197
				      FEDORA_PKGDB_COLLECTIONS_API_URI,
				      priv->cachefn,
				      cancellable,
				      error)) {
198
		gs_utils_error_add_origin_id (error, priv->cached_origin);
199 200 201 202
		return FALSE;
	}

	/* success */
203
	priv->is_valid = FALSE;
204
	return TRUE;
205 206
}

207 208 209 210 211 212
gboolean
gs_plugin_refresh (GsPlugin *plugin,
		   guint cache_age,
		   GCancellable *cancellable,
		   GError **error)
{
213 214
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
215
	return _refresh_cache (plugin, cache_age, cancellable, error);
216 217
}

218
static gchar *
219
_get_upgrade_css_background (guint version)
220 221 222 223
{
	g_autofree gchar *filename1 = NULL;
	g_autofree gchar *filename2 = NULL;

224
	filename1 = g_strdup_printf ("/usr/share/backgrounds/f%u/default/standard/f%u.png", version, version);
225 226 227
	if (g_file_test (filename1, G_FILE_TEST_EXISTS))
		return g_strdup_printf ("url('%s')", filename1);

228
	filename2 = g_strdup_printf ("/usr/share/gnome-software/backgrounds/f%u.png", version);
229 230 231 232 233 234 235
	if (g_file_test (filename2, G_FILE_TEST_EXISTS))
		return g_strdup_printf ("url('%s')", filename2);

	/* fall back to solid colour */
	return g_strdup_printf ("#151E65");
}

236
static gint
237
_sort_items_cb (gconstpointer a, gconstpointer b)
238
{
239 240
	PkgdbItem *item_a = *((PkgdbItem **) a);
	PkgdbItem *item_b = *((PkgdbItem **) b);
241

242
	if (item_a->version > item_b->version)
243
		return 1;
244
	if (item_a->version < item_b->version)
245 246 247 248
		return -1;
	return 0;
}

249
static GsApp *
250
_create_upgrade_from_info (GsPlugin *plugin, PkgdbItem *item)
251
{
252
	GsApp *app;
253
	g_autofree gchar *app_id = NULL;
254 255 256 257 258 259 260 261
	g_autofree gchar *app_version = NULL;
	g_autofree gchar *background = NULL;
	g_autofree gchar *cache_key = NULL;
	g_autofree gchar *css = NULL;
	g_autofree gchar *url = NULL;
	g_autoptr(AsIcon) ic = NULL;

	/* search in the cache */
262
	cache_key = g_strdup_printf ("release-%u", item->version);
263 264 265 266
	app = gs_plugin_cache_lookup (plugin, cache_key);
	if (app != NULL)
		return app;

267
	app_id = g_strdup_printf ("org.fedoraproject.Fedora-%u", item->version);
268
	app_version = g_strdup_printf ("%u", item->version);
269 270 271 272 273 274 275

	/* icon from disk */
	ic = as_icon_new ();
	as_icon_set_kind (ic, AS_ICON_KIND_LOCAL);
	as_icon_set_filename (ic, "/usr/share/pixmaps/fedora-logo-sprite.png");

	/* create */
276
	app = gs_app_new (app_id);
277
	gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
278
	gs_app_set_kind (app, AS_APP_KIND_OS_UPGRADE);
279
	gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_PACKAGE);
280 281 282
	gs_app_set_name (app, GS_APP_QUALITY_LOWEST, item->name);
	gs_app_set_summary (app, GS_APP_QUALITY_LOWEST,
			    /* TRANSLATORS: this is a title for Fedora distro upgrades */
283
			    _("Upgrade for the latest features, performance and stability improvements."));
284 285 286 287
	gs_app_set_version (app, app_version);
	gs_app_set_size_installed (app, 1024 * 1024 * 1024); /* estimate */
	gs_app_set_size_download (app, 256 * 1024 * 1024); /* estimate */
	gs_app_set_license (app, GS_APP_QUALITY_LOWEST, "LicenseRef-free");
288 289 290
	gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
	gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE);
	gs_app_add_quirk (app, GS_APP_QUIRK_NOT_REVIEWABLE);
291 292 293 294
	gs_app_add_icon (app, ic);

	/* show a Fedora magazine article for the release */
	url = g_strdup_printf ("https://fedoramagazine.org/whats-new-fedora-%u-workstation",
295
			       item->version);
296 297 298
	gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url);

	/* use a fancy background */
299
	background = _get_upgrade_css_background (item->version);
300 301 302 303 304 305 306 307 308
	css = g_strdup_printf ("background: %s;"
			       "background-position: center;"
			       "background-size: cover;",
			       background);
	gs_app_set_metadata (app, "GnomeSoftware::UpgradeBanner-css", css);

	/* save in the cache */
	gs_plugin_cache_add (plugin, cache_key, app);

309 310
	/* success */
	return app;
311 312 313
}

static gboolean
314
_is_valid_upgrade (GsPlugin *plugin, PkgdbItem *item)
315 316 317 318
{
	GsPluginData *priv = gs_plugin_get_data (plugin);

	/* only interested in upgrades to the same distro */
319
	if (g_strcmp0 (item->name, priv->os_name) != 0)
320 321 322
		return FALSE;

	/* only interested in newer versions, but not more than N+2 */
323 324
	if (item->version <= priv->os_version ||
	    item->version > priv->os_version + 2)
325 326 327 328
		return FALSE;

	/* only interested in non-devel distros */
	if (!g_settings_get_boolean (priv->settings, "show-upgrade-prerelease")) {
329
		if (item->status == PKGDB_ITEM_STATUS_DEVEL)
330 331 332 333 334 335 336
			return FALSE;
	}

	/* success */
	return TRUE;
}

337 338
static gboolean
_ensure_cache (GsPlugin *plugin, GCancellable *cancellable, GError **error)
339
{
340
	GsPluginData *priv = gs_plugin_get_data (plugin);
341 342
	JsonArray *collections;
	JsonObject *root;
343 344
	gsize len;
	g_autofree gchar *data = NULL;
345 346 347 348 349
	g_autoptr(JsonParser) parser = NULL;

	/* already done */
	if (priv->is_valid)
		return TRUE;
350

351
	/* just ensure there is any data, no matter how old */
352
	if (!_refresh_cache (plugin, G_MAXUINT, cancellable, error))
353 354
		return FALSE;

355
	/* get cached file */
356 357
	if (!g_file_get_contents (priv->cachefn, &data, &len, error)) {
		gs_utils_error_convert_gio (error);
358
		return FALSE;
359
	}
360

361
	/* parse data */
362 363
	parser = json_parser_new ();
	if (!json_parser_load_from_data (parser, data, len, error))
364
		return FALSE;
365

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
	root = json_node_get_object (json_parser_get_root (parser));
	if (root == NULL) {
		g_set_error (error,
			     GS_PLUGIN_ERROR,
			     GS_PLUGIN_ERROR_INVALID_FORMAT,
			     "no root object");
		return FALSE;
	}

	collections = json_object_get_array_member (root, "collections");
	if (collections == NULL) {
		g_set_error (error,
			     GS_PLUGIN_ERROR,
			     GS_PLUGIN_ERROR_INVALID_FORMAT,
			     "no collections object");
		return FALSE;
	}

	g_ptr_array_set_size (priv->distros, 0);
	for (guint i = 0; i < json_array_get_length (collections); i++) {
		PkgdbItem *item;
		JsonObject *collection;
		PkgdbItemStatus status;
		const gchar *name;
		const gchar *status_str;
		const gchar *version_str;
		gchar *endptr = NULL;
		guint64 version;

		collection = json_array_get_object_element (collections, i);
		if (collection == NULL)
			continue;

		name = json_object_get_string_member (collection, "name");
		if (name == NULL)
			continue;

		status_str = json_object_get_string_member (collection, "status");
		if (status_str == NULL)
			continue;

		if (g_strcmp0 (status_str, "Active") == 0)
			status = PKGDB_ITEM_STATUS_ACTIVE;
		else if (g_strcmp0 (status_str, "Under Development") == 0)
			status = PKGDB_ITEM_STATUS_DEVEL;
		else if (g_strcmp0 (status_str, "EOL") == 0)
			status = PKGDB_ITEM_STATUS_EOL;
		else
			continue;

		version_str = json_object_get_string_member (collection, "version");
		if (version_str == NULL)
			continue;

		version = g_ascii_strtoull (version_str, &endptr, 10);
		if (endptr == version_str || version > G_MAXUINT)
			continue;

		/* add item */
		item = g_slice_new0 (PkgdbItem);
		item->name = g_strdup (name);
		item->status = status;
		item->version = (guint) version;
		g_ptr_array_add (priv->distros, item);
	}

	/* ensure in correct order */
	g_ptr_array_sort (priv->distros, _sort_items_cb);

	/* success */
	priv->is_valid = TRUE;
	return TRUE;
}

static PkgdbItem *
_get_item_by_cpe_name (GsPlugin *plugin, const gchar *cpe_name)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	guint64 version;
	g_auto(GStrv) split = NULL;

	/* split up 'cpe:/o:fedoraproject:fedora:26' to sections */
	split = g_strsplit (cpe_name, ":", -1);
	if (g_strv_length (split) < 5) {
		g_warning ("CPE invalid format: %s", cpe_name);
		return NULL;
	}

	/* find the correct collection */
	version = g_ascii_strtoull (split[4], NULL, 10);
	if (version == 0) {
		g_warning ("failed to parse CPE version: %s", split[4]);
		return NULL;
	}
	for (guint i = 0; i < priv->distros->len; i++) {
		PkgdbItem *item = g_ptr_array_index (priv->distros, i);
		if (g_ascii_strcasecmp (item->name, split[3]) == 0 &&
		    item->version == version)
			return item;
	}
	return NULL;
}

gboolean
gs_plugin_add_distro_upgrades (GsPlugin *plugin,
			       GsAppList *list,
			       GCancellable *cancellable,
			       GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
476
	g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
477 478 479 480 481 482 483 484 485

	/* ensure valid data is loaded */
	if (!_ensure_cache (plugin, cancellable, error))
		return FALSE;

	/* are any distros upgradable */
	for (guint i = 0; i < priv->distros->len; i++) {
		PkgdbItem *item = g_ptr_array_index (priv->distros, i);
		if (_is_valid_upgrade (plugin, item)) {
486
			g_autoptr(GsApp) app = NULL;
487
			app = _create_upgrade_from_info (plugin, item);
488 489
			gs_app_list_add (list, app);
		}
490 491 492 493
	}

	return TRUE;
}
494 495 496 497 498 499 500 501

gboolean
gs_plugin_refine_app (GsPlugin *plugin,
		      GsApp *app,
		      GsPluginRefineFlags flags,
		      GCancellable *cancellable,
		      GError **error)
{
502
	GsPluginData *priv = gs_plugin_get_data (plugin);
503 504
	PkgdbItem *item;
	const gchar *cpe_name;
505
	g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541

	/* not for us */
	if (gs_app_get_kind (app) != AS_APP_KIND_OS_UPGRADE)
		return TRUE;

	/* not enough metadata */
	cpe_name = gs_app_get_metadata_item (app, "GnomeSoftware::CpeName");
	if (cpe_name == NULL)
		return TRUE;

	/* ensure valid data is loaded */
	if (!_ensure_cache (plugin, cancellable, error))
		return FALSE;

	/* find item */
	item = _get_item_by_cpe_name (plugin, cpe_name);
	if (item == NULL) {
		g_warning ("did not find %s", cpe_name);
		return TRUE;
	}

	/* fix the state */
	switch (item->status) {
	case PKGDB_ITEM_STATUS_ACTIVE:
	case PKGDB_ITEM_STATUS_DEVEL:
		gs_app_set_state (app, AS_APP_STATE_UPDATABLE);
		break;
	case PKGDB_ITEM_STATUS_EOL:
		gs_app_set_state (app, AS_APP_STATE_UNAVAILABLE);
		break;
	default:
		break;
	}

	return TRUE;
}