summaryrefslogtreecommitdiffstats
path: root/plugins/external-appstream/gs-plugin-external-appstream.c
blob: e94e0c54c8f2fa64ca5dee1ab045eccebf2411b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 * vi:set noexpandtab tabstop=8 shiftwidth=8:
 *
 * Copyright (C) 2016-2018 Endless Mobile, Inc.
 *
 * Authors: Joaquim Rocha <jrocha@endlessm.com>
 *
 * SPDX-License-Identifier: GPL-2.0+
 */

#include <config.h>
#include <glib/gi18n.h>

#include <gnome-software.h>
#include "gs-external-appstream-utils.h"

struct GsPluginData {
	GSettings *settings;
};

void
gs_plugin_initialize (GsPlugin *plugin)
{
	GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData));
	const gchar *system_dir = gs_external_appstream_utils_get_system_dir ();

	priv->settings = g_settings_new ("org.gnome.software");

	/* run it before the appstream plugin */
	gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "appstream");

	g_debug ("appstream system dir: %s", system_dir);
}

void
gs_plugin_destroy (GsPlugin *plugin)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_object_unref (priv->settings);
}

static gboolean
gs_plugin_external_appstream_check (const gchar *appstream_path,
					    guint cache_age)
{
	g_autoptr(GFile) file = g_file_new_for_path (appstream_path);
	guint appstream_file_age = gs_utils_get_file_age (file);
	return appstream_file_age >= cache_age;
}

static gboolean
gs_plugin_external_appstream_install (const gchar *appstream_file,
				      GCancellable *cancellable,
				      GError **error)
{
	g_autoptr(GSubprocess) subprocess = NULL;
	const gchar *argv[] = { "pkexec",
				LIBEXECDIR "/gnome-software-install-appstream",
				appstream_file, NULL};
	g_debug ("Installing the appstream file %s in the system",
		 appstream_file);
	subprocess = g_subprocess_newv (argv,
					G_SUBPROCESS_FLAGS_STDOUT_PIPE |
					G_SUBPROCESS_FLAGS_STDIN_PIPE, error);
	if (subprocess == NULL)
		return FALSE;
	return g_subprocess_wait_check (subprocess, cancellable, error);
}

static gchar *
gs_plugin_external_appstream_get_modification_date (const gchar *file_path)
{
#ifndef GLIB_VERSION_2_62
	GTimeVal time_val;
#endif
	g_autoptr(GDateTime) date_time = NULL;
	g_autoptr(GFile) file = NULL;
	g_autoptr(GFileInfo) info = NULL;

	file = g_file_new_for_path (file_path);
	info = g_file_query_info (file,
				  G_FILE_ATTRIBUTE_TIME_MODIFIED,
				  G_FILE_QUERY_INFO_NONE,
				  NULL,
				  NULL);
	if (info == NULL)
		return NULL;
#ifdef GLIB_VERSION_2_62
	date_time = g_file_info_get_modification_date_time (info);
#else
	g_file_info_get_modification_time (info, &time_val);
	date_time = g_date_time_new_from_timeval_local (&time_val);
#endif
	return g_date_time_format (date_time, "%a, %d %b %Y %H:%M:%S %Z");
}

