summaryrefslogtreecommitdiffstats
path: root/app/gimp-update.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--app/gimp-update.c593
1 files changed, 593 insertions, 0 deletions
diff --git a/app/gimp-update.c b/app/gimp-update.c
new file mode 100644
index 0000000..79d7b38
--- /dev/null
+++ b/app/gimp-update.c
@@ -0,0 +1,593 @@
+/* GIMP - The GNU Image Manipulation Program
+ * Copyright (C) 1995 Spencer Kimball and Peter Mattis
+ *
+ * gimp-update.c
+ * Copyright (C) 2019 Jehan
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <glib.h>
+#include <json-glib/json-glib.h>
+#include <stdio.h>
+
+#ifdef PLATFORM_OSX
+#import <Foundation/Foundation.h>
+#endif /* PLATFORM_OSX */
+
+#ifndef GIMP_CONSOLE_COMPILATION
+#include <gtk/gtk.h>
+#endif
+
+#include "libgimpbase/gimpbase.h"
+
+#include "core/core-types.h"
+
+#include "config/gimpcoreconfig.h"
+
+#ifndef GIMP_CONSOLE_COMPILATION
+#include "dialogs/about-dialog.h"
+#endif
+
+#include "gimp-intl.h"
+#include "gimp-update.h"
+#include "gimp-version.h"
+
+static gboolean gimp_update_known (GimpCoreConfig *config,
+ const gchar *last_version,
+ gint64 release_timestamp,
+ gint build_revision,
+ const gchar *comment);
+static void gimp_check_updates_process (const gchar *source,
+ gchar *file_contents,
+ gsize file_length,
+ GimpCoreConfig *config);
+#ifndef PLATFORM_OSX
+static void gimp_check_updates_callback (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data);
+#endif /* PLATFORM_OSX */
+static void gimp_update_about_dialog (GimpCoreConfig *config,
+ const GParamSpec *pspec,
+ gpointer user_data);
+
+static gboolean gimp_version_break (const gchar *v,
+ gint *major,
+ gint *minor,
+ gint *micro);
+
+static const gchar * gimp_get_version_url (void);
+
+#ifdef PLATFORM_OSX
+static gint gimp_check_updates_process_idle (gpointer data);
+#endif
+
+/* Private Functions */
+
+/**
+ * gimp_update_known:
+ * @config:
+ * @last_version:
+ * @release_timestamp: must be non-zero is @last_version is not %NULL.
+ * @build_revision:
+ * @build_comment:
+ *
+ * Compare @last_version with currently running version. If the checked
+ * version is more recent than running version, then @config properties
+ * are updated appropriately (which may trigger a dialog depending on
+ * update policy settings).
+ * If @last_version is %NULL, the currently stored "last known release"
+ * is compared. Even if we haven't made any new remote checks, it is
+ * important to always compare again stored last release, otherwise we
+ * might warn of the same current version, or worse an older version.
+ *
+ * Returns: %TRUE is @last_version (or stored last known release if
+ * @last_version was %NULL) is newer than running version.
+ */
+static gboolean
+gimp_update_known (GimpCoreConfig *config,
+ const gchar *last_version,
+ gint64 release_timestamp,
+ gint build_revision,
+ const gchar *build_comment)
+{
+ gboolean new_check = (last_version != NULL);
+ gint major;
+ gint minor;
+ gint micro;
+
+ if (last_version && release_timestamp == 0)
+ {
+ /* I don't exit with a g_return_val_if_fail() assert because this
+ * is not necessarily a code bug. It may be data issues. So let's
+ * just return with an error printed on stderr.
+ */
+ g_printerr ("%s: version %s with no release dates.\n",
+ G_STRFUNC, last_version);
+ return FALSE;
+ }
+
+ if (last_version == NULL)
+ {
+ last_version = config->last_known_release;
+ release_timestamp = config->last_release_timestamp;
+ build_revision = config->last_revision;
+ build_comment = config->last_release_comment;
+ }
+
+ if (last_version)
+ {
+ if (gimp_version_break (last_version, &major, &minor, &micro))
+ {
+ if (/* We are using a newer version than last check. This could
+ * happen if updating the config files without having
+ * re-checked the remote JSON file.
+ */
+ (major < GIMP_MAJOR_VERSION ||
+ (major == GIMP_MAJOR_VERSION && minor < GIMP_MINOR_VERSION) ||
+ (major == GIMP_MAJOR_VERSION && minor == GIMP_MINOR_VERSION && micro < GIMP_MICRO_VERSION)) ||
+ /* Already using the last officially released
+ * revision. */
+ (major == GIMP_MAJOR_VERSION &&
+ minor == GIMP_MINOR_VERSION &&
+ micro == GIMP_MICRO_VERSION &&
+ build_revision <= gimp_version_get_revision ()))
+ {
+ last_version = NULL;
+ }
+ }
+ else
+ {
+ /* If version is not properly parsed, something is wrong with
+ * upstream version number or parsing. This should not happen.
+ */
+ g_printerr ("%s: version not properly formatted: %s\n",
+ G_STRFUNC, last_version);
+
+ return FALSE;
+ }
+ }
+
+ if (last_version == NULL)
+ {
+ release_timestamp = 0;
+ build_revision = 0;
+ build_comment = NULL;
+ }
+
+ if (new_check)
+ g_object_set (config,
+ "check-update-timestamp", g_get_real_time() / G_USEC_PER_SEC,
+ NULL);
+
+ g_object_set (config,
+ "last-release-timestamp", release_timestamp,
+ "last-known-release", last_version,
+ "last-revision", build_revision,
+ "last-release-comment", build_comment,
+ NULL);
+
+ /* Are we running an old GIMP? */
+ return (last_version != NULL);
+}
+
+static gboolean
+gimp_version_break (const gchar *v,
+ gint *major,
+ gint *minor,
+ gint *micro)
+{
+ gchar **versions;
+
+ *major = 0;
+ *minor = 0;
+ *micro = 0;
+
+ if (v == NULL)
+ return FALSE;
+
+ versions = g_strsplit_set (v, ".", 3);
+ if (versions[0] != NULL)
+ {
+ *major = g_ascii_strtoll (versions[0], NULL, 10);
+ if (versions[1] != NULL)
+ {
+ *minor = g_ascii_strtoll (versions[1], NULL, 10);
+ if (versions[2] != NULL)
+ {
+ *micro = g_ascii_strtoll (versions[2], NULL, 10);
+ return TRUE;
+ }
+ }
+ }
+ g_strfreev (versions);
+
+ return (*major > 0);
+}
+
+static void
+gimp_check_updates_process (const gchar *source,
+ gchar *file_contents,
+ gsize file_length,
+ GimpCoreConfig *config)
+{
+ const gchar *platform;
+ const gchar *last_version = NULL;
+ const gchar *release_date = NULL;
+ const gchar *build_comment = NULL;
+ GError *error = NULL;
+ gint64 release_timestamp = 0;
+ gint build_revision = 0;
+ JsonParser *parser;
+ JsonPath *path;
+ JsonNode *result;
+ JsonArray *versions;
+ gint i;
+
+ /* For Windows and macOS, let's look if installers are available.
+ * For other platforms, let's just look for source release.
+ */
+ if (g_strcmp0 (GIMP_BUILD_PLATFORM_FAMILY, "windows") == 0 ||
+ g_strcmp0 (GIMP_BUILD_PLATFORM_FAMILY, "macos") == 0)
+ platform = GIMP_BUILD_PLATFORM_FAMILY;
+ else
+ platform = "source";
+
+ parser = json_parser_new ();
+ if (! json_parser_load_from_data (parser, file_contents, file_length, &error))
+ {
+ g_printerr ("%s: parsing of %s failed: %s\n", G_STRFUNC,
+ source, error->message);
+ g_free (file_contents);
+ g_clear_object (&parser);
+ g_clear_error (&error);
+
+ return;
+ }
+
+ path = json_path_new ();
+ /* Ideally we could just use Json path filters like this to
+ * retrieve only released binaries for a given platform:
+ * g_strdup_printf ("$['STABLE'][?(@.%s)]['version']", platform);
+ * json_array_get_string_element (result, 0);
+ * And that would be it! We'd have our last release for given
+ * platform.
+ * Unfortunately json-glib does not support filter syntax, so we
+ * end up looping through releases.
+ */
+ if (! json_path_compile (path, "$['STABLE'][*]", &error))
+ {
+#ifdef GIMP_UNSTABLE
+ g_printerr("Path compilation failed: %s\n", error->message);
+#endif
+ g_free (file_contents);
+ g_clear_object (&parser);
+ g_clear_error (&error);
+
+ return;
+ }
+ result = json_path_match (path, json_parser_get_root (parser));
+ g_return_if_fail (JSON_NODE_HOLDS_ARRAY (result));
+
+ versions = json_node_get_array (result);
+ for (i = 0; i < (gint) json_array_get_length (versions); i++)
+ {
+ JsonObject *version;
+
+ /* Note that we don't actually look for the highest version,
+ * but for the highest version for which a build for your
+ * platform (and optional build-id) is available.
+ *
+ * So we loop through the version list then the build array
+ * and break at first compatible release, since JSON arrays
+ * are ordered.
+ */
+ version = json_array_get_object_element (versions, i);
+ if (json_object_has_member (version, platform))
+ {
+ JsonArray *builds;
+ gint j;
+
+ builds = json_object_get_array_member (version, platform);
+
+ for (j = 0; j < (gint) json_array_get_length (builds); j++)
+ {
+ const gchar *build_id = NULL;
+ JsonObject *build;
+
+ build = json_array_get_object_element (builds, j);
+ if (json_object_has_member (build, "build-id"))
+ build_id = json_object_get_string_member (build, "build-id");
+ if (g_strcmp0 (build_id, GIMP_BUILD_ID) == 0 ||
+ g_strcmp0 (platform, "source") == 0)
+ {
+ /* Release date is the build date if any set,
+ * otherwise the main version release date.
+ */
+ if (json_object_has_member (build, "date"))
+ release_date = json_object_get_string_member (build, "date");
+ else
+ release_date = json_object_get_string_member (version, "date");
+
+ /* These are optional data. */
+ if (json_object_has_member (build, "revision"))
+ build_revision = json_object_get_int_member (build, "revision");
+ if (json_object_has_member (build, "comment"))
+ build_comment = json_object_get_string_member (build, "comment");
+ break;
+ }
+ }
+
+ if (release_date)
+ {
+ last_version = json_object_get_string_member (version, "version");
+ break;
+ }
+ }
+ }
+
+ if (last_version && release_date)
+ {
+ GDateTime *datetime;
+ gchar *str;
+
+ str = g_strdup_printf ("%s 00:00:00Z", release_date);
+ datetime = g_date_time_new_from_iso8601 (str, NULL);
+ g_free (str);
+
+ if (datetime)
+ {
+ release_timestamp = g_date_time_to_unix (datetime);
+ g_date_time_unref (datetime);
+ }
+ else
+ {
+ /* JSON file data bug. */
+ g_printerr ("%s: release date for version %s not properly formatted: %s\n",
+ G_STRFUNC, last_version, release_date);
+
+ last_version = NULL;
+ release_date = NULL;
+ build_revision = 0;
+ build_comment = NULL;
+ }
+ }
+ gimp_update_known (config, last_version, release_timestamp, build_revision, build_comment);
+
+ g_object_unref (path);
+ g_object_unref (parser);
+ g_free (file_contents);
+}
+
+#ifndef PLATFORM_OSX
+static void
+gimp_check_updates_callback (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GimpCoreConfig *config = user_data;
+ char *file_contents = NULL;
+ gsize file_length = 0;
+ GError *error = NULL;
+
+ if (g_file_load_contents_finish (G_FILE (source), result,
+ &file_contents, &file_length,
+ NULL, &error))
+ {
+ gimp_check_updates_process (g_file_get_uri (G_FILE (source)), file_contents, file_length, config);
+ }
+ else
+ {
+ g_printerr("%s: loading of %s failed: %s\n", G_STRFUNC,
+ g_file_get_uri (G_FILE (source)), error->message);
+ g_clear_error (&error);
+ }
+}
+#endif /* PLATFORM_OSX */
+
+static void
+gimp_update_about_dialog (GimpCoreConfig *config,
+ const GParamSpec *pspec,
+ gpointer user_data)
+{
+ g_signal_handlers_disconnect_by_func (config,
+ (GCallback) gimp_update_about_dialog,
+ NULL);
+
+ if (config->last_known_release != NULL)
+ {
+#ifndef GIMP_CONSOLE_COMPILATION
+ gtk_widget_show (about_dialog_create (config));
+#else
+ g_warning (_("A new version of GIMP (%s) was released.\n"
+ "It is recommended to update."),
+ config->last_known_release);
+#endif
+ }
+}
+
+static const gchar *
+gimp_get_version_url ()
+{
+#ifdef GIMP_RELEASE
+ return "https://www.gimp.org/gimp_versions.json";
+#else
+ if (g_getenv ("GIMP_DEV_VERSIONS_JSON"))
+ return g_getenv ("GIMP_DEV_VERSIONS_JSON");
+ else
+ return "https://testing.gimp.org/gimp_versions.json";
+#endif
+}
+
+#ifdef PLATFORM_OSX
+typedef struct _GimpCheckUpdatesData
+{
+ const gchar *gimp_versions;
+ gchar *json_result;
+ gsize json_size;
+ GimpCoreConfig *config;
+} GimpCheckUpdatesData;
+
+static int
+gimp_check_updates_process_idle (gpointer data)
+{
+ GimpCheckUpdatesData *check_updates_data = (GimpCheckUpdatesData *) data;
+
+ gimp_check_updates_process (check_updates_data->gimp_versions,
+ check_updates_data->json_result,
+ check_updates_data->json_size,
+ check_updates_data->config);
+
+ g_free (check_updates_data);
+
+ return FALSE; /* remove idle */
+}
+#endif /* PLATFORM_OSX */
+
+/* Public Functions */
+
+/*
+ * gimp_update_auto_check:
+ * @config:
+ *
+ * Run the check for newer versions of GIMP if conditions are right.
+ *
+ * Returns: %TRUE if a check was actually run.
+ */
+gboolean
+gimp_update_auto_check (GimpCoreConfig *config)
+{
+ gint64 prev_update_timestamp;
+ gint64 current_timestamp;
+
+ /* Builds with update check deactivated just always return FALSE. */
+#ifdef CHECK_UPDATE
+ /* Allows to disable updates at package level with a build having the
+ * version check code built-in.
+ * For instance, it would allow to use the same Windows installer for
+ * the Windows Store (with update check disabled because it comes with
+ * its own update channel).
+ */
+ if (! gimp_version_check_update () ||
+ ! config->check_updates)
+#endif
+ return FALSE;
+
+ g_object_get (config,
+ "check-update-timestamp", &prev_update_timestamp,
+ NULL);
+ current_timestamp = g_get_real_time() / G_USEC_PER_SEC;
+
+ /* Get rid of invalid saved timestamps. */
+ if (prev_update_timestamp > current_timestamp)
+ prev_update_timestamp = -1;
+
+#ifdef GIMP_RELEASE
+ /* Do not check more than once a week. */
+ if (current_timestamp - prev_update_timestamp < 3600L * 24L * 7L)
+ return FALSE;
+#endif
+
+ g_signal_connect (config, "notify::last-known-release",
+ (GCallback) gimp_update_about_dialog,
+ NULL);
+
+ gimp_update_check (config);
+
+ return TRUE;
+}
+
+/*
+ * gimp_update_check:
+ * @config:
+ *
+ * Run the check for newer versions of GIMP inconditionnally.
+ */
+void
+gimp_update_check (GimpCoreConfig *config)
+{
+#ifdef PLATFORM_OSX
+ const gchar *gimp_versions;
+
+ gimp_versions = gimp_get_version_url ();
+
+ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
+ [request setURL:[NSURL URLWithString:@(gimp_versions)]];
+ [request setHTTPMethod:@"GET"];
+
+ NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
+ /* completionHandler is called on a background thread */
+ [[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
+ NSString *reply;
+ gchar *json_result;
+ GimpCheckUpdatesData *update_results;
+
+ if (error)
+ {
+ g_printerr ("%s: gimp_update_check failed to get update from %s, with error: %s\n",
+ G_STRFUNC,
+ gimp_versions,
+ [error.localizedDescription UTF8String]);
+ return;
+ }
+
+ if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
+ NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
+
+ if (statusCode != 200)
+ {
+ g_printerr ("%s: gimp_update_check failed to get update from %s, with status code: %d\n",
+ G_STRFUNC,
+ gimp_versions,
+ (int)statusCode);
+ return;
+ }
+ }
+
+ reply = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ json_result = g_strdup ([reply UTF8String]); /* will be freed by gimp_check_updates_process */
+
+ update_results = g_new (GimpCheckUpdatesData, 1);
+
+ update_results->gimp_versions = gimp_versions;
+ update_results->json_result = json_result;
+ update_results->json_size = [reply lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+ update_results->config = config;
+
+ g_idle_add ((GSourceFunc) gimp_check_updates_process_idle, (gpointer) update_results);
+ }] resume];
+#else
+ GFile *gimp_versions;
+
+ gimp_versions = g_file_new_for_uri (gimp_get_version_url ());
+
+ g_file_load_contents_async (gimp_versions, NULL, gimp_check_updates_callback, config);
+ g_object_unref (gimp_versions);
+#endif /* PLATFORM_OSX */
+}
+
+/*
+ * gimp_update_refresh:
+ * @config:
+ *
+ * Do not execute a remote check, but refresh the known release data as
+ * it may be outdated.
+ */
+void
+gimp_update_refresh (GimpCoreConfig *config)
+{
+ gimp_update_known (config, NULL, 0, 0, NULL);
+}