/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set sw=2 ts=2 sts=2 et: */ /* * Copyright © 2011 Igalia S.L. * * 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 2, 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 . */ #include "config.h" #include "ephy-web-app-utils.h" #include "ephy-debug.h" #include "ephy-file-helpers.h" #include #include #include #include #include #include #define EPHY_WEB_APP_DESKTOP_FILE_PREFIX "epiphany-" /* This is necessary because of gnome-shell's guessing of a .desktop filename from WM_CLASS property. */ static char * get_wm_class_from_app_title (const char *title) { char *normal_title; char *wm_class; char *checksum; normal_title = g_utf8_strdown (title, -1); g_strdelimit (normal_title, " ", '-'); g_strdelimit (normal_title, G_DIR_SEPARATOR_S, '-'); checksum = g_compute_checksum_for_string (G_CHECKSUM_SHA1, title, -1); wm_class = g_strconcat (EPHY_WEB_APP_DESKTOP_FILE_PREFIX, normal_title, "-", checksum, NULL); g_free (checksum); g_free (normal_title); return wm_class; } /* Gets the proper .desktop filename from a WM_CLASS string, converting to the local charset when needed. */ static char * desktop_filename_from_wm_class (char *wm_class) { char *encoded; char *filename = NULL; GError *error = NULL; encoded = g_filename_from_utf8 (wm_class, -1, NULL, NULL, &error); if (error) { g_warning ("%s", error->message); g_error_free (error); return NULL; } filename = g_strconcat (encoded, ".desktop", NULL); g_free (encoded); return filename; } /** * ephy_web_application_get_profile_directory: * @name: the application name * * Gets the directory where the profile for @name is meant to be stored. * * Returns: (transfer full): A newly allocated string. **/ char * ephy_web_application_get_profile_directory (const char *name) { char *dot_dir, *app_dir, *wm_class, *profile_dir, *encoded; GError *error = NULL; wm_class = get_wm_class_from_app_title (name); encoded = g_filename_from_utf8 (wm_class, -1, NULL, NULL, &error); g_free (wm_class); if (error) { g_warning ("%s", error->message); g_error_free (error); return NULL; } dot_dir = !ephy_dot_dir_is_default () ? ephy_default_dot_dir () : NULL; app_dir = g_strconcat (EPHY_WEB_APP_PREFIX, encoded, NULL); profile_dir = g_build_filename (dot_dir ? dot_dir : ephy_dot_dir (), app_dir, NULL); g_free (encoded); g_free (app_dir); g_free (dot_dir); return profile_dir; } /** * ephy_web_application_delete: * @name: the name of the web application do delete * * Deletes all the data associated with a Web Application created by * Epiphany. * * Returns: %TRUE if the web app was succesfully deleted, %FALSE otherwise **/ gboolean ephy_web_application_delete (const char *name) { char *profile_dir = NULL; char *desktop_file = NULL, *desktop_path = NULL; char *wm_class; GFile *launcher = NULL; gboolean return_value = FALSE; g_return_val_if_fail (name, FALSE); profile_dir = ephy_web_application_get_profile_directory (name); if (!profile_dir) goto out; /* If there's no profile dir for this app, it means it does not * exist. */ if (!g_file_test (profile_dir, G_FILE_TEST_IS_DIR)) { g_warning ("No application with name '%s' is installed.\n", name); goto out; } if (!ephy_file_delete_dir_recursively (profile_dir, NULL)) goto out; LOG ("Deleted application profile.\n"); wm_class = get_wm_class_from_app_title (name); desktop_file = desktop_filename_from_wm_class (wm_class); g_free (wm_class); if (!desktop_file) goto out; desktop_path = g_build_filename (g_get_user_data_dir (), "applications", desktop_file, NULL); if (g_file_test (desktop_path, G_FILE_TEST_IS_DIR)) { launcher = g_file_new_for_path (desktop_path); if (!g_file_delete (launcher, NULL, NULL)) goto out; LOG ("Deleted application launcher.\n"); } return_value = TRUE; out: g_free (profile_dir); if (launcher) g_object_unref (launcher); g_free (desktop_file); g_free (desktop_path); return return_value; } static char * create_desktop_file (const char *address, const char *profile_dir, const char *title, GdkPixbuf *icon) { GKeyFile *file = NULL; char *exec_string; char *data = NULL; char *filename, *apps_path, *desktop_file_path = NULL; char *link_path; char *wm_class; GFile *link; GError *error = NULL; g_return_val_if_fail (profile_dir, NULL); wm_class = get_wm_class_from_app_title (title); filename = desktop_filename_from_wm_class (wm_class); if (!filename) goto out; file = g_key_file_new (); g_key_file_set_value (file, "Desktop Entry", "Name", title); exec_string = g_strdup_printf ("epiphany --application-mode --profile=\"%s\" %s", profile_dir, address); g_key_file_set_value (file, "Desktop Entry", "Exec", exec_string); g_free (exec_string); g_key_file_set_value (file, "Desktop Entry", "StartupNotify", "true"); g_key_file_set_value (file, "Desktop Entry", "Terminal", "false"); g_key_file_set_value (file, "Desktop Entry", "Type", "Application"); g_key_file_set_value (file, "Desktop Entry", "Categories", "Network;GNOME;GTK;"); if (icon) { GOutputStream *stream; char *path; GFile *image; path = g_build_filename (profile_dir, EPHY_WEB_APP_ICON_NAME, NULL); image = g_file_new_for_path (path); stream = (GOutputStream *)g_file_create (image, 0, NULL, NULL); gdk_pixbuf_save_to_stream (icon, stream, "png", NULL, NULL, NULL); g_key_file_set_value (file, "Desktop Entry", "Icon", path); g_object_unref (stream); g_object_unref (image); g_free (path); } g_key_file_set_value (file, "Desktop Entry", "StartupWMClass", wm_class); data = g_key_file_to_data (file, NULL, NULL); desktop_file_path = g_build_filename (profile_dir, filename, NULL); if (!g_file_set_contents (desktop_file_path, data, -1, NULL)) { g_free (desktop_file_path); desktop_file_path = NULL; } /* Create a symlink in XDG_DATA_DIR/applications for the Shell to * pick up this application. */ apps_path = g_build_filename (g_get_user_data_dir (), "applications", NULL); if (ephy_ensure_dir_exists (apps_path, &error)) { link_path = g_build_filename (apps_path, filename, NULL); link = g_file_new_for_path (link_path); g_free (link_path); g_file_make_symbolic_link (link, desktop_file_path, NULL, NULL); g_object_unref (link); } else { g_warning ("Error creating application symlink: %s", error->message); g_error_free (error); } g_free (apps_path); g_free (filename); out: g_free (wm_class); g_free (data); g_key_file_free (file); return desktop_file_path; } static SoupCookieJar *get_current_cookie_jar (void) { char *filename; SoupCookieJar *jar; char *dot_dir; /* FIXME: There's no API in WebKit2 to get all cookies, so we create a * temp read-only jar for the current cookies to read from it. * It would be better to have an API in WebKit to get the cookies instead. */ dot_dir = !ephy_dot_dir_is_default () ? ephy_default_dot_dir () : NULL; filename = g_build_filename (dot_dir ? dot_dir : ephy_dot_dir (), "cookies.sqlite", NULL); jar = (SoupCookieJar *)soup_cookie_jar_db_new (filename, TRUE); g_free (filename); g_free (dot_dir); return jar; } static void create_cookie_jar_for_domain (const char *address, const char *directory) { GSList *cookies, *p; SoupCookieJar *current_jar, *new_jar; char *domain, *filename; SoupURI *uri; /* Create the new cookie jar */ filename = g_build_filename (directory, "cookies.sqlite", NULL); new_jar = (SoupCookieJar *)soup_cookie_jar_db_new (filename, FALSE); g_free (filename); /* The app domain for the current view */ uri = soup_uri_new (address); domain = uri->host; /* The current cookies */ current_jar = get_current_cookie_jar (); if (!current_jar) { soup_uri_free (uri); return; } cookies = soup_cookie_jar_all_cookies (current_jar); for (p = cookies; p; p = p->next) { SoupCookie *cookie = (SoupCookie *)p->data; if (soup_cookie_domain_matches (cookie, domain)) soup_cookie_jar_add_cookie (new_jar, cookie); else soup_cookie_free (cookie); } soup_uri_free (uri); g_slist_free (cookies); g_object_unref (current_jar); g_object_unref (new_jar); } /** * ephy_web_application_create: * @address: the address of the new web application * @name: the name for the new web application * @icon: the icon for the new web application * * Creates a new Web Application for @address. * * Returns: (transfer-full): the path to the desktop file representing the new application **/ char * ephy_web_application_create (const char *address, const char *name, GdkPixbuf *icon) { char *profile_dir = NULL; char *desktop_file_path = NULL; /* If there's already a WebApp profile for the contents of this * view, do nothing. */ profile_dir = ephy_web_application_get_profile_directory (name); if (g_file_test (profile_dir, G_FILE_TEST_IS_DIR)) { LOG ("Profile directory %s already exists", profile_dir); goto out; } /* Create the profile directory, populate it. */ if (g_mkdir (profile_dir, 488) == -1) { LOG ("Failed to create directory %s", profile_dir); goto out; } /* Things we need in a WebApp's profile: - Our own cookies file, copying the relevant cookies for the app's domain. */ create_cookie_jar_for_domain (address, profile_dir); /* Create the deskop file. */ desktop_file_path = create_desktop_file (address, profile_dir, name, icon); out: if (profile_dir) g_free (profile_dir); return desktop_file_path; } char * ephy_web_application_ensure_for_app_info (GAppInfo *app_info) { char *profile_dir; const char *cmd; char *address; profile_dir = ephy_web_application_get_profile_directory (g_app_info_get_name (app_info)); if (g_mkdir (profile_dir, 488) == -1) { if (errno == EEXIST) return profile_dir; g_free (profile_dir); return NULL; } /* The address should be the last command line argument in the desktop file */ cmd = g_app_info_get_commandline (app_info); if (!cmd) { g_free (profile_dir); return NULL; } address = g_strrstr (cmd, " "); if (!address) { g_free (profile_dir); return NULL; } address++; if (*address == '\0') { g_free (profile_dir); return NULL; } create_cookie_jar_for_domain (address, profile_dir); return profile_dir; } void ephy_web_application_setup_from_profile_directory (const char *profile_directory) { char *app_name; char *app_icon; g_return_if_fail (profile_directory != NULL); app_name = g_strrstr (profile_directory, EPHY_WEB_APP_PREFIX); if (!app_name) return; /* Skip the 'app-' part */ app_name += strlen (EPHY_WEB_APP_PREFIX); g_set_prgname (app_name); g_set_application_name (app_name); app_icon = g_build_filename (profile_directory, EPHY_WEB_APP_ICON_NAME, NULL); gtk_window_set_default_icon_from_file (app_icon, NULL); g_free (app_icon); /* We need to re-set this because we have already parsed the * options, which inits GTK+ and sets this as a side effect. */ gdk_set_program_class (app_name); } void ephy_web_application_setup_from_desktop_file (GDesktopAppInfo *desktop_info) { GAppInfo *app_info; const char *app_name; const char *wm_class; GIcon *icon; g_return_if_fail (G_IS_DESKTOP_APP_INFO (desktop_info)); app_info = G_APP_INFO (desktop_info); app_name = g_app_info_get_name (app_info); if (!app_name) return; g_set_prgname (app_name); g_set_application_name (app_name); icon = g_app_info_get_icon (app_info); if (G_IS_FILE_ICON (icon)) { GFile *file = g_file_icon_get_file (G_FILE_ICON (icon)); char *path = file ? g_file_get_path (file) : NULL; if (path) { gtk_window_set_default_icon_from_file (path, NULL); g_free (path); } g_clear_object (&file); } else if (G_IS_THEMED_ICON (icon)) { const char * const *names = g_themed_icon_get_names (G_THEMED_ICON (icon)); if (names) gtk_window_set_default_icon_name (names[0]); } g_clear_object (&icon); /* We need to re-set this because we have already parsed the * options, which inits GTK+ and sets this as a side effect. */ wm_class = g_desktop_app_info_get_startup_wm_class (desktop_info); if (wm_class) gdk_set_program_class (wm_class); } /** * ephy_web_application_get_application_list: * * Gets a list of the currently installed web applications. * Free the returned GList with * ephy_web_application_free_application_list. * * Returns: (transfer-full): a #GList of #EphyWebApplication objects **/ GList * ephy_web_application_get_application_list (void) { GFileEnumerator *children = NULL; GFileInfo *info; GList *applications = NULL; GFile *dot_dir; char *default_dot_dir; default_dot_dir = !ephy_dot_dir_is_default () ? ephy_default_dot_dir () : NULL; dot_dir = g_file_new_for_path (default_dot_dir ? default_dot_dir : ephy_dot_dir ()); children = g_file_enumerate_children (dot_dir, "standard::name", 0, NULL, NULL); g_object_unref (dot_dir); info = g_file_enumerator_next_file (children, NULL, NULL); while (info) { EphyWebApplication *app; const char *name; glong prefix_length = g_utf8_strlen (EPHY_WEB_APP_PREFIX, -1); name = g_file_info_get_name (info); if (g_str_has_prefix (name, EPHY_WEB_APP_PREFIX)) { char *profile_dir; guint64 created; GDate *date; char *desktop_file, *desktop_file_path; char *contents; GFileInfo *desktop_info; app = g_slice_new0 (EphyWebApplication); profile_dir = g_build_filename (default_dot_dir ? default_dot_dir : ephy_dot_dir (), name, NULL); app->icon_url = g_build_filename (profile_dir, EPHY_WEB_APP_ICON_NAME, NULL); desktop_file = g_strconcat (name + prefix_length, ".desktop", NULL); desktop_file_path = g_build_filename (profile_dir, desktop_file, NULL); app->desktop_file = g_strdup (desktop_file); if (g_file_get_contents (desktop_file_path, &contents, NULL, NULL)) { char *exec; char **strings; GKeyFile *key; int i; GFile *file; key = g_key_file_new (); g_key_file_load_from_data (key, contents, -1, 0, NULL); app->name = g_key_file_get_string (key, "Desktop Entry", "Name", NULL); exec = g_key_file_get_string (key, "Desktop Entry", "Exec", NULL); strings = g_strsplit (exec, " ", -1); for (i = 0; strings[i]; i++); app->url = g_strdup (strings[i - 1]); g_strfreev (strings); g_free (exec); g_key_file_free (key); file = g_file_new_for_path (desktop_file_path); /* FIXME: this should use TIME_CREATED but it does not seem to be working. */ desktop_info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED, 0, NULL, NULL); created = g_file_info_get_attribute_uint64 (desktop_info, G_FILE_ATTRIBUTE_TIME_MODIFIED); date = g_date_new (); g_date_set_time_t (date, (time_t)created); g_date_strftime (app->install_date, 127, "%x", date); g_date_free (date); g_object_unref (file); g_object_unref (desktop_info); applications = g_list_append (applications, app); } g_free (contents); g_free (desktop_file); g_free (profile_dir); g_free (desktop_file_path); } g_object_unref (info); info = g_file_enumerator_next_file (children, NULL, NULL); } g_object_unref (children); g_free (default_dot_dir); return applications; } static void ephy_web_application_free (EphyWebApplication *app) { g_free (app->name); g_free (app->icon_url); g_free (app->url); g_free (app->desktop_file); g_slice_free (EphyWebApplication, app); } /** * ephy_web_application_free_application_list: * @list: an #EphyWebApplication GList * * Frees a @list as given by ephy_web_application_get_application_list. **/ void ephy_web_application_free_application_list (GList *list) { GList *p; for (p = list; p; p = p->next) ephy_web_application_free ((EphyWebApplication *)p->data); g_list_free (list); } /** * ephy_web_application_exists: * @name: the potential name of the web application * * Returns: whether an application with @name exists. **/ gboolean ephy_web_application_exists (const char *name) { char *profile_dir; gboolean profile_exists; profile_dir = ephy_web_application_get_profile_directory (name); profile_exists = g_file_test (profile_dir, G_FILE_TEST_IS_DIR); g_free (profile_dir); return profile_exists; }