static gboolean
gs_plugin_external_appstream_refresh_sys (GsPlugin *plugin,
					  const gchar *url,
					  guint cache_age,
					  GCancellable *cancellable,
					  GError **error)
{
	GOutputStream *outstream = NULL;
	SoupSession *soup_session;
	guint status_code;
	gboolean file_written;
	g_autofree gchar *tmp_file_path = NULL;
	g_autofree gchar *file_name = NULL;
	g_autofree gchar *local_mod_date = NULL;
	g_autofree gchar *target_file_path = NULL;
	g_autofree gchar *tmp_file_tmpl = NULL;
	g_autoptr(GFileIOStream) iostream = NULL;
	g_autoptr(GFile) tmp_file = NULL;
	g_autoptr(SoupMessage) msg = NULL;

	/* check age */
	file_name = g_path_get_basename (url);
	target_file_path = gs_external_appstream_utils_get_file_cache_path (file_name);
	if (!gs_plugin_external_appstream_check (target_file_path, cache_age)) {
		g_debug ("skipping updating external appstream file %s: "
			 "cache age is older than file",
			 target_file_path);
		return TRUE;
	}

	msg = soup_message_new (SOUP_METHOD_GET, url);

	/* Set the If-Modified-Since header if the target file exists */
	local_mod_date = gs_plugin_external_appstream_get_modification_date (target_file_path);
	if (local_mod_date != NULL) {
		g_debug ("Requesting contents of %s if modified since %s",
			 url, local_mod_date);
		soup_message_headers_append (msg->request_headers,
					     "If-Modified-Since",
					     local_mod_date);
	}

	/* get the data */
	soup_session = gs_plugin_get_soup_session (plugin);
	status_code = soup_session_send_message (soup_session, msg);
	if (status_code != SOUP_STATUS_OK) {
		if (status_code == SOUP_STATUS_NOT_MODIFIED) {
			g_debug ("Not updating %s has not modified since %s",
				 target_file_path, local_mod_date);
			return TRUE;
		}

		g_set_error (error, GS_PLUGIN_ERROR,
			     GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
			     "Failed to download appstream file %s: %s",
			     url, soup_status_get_phrase (status_code));
		return FALSE;
	}

	/* write the download contents into a file that will be copied into
	 * the system */
	tmp_file_path = gs_utils_get_cache_filename ("external-appstream",
						     file_name,
						     GS_UTILS_CACHE_FLAG_WRITEABLE,
						     error);
	if (tmp_file_path == NULL)
		return FALSE;

	tmp_file = g_file_new_for_path (tmp_file_path);

	/* ensure the file doesn't exist */
	if (g_file_query_exists (tmp_file, cancellable) &&
	    !g_file_delete (tmp_file, cancellable, error))
		return FALSE;

	iostream = g_file_create_readwrite (tmp_file, G_FILE_CREATE_NONE,
					    cancellable, error);

	if (iostream == NULL)
		return FALSE;

	g_debug ("Downloaded appstream file %s", tmp_file_path);

	/* write to file */
	outstream = g_io_stream_get_output_stream (G_IO_STREAM (iostream));
	file_written = g_output_stream_write_all (outstream,
						  msg->response_body->data,
						  msg->response_body->length,
						  NULL, cancellable, error);

	/* close the file */
	g_output_stream_close (outstream, cancellable, NULL);

	/* install file systemwide */
	if (file_written) {
		if (gs_plugin_external_appstream_install (tmp_file_path,
							  cancellable,
							  error)) {
			g_debug ("Installed appstream file %s", tmp_file_path);
		} else {
			file_written = FALSE;
		}
	}

	/* clean up the temporary file */
	g_file_delete (tmp_file, cancellable, NULL);
	return file_written;
}

static gboolean
gs_plugin_external_appstream_refresh_user (GsPlugin *plugin,
					   const gchar *url,
					   guint cache_age,
					   GCancellable *cancellable,
					   GError **error)
{
	guint file_age;
	g_autofree gchar *basename = NULL;
	g_autofree gchar *fullpath = NULL;
	g_autoptr(GFile) file = NULL;
	g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin));

	/* check age */
	basename = g_path_get_basename (url);
	fullpath = g_build_filename (g_get_user_data_dir (),
				     "app-info",
				     "xmls",
				     basename,
				     NULL);
	file = g_file_new_for_path (fullpath);
	file_age = gs_utils_get_file_age (file);
	if (file_age < cache_age) {
		g_debug ("skipping %s: cache age is older than file", fullpath);
		return TRUE;
	}

	/* download file */
	gs_app_set_summary_missing (app_dl,
				    /* TRANSLATORS: status text when downloading */
				    _("Downloading extra metadata files…"));
	return gs_plugin_download_file (plugin, app_dl, url, fullpath,
					cancellable, error);
}

static gboolean
gs_plugin_external_appstream_refresh_url (GsPlugin *plugin,
					  const gchar *url,
					  guint cache_age,
					  GCancellable *cancellable,
					  GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	if (g_settings_get_strv (priv->settings, "external-appstream-urls")) {
		return gs_plugin_external_appstream_refresh_sys (plugin, url,
								 cache_age,
								 cancellable,
								 error);
	}
	return gs_plugin_external_appstream_refresh_user (plugin, url, cache_age,
							  cancellable, error);
}

gboolean
gs_plugin_refresh (GsPlugin *plugin,
		   guint cache_age,
		   GCancellable *cancellable,
		   GError **error)
{
	GsPluginData *priv = gs_plugin_get_data (plugin);
	g_auto(GStrv) appstream_urls = NULL;

	appstream_urls = g_settings_get_strv (priv->settings,
					      "external-appstream-urls");
	for (guint i = 0; appstream_urls[i] != NULL; ++i) {
		g_autoptr(GError) error_local = NULL;
		if (!g_str_has_prefix (appstream_urls[i], "https")) {
			g_warning ("Not considering %s as an external "
				   "appstream source: please use an https URL",
				   appstream_urls[i]);
			continue;
		}
		if (!gs_plugin_external_appstream_refresh_url (plugin,
							       appstream_urls[i],
							       cache_age,
							       cancellable,
							       &error_local)) {
			g_warning ("Failed to update external appstream file: %s",
				   error_local->message);
		}
	}

	return TRUE;
}