diff options
-rw-r--r-- | AUTHORS | 11 | ||||
-rw-r--r-- | Makefile.am | 3 | ||||
-rw-r--r-- | configure.ac | 7 | ||||
-rw-r--r-- | po/POTFILES.in | 3 | ||||
-rw-r--r-- | share/Makefile.am | 28 | ||||
-rw-r--r-- | share/caja-share.c | 1284 | ||||
-rw-r--r-- | share/caja-share.h | 65 | ||||
-rw-r--r-- | share/mate-file-manager-share.c | 1284 | ||||
-rw-r--r-- | share/mate-file-manager-share.h | 65 | ||||
-rw-r--r-- | share/share-dialog.ui | 301 | ||||
-rw-r--r-- | share/shares.c | 1023 | ||||
-rw-r--r-- | share/shares.h | 47 |
12 files changed, 4120 insertions, 1 deletions
@@ -22,3 +22,14 @@ Roberto Majadas <[email protected]> This module it's highly influenced by file-roller module Thanks Paolo Bacchilega :) Bluetooth plugin: Bastien Nocera <[email protected]> + +nautilus-share: +Sebastien Estienne <[email protected]> +Federico Mena Quintero <[email protected]> +Ed Catmur <[email protected]> : dbus 0.3x API +VoJcEK <[email protected]> : polish translation +Michael Kanis <[email protected]> : german translation +WareKala <[email protected]> : finnish translation +Mirko Maischberger <[email protected]> : italian translation +Aaron Kurtz <[email protected]> : Fedora Core 4 package +Lucius Curado <[email protected]> : brazilian translation diff --git a/Makefile.am b/Makefile.am index f7defe6..c0a4f55 100644 --- a/Makefile.am +++ b/Makefile.am @@ -4,7 +4,8 @@ SUBDIRS = \ gksu \ image-converter \ open-terminal \ - sendto + sendto \ + share EXTRA_DIST = \ intltool-extract.in \ diff --git a/configure.ac b/configure.ac index 41141cb..66da57a 100644 --- a/configure.ac +++ b/configure.ac @@ -71,6 +71,12 @@ AC_SUBST(SENDTO_CFLAGS) AC_SUBST(SENDTO_LIBS) AC_DEFINE_UNQUOTED(GAJIM_SHARE_DIR, "$prefix/share/gajim", [path to gajim share dir]) +# share extension +PKG_CHECK_MODULES(SHARE, + glib-2.0 >= $GLIB_REQUIRED) +AC_SUBST(SHARE_CFLAGS) +AC_SUBST(SHARE_LIBS) + # Get caja extensions directory CAJA_EXTENSION_DIR=`$PKG_CONFIG --variable=extensiondir libcaja-extension` AC_SUBST(CAJA_EXTENSION_DIR) @@ -98,6 +104,7 @@ AC_OUTPUT([ sendto/plugins/pidgin/Makefile sendto/plugins/removable-devices/Makefile sendto/plugins/upnp/Makefile + share/Makefile po/Makefile.in ]) diff --git a/po/POTFILES.in b/po/POTFILES.in index 102d299..40d92aa 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -20,3 +20,6 @@ sendto/plugins/caja-burn/caja-burn.c sendto/plugins/pidgin/pidgin.c sendto/plugins/removable-devices/removable-devices.c sendto/plugins/upnp/upnp.c +share/mate-file-manager-share.c +[type: gettext/glade]share/share-dialog.ui +share/shares.c diff --git a/share/Makefile.am b/share/Makefile.am new file mode 100644 index 0000000..3a9e28c --- /dev/null +++ b/share/Makefile.am @@ -0,0 +1,28 @@ +interfacesdir = $(datadir)/caja-share +interfaces_DATA = share-dialog.ui +EXTRA_DIST = share-dialog.ui + +libcaja_share_la_CFLAGS = \ + -Wall \ + -DG_LOG_DOMAIN=\"Caja-Share\" \ + -DINTERFACES_DIR=\"$(interfacesdir)\" \ + -DDATADIR=\"$(datadir)\" \ + -DMATELOCALEDIR=\""$(datadir)/locale"\" \ + -I$(top_srcdir) \ + -I$(top_builddir) \ + $(WARN_CFLAGS) \ + $(DISABLE_DEPRECATED_CFLAGS) \ + $(CAJA_CFLAGS) \ + $(SHARE_CFLAGS) + +caja_extensiondir=$(CAJA_EXTENSION_DIR) +caja_extension_LTLIBRARIES=libcaja-share.la + +libcaja_share_la_SOURCES = \ + mate-file-manager-share.c \ + mate-file-manager-share.h \ + shares.c \ + shares.h + +libcaja_share_la_LDFLAGS = -module -avoid-version +libcaja_share_la_LIBADD = $(SHARE_LIBS) $(CAJA_LIBS) diff --git a/share/caja-share.c b/share/caja-share.c new file mode 100644 index 0000000..986f48d --- /dev/null +++ b/share/caja-share.c @@ -0,0 +1,1284 @@ +/* mate-file-manager-share -- Caja File Sharing Extension + * + * Sebastien Estienne <[email protected]> + * + * 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 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + * (C) Copyright 2005 Ethium, Inc. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <libcaja-extension/caja-extension-types.h> +#include <libcaja-extension/caja-column-provider.h> +#include <libcaja-extension/caja-extension-types.h> +#include <libcaja-extension/caja-file-info.h> +#include <libcaja-extension/caja-info-provider.h> +#include <libcaja-extension/caja-menu-provider.h> +#include <libcaja-extension/caja-property-page-provider.h> + +#include "mate-file-manager-share.h" + +#include <glib/gi18n-lib.h> + +#include <gio/gio.h> + +#include <gtk/gtk.h> + +#include <string.h> +#include <time.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <stdlib.h> + +#include "shares.h" + + +#define NEED_IF_GUESTOK_MASK (S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) /* need go+rx for guest enabled usershares */ +#define NEED_IF_WRITABLE_MASK (S_IWGRP | S_IWOTH) /* writable usershares need go+w additionally*/ +#define NEED_ALL_MASK (NEED_IF_GUESTOK_MASK | NEED_IF_WRITABLE_MASK) + +static GObjectClass *parent_class; + +/* Structure to hold all the information for a share's property page. If + * you add stuff to this, add it to free_property_page_cb() as well. + */ +typedef struct { + char *path; /* Full path which is being shared */ + CajaFileInfo *fileinfo; /* Caja file to which this page refers */ + + GtkBuilder *xml; + + GtkWidget *main; /* Widget that holds all the rest. Its "PropertyPage" GObject-data points to this PropertyPage structure */ + + GtkWidget *checkbutton_share_folder; + GtkWidget *hbox_share_name; + GtkWidget *hbox_share_comment; + GtkWidget *entry_share_name; + GtkWidget *checkbutton_share_rw_ro; + GtkWidget *checkbutton_share_guest_ok; + GtkWidget *entry_share_comment; + GtkWidget *label_status; + GtkWidget *button_cancel; + GtkWidget *button_apply; + + GtkWidget *standalone_window; + + gboolean was_initially_shared; + gboolean was_writable; + gboolean is_dirty; +} PropertyPage; + +static void property_page_set_warning (PropertyPage *page); +static void property_page_set_error (PropertyPage *page, const char *message); +static void property_page_set_normal (PropertyPage *page); + +static void +property_page_validate_fields (PropertyPage *page) +{ + const char *name; + + name = gtk_entry_get_text (GTK_ENTRY (page->entry_share_name)); + + if (g_utf8_strlen (name, -1) <= 12) + property_page_set_normal (page); + else + property_page_set_warning (page); +} + +static gboolean +message_confirm_missing_permissions (GtkWidget *widget, const char *path, mode_t need_mask) +{ + GtkWidget *toplevel; + GtkWidget *dialog; + char *display_name; + gboolean result; + + toplevel = gtk_widget_get_toplevel (widget); + if (!GTK_IS_WINDOW (toplevel)) + toplevel = NULL; + + display_name = g_filename_display_basename (path); + + dialog = gtk_message_dialog_new (toplevel ? GTK_WINDOW (toplevel) : NULL, + 0, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + _("Caja needs to add some permissions to your folder \"%s\" in order to share it"), + display_name); + + /* FIXME: the following message only mentions "permission by others". We + * should probably be more explicit and mention group/other permissions. + * We'll be able to do that after the period of string freeze. + */ + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + _("The folder \"%s\" needs the following extra permissions for sharing to work:\n" + "%s%s%s" + "Do you want Caja to add these permissions to the folder automatically?"), + display_name, + (need_mask & (S_IRGRP | S_IROTH)) ? _(" - read permission by others\n") : "", + (need_mask & (S_IWGRP | S_IWOTH)) ? _(" - write permission by others\n") : "", + (need_mask & (S_IXGRP | S_IXOTH)) ? _(" - execute permission by others\n") : ""); + g_free (display_name); + + gtk_dialog_add_button (GTK_DIALOG (dialog), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("Add the permissions automatically"), GTK_RESPONSE_ACCEPT); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); + + result = gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT; + gtk_widget_destroy (dialog); + + return result; +} + +static void +error_when_changing_permissions (GtkWidget *widget, const char *path) +{ + GtkWidget *toplevel; + GtkWidget *dialog; + char *display_name; + + toplevel = gtk_widget_get_toplevel (widget); + if (!GTK_IS_WINDOW (toplevel)) + toplevel = NULL; + + display_name = g_filename_display_basename (path); + + dialog = gtk_message_dialog_new (toplevel ? GTK_WINDOW (toplevel) : NULL, + 0, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + _("Could not change the permissions of folder \"%s\""), + display_name); + g_free (display_name); + + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); +} + +static char * +get_key_file_path (void) +{ + return g_build_filename (g_get_home_dir (), ".mate2", "mate-file-manager-share-modified-permissions", NULL); +} + +static void +save_key_file (const char *filename, GKeyFile *key_file) +{ + char *contents; + gsize length; + + /* NULL GError */ + contents = g_key_file_to_data (key_file, &length, NULL); + if (!contents) + return; + + /* NULL GError */ + g_file_set_contents (filename, contents, length, NULL); + + g_free (contents); +} + +static void +save_changed_permissions (const char *path, mode_t need_mask) +{ + GKeyFile *key_file; + char *key_file_path; + char str[50]; + + key_file = g_key_file_new (); + key_file_path = get_key_file_path (); + + /* NULL GError + * + * We don't check the return value of this. If the file doesn't exist, we'll + * simply want to create it. + */ + g_key_file_load_from_file (key_file, key_file_path, 0, NULL); + + g_snprintf (str, sizeof (str), "%o", (guint) need_mask); /* octal, baby */ + g_key_file_set_string (key_file, path, "need_mask", str); + + save_key_file (key_file_path, key_file); + + g_key_file_free (key_file); + g_free (key_file_path); +} + +static void +remove_permissions (const char *path, mode_t need_mask) +{ + struct stat st; + mode_t new_mode; + + if (need_mask == 0) + return; + + if (stat (path, &st) != 0) + return; + + new_mode = st.st_mode & ~need_mask; + + /* Bleah, no error checking */ + chmod (path, new_mode); +} + +static void +remove_from_saved_permissions (const char *path, mode_t remove_mask) +{ + GKeyFile *key_file; + char *key_file_path; + + if (remove_mask == 0) + return; + + key_file = g_key_file_new (); + key_file_path = get_key_file_path (); + + if (g_key_file_load_from_file (key_file, key_file_path, 0, NULL)) + { + mode_t need_mask; + mode_t remove_from_current_mask; + char *str; + + need_mask = 0; + + /* NULL GError */ + str = g_key_file_get_string (key_file, path, "need_mask", NULL); + + if (str) + { + guint i; + + if (sscanf (str, "%o", &i) == 1) /* octal */ + need_mask = i; + + g_free (str); + } + + remove_from_current_mask = need_mask & remove_mask; + remove_permissions (path, remove_from_current_mask); + + need_mask &= ~remove_mask; + + if (need_mask == 0) + { + /* NULL GError */ + g_key_file_remove_group (key_file, path, NULL); + } + else + { + char buf[50]; + + g_snprintf (buf, sizeof (buf), "%o", (guint) need_mask); /* octal */ + g_key_file_set_string (key_file, path, "need_mask", buf); + } + + save_key_file (key_file_path, key_file); + } + + g_key_file_free (key_file); + g_free (key_file_path); +} + +static void +restore_saved_permissions (const char *path) +{ + remove_from_saved_permissions (path, NEED_ALL_MASK); +} + +static void +restore_write_permissions (const char *path) +{ + remove_from_saved_permissions (path, NEED_IF_WRITABLE_MASK); +} + +typedef enum { + CONFIRM_CANCEL_OR_ERROR, + CONFIRM_NO_MODIFICATIONS, + CONFIRM_MODIFIED +} ConfirmPermissionsStatus; + +static ConfirmPermissionsStatus +confirm_sharing_permissions (GtkWidget *widget, const char *path, gboolean is_shared, gboolean guest_ok, gboolean is_writable) +{ + struct stat st; + mode_t mode, new_mode, need_mask; + + if (!is_shared) + return CONFIRM_NO_MODIFICATIONS; + + if (stat (path, &st) != 0) + return CONFIRM_NO_MODIFICATIONS; /* We'll just let "net usershare" give back an error if the file disappears */ + + new_mode = mode = st.st_mode; + + if (guest_ok) + new_mode |= NEED_IF_GUESTOK_MASK; + if (is_writable) + new_mode |= NEED_IF_WRITABLE_MASK; + + need_mask = new_mode & ~mode; + + if (need_mask != 0) + { + g_assert (mode != new_mode); + + if (!message_confirm_missing_permissions (widget, path, need_mask)) + return CONFIRM_CANCEL_OR_ERROR; + + if (chmod (path, new_mode) != 0) + { + error_when_changing_permissions (widget, path); + return CONFIRM_CANCEL_OR_ERROR; + } + + save_changed_permissions (path, need_mask); + + return CONFIRM_MODIFIED; + } + else + { + g_assert (mode == new_mode); + return CONFIRM_NO_MODIFICATIONS; + } + + g_assert_not_reached (); + return CONFIRM_CANCEL_OR_ERROR; +} + +static gboolean +property_page_commit (PropertyPage *page) +{ + gboolean is_shared; + ShareInfo share_info; + ConfirmPermissionsStatus status; + GError *error; + gboolean retval; + + is_shared = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_folder)); + + share_info.path = page->path; + share_info.share_name = (char *) gtk_entry_get_text (GTK_ENTRY (page->entry_share_name)); + share_info.comment = (char *) gtk_entry_get_text (GTK_ENTRY (page->entry_share_comment)); + share_info.is_writable = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_rw_ro)); + share_info.guest_ok = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_guest_ok)); + + /* Do we need to unset the write permissions that we added in the past? */ + if (is_shared && page->was_writable && !share_info.is_writable) + restore_write_permissions (page->path); + + status = confirm_sharing_permissions (page->main, page->path, is_shared, share_info.guest_ok, share_info.is_writable); + if (status == CONFIRM_CANCEL_OR_ERROR) + return FALSE; /* the user didn't want us to change his folder's permissions */ + + error = NULL; + retval = shares_modify_share (share_info.path, is_shared ? &share_info : NULL, &error); + + if (!retval) + { + property_page_set_error (page, error->message); + g_error_free (error); + + /* Since the operation failed, we restore things to the way they were */ + if (status == CONFIRM_MODIFIED) + restore_saved_permissions (page->path); + } + else + { + property_page_validate_fields (page); + caja_file_info_invalidate_extension_info (page->fileinfo); + } + + if (!is_shared) + restore_saved_permissions (page->path); + + /* update initially shared state, so that we may undo later on */ + if (retval) + { + page->was_initially_shared = is_shared; + page->is_dirty = FALSE; + } + + return retval; +} + +/*--------------------------------------------------------------------------*/ +static gchar * +get_fullpath_from_fileinfo(CajaFileInfo *fileinfo) +{ + GFile *file; + gchar *fullpath; + + g_assert (fileinfo != NULL); + + file = caja_file_info_get_location(fileinfo); + fullpath = g_file_get_path(file); + g_assert (fullpath != NULL && g_file_is_native(file)); /* In the beginning we checked that this was a local URI */ + g_object_unref(file); + + return(fullpath); +} + + +/*--------------------------------------------------------------------------*/ +static void +property_page_set_warning (PropertyPage *page) +{ + GdkColor colorYellow; + + gtk_label_set_text (GTK_LABEL (page->label_status), _("Share name is too long")); + + gdk_color_parse ("#ECDF62", &colorYellow); + gtk_widget_modify_base (page->entry_share_name, GTK_STATE_NORMAL, &colorYellow); +} + + +static void +property_page_set_error (PropertyPage *page, const char *message) +{ + GdkColor colorRed; + + gtk_label_set_text (GTK_LABEL (page->label_status), message); + + gdk_color_parse ("#C1665A", &colorRed); + gtk_widget_modify_base (page->entry_share_name, GTK_STATE_NORMAL, &colorRed); +} + +static void +property_page_set_normal (PropertyPage *page) +{ + gtk_label_set_text (GTK_LABEL (page->label_status), ""); + gtk_widget_modify_base (page->entry_share_name, GTK_STATE_NORMAL, NULL); +} + +static gboolean +property_page_share_name_is_valid (PropertyPage *page) +{ + const char *newname; + + newname = gtk_entry_get_text (GTK_ENTRY (page->entry_share_name)); + + if (strlen (newname) == 0) + { + property_page_set_error (page, _("The share name cannot be empty")); + return FALSE; + } + else + { + GError *error; + gboolean exists; + + error = NULL; + if (!shares_get_share_name_exists (newname, &exists, &error)) + { + char *str; + + str = g_strdup_printf (_("Error while getting share information: %s"), error->message); + property_page_set_error (page, str); + g_free (str); + g_error_free (error); + + return FALSE; + } + + if (exists) + { + property_page_set_error (page, _("Another share has the same name")); + return FALSE; + } + else + { + property_page_set_normal (page); + return TRUE; + } + } +} + +static void +property_page_set_controls_sensitivity (PropertyPage *page, + gboolean sensitive) +{ + gtk_widget_set_sensitive (page->entry_share_name, sensitive); + gtk_widget_set_sensitive (page->entry_share_comment, sensitive); + gtk_widget_set_sensitive (page->hbox_share_comment, sensitive); + gtk_widget_set_sensitive (page->hbox_share_name, sensitive); + gtk_widget_set_sensitive (page->checkbutton_share_rw_ro, sensitive); + + if (sensitive) + { + gboolean guest_ok_allowed; + shares_supports_guest_ok (&guest_ok_allowed, NULL); + gtk_widget_set_sensitive (page->checkbutton_share_guest_ok, guest_ok_allowed); + } + else + gtk_widget_set_sensitive (page->checkbutton_share_guest_ok, FALSE); +} + +static void +property_page_check_sensitivity (PropertyPage *page) +{ + gboolean enabled; + gboolean apply_is_sensitive; + + enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_folder)); + property_page_set_controls_sensitivity (page, enabled); + + if (enabled) + apply_is_sensitive = page->is_dirty || !page->was_initially_shared; + else + apply_is_sensitive = page->was_initially_shared; + + gtk_widget_set_sensitive (page->button_apply, apply_is_sensitive); + gtk_button_set_label (GTK_BUTTON(page->button_apply), + page->was_initially_shared ? _("Modify _Share") : _("Create _Share")); +} + +static void +modify_share_name_text_entry (GtkEditable *editable, + gpointer user_data) +{ + PropertyPage *page; + + page = user_data; + + page->is_dirty = TRUE; + + /* This function does simple validation on the share name and sets the error + * label; just let it run and ignore the result value. + */ + property_page_share_name_is_valid (page); + + property_page_check_sensitivity (page); +} + +static void +modify_share_comment_text_entry (GtkEditable *editable, + gpointer user_data) +{ + PropertyPage *page; + + page = user_data; + + page->is_dirty = TRUE; + property_page_check_sensitivity (page); +} + +/*--------------------------------------------------------------------------*/ +static void +on_checkbutton_share_folder_toggled (GtkToggleButton *togglebutton, + gpointer user_data) +{ + PropertyPage *page; + + page = user_data; + + property_page_check_sensitivity (page); +} + +static void +on_checkbutton_rw_ro_toggled (GtkToggleButton *togglebutton, + gpointer user_data) +{ + PropertyPage *page; + + page = user_data; + + page->is_dirty = TRUE; + + property_page_check_sensitivity (page); +} + +static void +on_checkbutton_guest_ok_toggled (GtkToggleButton *togglebutton, + gpointer user_data) +{ + PropertyPage *page; + + page = user_data; + + page->is_dirty = TRUE; + + property_page_check_sensitivity (page); +} + +static void +free_property_page_cb (gpointer data) +{ + PropertyPage *page; + + page = data; + + g_free (page->path); + g_object_unref (page->fileinfo); + g_object_unref (page->xml); + + g_free (page); +} + +static void +button_apply_clicked_cb (GtkButton *button, + gpointer data) +{ + PropertyPage *page; + + page = data; + + if (property_page_commit (page)) + { + if (page->standalone_window) + gtk_widget_destroy (page->standalone_window); + else + property_page_check_sensitivity (page); + } +} + +/*--------------------------------------------------------------------------*/ +static PropertyPage * +create_property_page (CajaFileInfo *fileinfo) +{ + PropertyPage *page; + GError *error; + ShareInfo *share_info; + char *share_name; + gboolean free_share_name; + const char *comment; + char *apply_button_label; + + page = g_new0 (PropertyPage, 1); + + page->path = get_fullpath_from_fileinfo(fileinfo); + page->fileinfo = g_object_ref (fileinfo); + + error = NULL; + if (!shares_get_share_info_for_path (page->path, &share_info, &error)) + { + /* We'll assume that there is no share for that path, but we'll still + * bring up an error dialog. + */ + GtkWidget *message; + + message = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + _("There was an error while getting the sharing information")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (message), "%s", error->message); + gtk_widget_show (message); + + share_info = NULL; + g_error_free (error); + error = NULL; + } + + + page->xml = gtk_builder_new (); + gtk_builder_set_translation_domain (page->xml, "mate-file-manager-share"); + g_assert (gtk_builder_add_from_file (page->xml, + INTERFACES_DIR"/share-dialog.ui", &error)); + + page->main = GTK_WIDGET (gtk_builder_get_object (page->xml, "vbox1")); + g_assert (page->main != NULL); + + g_object_set_data_full (G_OBJECT (page->main), + "PropertyPage", + page, + free_property_page_cb); + + page->checkbutton_share_folder = GTK_WIDGET (gtk_builder_get_object (page->xml,"checkbutton_share_folder")); + page->hbox_share_comment = GTK_WIDGET (gtk_builder_get_object (page->xml,"hbox_share_comment")); + page->hbox_share_name = GTK_WIDGET (gtk_builder_get_object (page->xml,"hbox_share_name")); + page->checkbutton_share_rw_ro = GTK_WIDGET (gtk_builder_get_object (page->xml,"checkbutton_share_rw_ro")); + page->checkbutton_share_guest_ok = GTK_WIDGET (gtk_builder_get_object (page->xml,"checkbutton_share_guest_ok")); + page->entry_share_name = GTK_WIDGET (gtk_builder_get_object (page->xml,"entry_share_name")); + page->entry_share_comment = GTK_WIDGET (gtk_builder_get_object (page->xml,"entry_share_comment")); + page->label_status = GTK_WIDGET (gtk_builder_get_object (page->xml,"label_status")); + page->button_cancel = GTK_WIDGET (gtk_builder_get_object (page->xml,"button_cancel")); + page->button_apply = GTK_WIDGET (gtk_builder_get_object (page->xml,"button_apply")); + + /* Sanity check so that we don't screw up the Glade file */ + g_assert (page->checkbutton_share_folder != NULL + && page->hbox_share_comment != NULL + && page->hbox_share_name != NULL + && page->checkbutton_share_rw_ro != NULL + && page->checkbutton_share_guest_ok != NULL + && page->entry_share_name != NULL + && page->entry_share_comment != NULL + && page->label_status != NULL + && page->button_cancel != NULL + && page->button_apply != NULL); + + if (share_info) + { + page->was_initially_shared = TRUE; + page->was_writable = share_info->is_writable; + } + + /* Share name */ + + if (share_info) + { + share_name = share_info->share_name; + free_share_name = FALSE; + } + else + { + share_name = g_filename_display_basename (page->path); + free_share_name = TRUE; + } + + gtk_entry_set_text (GTK_ENTRY (page->entry_share_name), share_name); + + if (free_share_name) + g_free (share_name); + + /* Comment */ + + if (share_info == NULL || share_info->comment == NULL) + comment = ""; + else + comment = share_info->comment; + + gtk_entry_set_text (GTK_ENTRY (page->entry_share_comment), comment); + + /* Share toggle */ + + if (share_info) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_folder), TRUE); + else + { + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_folder), FALSE); + } + + /* Share name */ + + if (g_utf8_strlen(gtk_entry_get_text (GTK_ENTRY (page->entry_share_name)), -1) > 12) + property_page_set_warning (page); + + /* Permissions */ + if (share_info != NULL && share_info->is_writable) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_rw_ro), TRUE); + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_rw_ro), FALSE); + + /* Guest access */ + if (share_info != NULL && share_info->guest_ok) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_guest_ok), TRUE); + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_guest_ok), FALSE); + + /* Apply button */ + + if (share_info) + apply_button_label = _("Modify _Share"); + else + apply_button_label = _("Create _Share"); + + gtk_button_set_label (GTK_BUTTON (page->button_apply), apply_button_label); + gtk_button_set_use_underline (GTK_BUTTON (page->button_apply), TRUE); + gtk_button_set_image (GTK_BUTTON (page->button_apply), gtk_image_new_from_stock (GTK_STOCK_SAVE, GTK_ICON_SIZE_BUTTON)); + + gtk_widget_set_sensitive (page->button_apply, FALSE); + + /* Sensitivity */ + + property_page_check_sensitivity (page); + + /* Signal handlers */ + + g_signal_connect (page->checkbutton_share_folder, "toggled", + G_CALLBACK (on_checkbutton_share_folder_toggled), + page); + + g_signal_connect (page->checkbutton_share_rw_ro, "toggled", + G_CALLBACK (on_checkbutton_rw_ro_toggled), + page); + + g_signal_connect (page->checkbutton_share_guest_ok, "toggled", + G_CALLBACK (on_checkbutton_guest_ok_toggled), + page); + + g_signal_connect (page->entry_share_name, "changed", + G_CALLBACK (modify_share_name_text_entry), + page); + + g_signal_connect (page->entry_share_comment, "changed", + G_CALLBACK (modify_share_comment_text_entry), + page); + + g_signal_connect (page->button_apply, "clicked", + G_CALLBACK (button_apply_clicked_cb), page); + + if (share_info != NULL) + shares_free_share_info (share_info); + + return page; +} + +/* Implementation of the CajaInfoProvider interface */ + +/* caja_info_provider_update_file_info + * This function is called by Caja when it wants the extension to + * fill in data about the file. It passes a CajaFileInfo object, + * which the extension can use to read data from the file, and which + * the extension should add data to. + * + * If the data can be added immediately (without doing blocking IO), + * the extension can do so, and return CAJA_OPERATION_COMPLETE. + * In this case the 'update_complete' and 'handle' parameters can be + * ignored. + * + * If waiting for the deata would block the UI, the extension should + * perform the task asynchronously, and return + * CAJA_OPERATION_IN_PROGRESS. The function must also set the + * 'handle' pointer to a value unique to the object, and invoke the + * 'update_complete' closure when the update is done. + * + * If the extension encounters an error, it should return + * CAJA_OPERATION_FAILED. + */ +typedef struct { + gboolean cancelled; + CajaInfoProvider *provider; + CajaFileInfo *file; + GClosure *update_complete; +} CajaShareHandle; + +static CajaShareStatus +get_share_status_and_free_share_info (ShareInfo *share_info) +{ + CajaShareStatus result; + + if (!share_info) + result = CAJA_SHARE_NOT_SHARED; + else + { + if (share_info->is_writable) + result = CAJA_SHARE_SHARED_RW; + else + result = CAJA_SHARE_SHARED_RO; + + shares_free_share_info (share_info); + } + + return result; +} + + +/*--------------------------------------------------------------------------*/ +static void +get_share_info_for_file_info (CajaFileInfo *file, ShareInfo **share_info, gboolean *is_shareable) +{ + char *uri; + char *local_path = NULL; + GFile *f; + + *share_info = NULL; + *is_shareable = FALSE; + + uri = caja_file_info_get_uri (file); + f = caja_file_info_get_location(file); + if (!uri) + goto out; + +#define NETWORK_SHARE_PREFIX "network:///share-" + + if (g_str_has_prefix (uri, NETWORK_SHARE_PREFIX)) + { + const char *share_name; + + share_name = uri + strlen (NETWORK_SHARE_PREFIX); + + /* FIXME: NULL GError */ + if (!shares_get_share_info_for_share_name (share_name, share_info, NULL)) + { + *share_info = NULL; + *is_shareable = TRUE; /* it *has* the prefix, anyway... we are just unsynchronized with what mate-vfs thinks */ + } + else + { + *is_shareable = TRUE; + } + + goto out; + } + + if (!caja_file_info_is_directory(file)) + goto out; + + local_path = g_file_get_path(f); + if (!local_path || !g_file_is_native(f)) + goto out; + + /* FIXME: NULL GError */ + if (!shares_get_share_info_for_path (local_path, share_info, NULL)) + goto out; + + *is_shareable = TRUE; + + out: + + g_object_unref(f); + g_free (uri); + g_free (local_path); +} + +/*--------------------------------------------------------------------------*/ +static CajaShareStatus +file_get_share_status_file(CajaFileInfo *file) +{ + ShareInfo *share_info; + gboolean is_shareable; + + get_share_info_for_file_info (file, &share_info, &is_shareable); + + if (!is_shareable) + return CAJA_SHARE_NOT_SHARED; + + return get_share_status_and_free_share_info (share_info); +} + +static CajaOperationResult +caja_share_update_file_info (CajaInfoProvider *provider, + CajaFileInfo *file, + GClosure *update_complete, + CajaOperationHandle **handle) +{ +/* gchar *share_status = NULL; */ + + switch (file_get_share_status_file (file)) { + + case CAJA_SHARE_SHARED_RO: + caja_file_info_add_emblem (file, "shared"); +/* share_status = _("shared (read only)"); */ + break; + + case CAJA_SHARE_SHARED_RW: + caja_file_info_add_emblem (file, "shared"); +/* share_status = _("shared (read and write)"); */ + break; + + case CAJA_SHARE_NOT_SHARED: +/* share_status = _("not shared"); */ + break; + + default: + g_assert_not_reached (); + break; + } + +/* caja_file_info_add_string_attribute (file, */ +/* "CajaShare::share_status", */ +/* share_status); */ + return CAJA_OPERATION_COMPLETE; +} + + +static void +caja_share_cancel_update (CajaInfoProvider *provider, + CajaOperationHandle *handle) +{ + CajaShareHandle *share_handle; + + share_handle = (CajaShareHandle*)handle; + share_handle->cancelled = TRUE; +} + +static void +caja_share_info_provider_iface_init (CajaInfoProviderIface *iface) +{ + iface->update_file_info = caja_share_update_file_info; + iface->cancel_update = caja_share_cancel_update; +} + +/*--------------------------------------------------------------------------*/ +/* caja_property_page_provider_get_pages + * + * This function is called by Caja when it wants property page + * items from the extension. + * + * This function is called in the main thread before a property page + * is shown, so it should return quickly. + * + * The function should return a GList of allocated CajaPropertyPage + * items. + */ +static GList * +caja_share_get_property_pages (CajaPropertyPageProvider *provider, + GList *files) +{ + PropertyPage *page; + GList *pages; + CajaPropertyPage *np_page; + CajaFileInfo *fileinfo; + ShareInfo *share_info; + gboolean is_shareable; + + /* Only show the property page if 1 file is selected */ + if (!files || files->next != NULL) { + return NULL; + } + + fileinfo = CAJA_FILE_INFO (files->data); + + get_share_info_for_file_info (fileinfo, &share_info, &is_shareable); + if (!is_shareable) + return NULL; + + page = create_property_page (fileinfo); + gtk_widget_hide (page->button_cancel); + + if (share_info) + shares_free_share_info (share_info); + + pages = NULL; + np_page = caja_property_page_new + ("CajaShare::property_page", + gtk_label_new (_("Share")), + page->main); + pages = g_list_append (pages, np_page); + + return pages; +} + +/*--------------------------------------------------------------------------*/ +static void +caja_share_property_page_provider_iface_init (CajaPropertyPageProviderIface *iface) +{ + iface->get_pages = caja_share_get_property_pages; +} + +/*--------------------------------------------------------------------------*/ +static void +caja_share_instance_init (CajaShare *share) +{ +} + +/*--------------------------------------------------------------------------*/ +static void +caja_share_class_init (CajaShareClass *class) +{ + parent_class = g_type_class_peek_parent (class); +} + +/* caja_menu_provider_get_file_items + * + * This function is called by Caja when it wants context menu + * items from the extension. + * + * This function is called in the main thread before a context menu + * is shown, so it should return quickly. + * + * The function should return a GList of allocated CajaMenuItem + * items. + */ + +static void +button_cancel_clicked_cb (GtkButton *button, gpointer data) +{ + GtkWidget *window; + + window = GTK_WIDGET (data); + gtk_widget_destroy (window); +} + +static void +share_this_folder_callback (CajaMenuItem *item, + gpointer user_data) +{ + CajaFileInfo *fileinfo; + PropertyPage *page; + GtkWidget * window; + + fileinfo = CAJA_FILE_INFO (user_data); + g_assert (fileinfo != NULL); + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_title (GTK_WINDOW (window), _("Folder Sharing")); + page = create_property_page (fileinfo); + page->standalone_window = window; + g_signal_connect (page->button_cancel, "clicked", + G_CALLBACK (button_cancel_clicked_cb), window); + + gtk_container_add (GTK_CONTAINER (window), page->main); + gtk_widget_show (window); +} + +static GList * +caja_share_get_file_items (CajaMenuProvider *provider, + GtkWidget *window, + GList *files) +{ + GList *items; + CajaMenuItem *item; + CajaFileInfo *fileinfo; + ShareInfo *share_info; + gboolean is_shareable; + + /* Only show the property page if 1 file is selected */ + if (!files || files->next != NULL) { + return NULL; + } + + fileinfo = CAJA_FILE_INFO (files->data); + + get_share_info_for_file_info (fileinfo, &share_info, &is_shareable); + + if (!is_shareable) + return NULL; + + if (share_info) + shares_free_share_info (share_info); + + /* We don't own a reference to the file info to keep it around, so acquire one */ + g_object_ref (fileinfo); + + /* FMQ: change the label to "Share with Windows users"? */ + item = caja_menu_item_new ("CajaShare::share", + _("Sharing Options"), + _("Share this Folder"), + "folder-remote"); + g_signal_connect (item, "activate", + G_CALLBACK (share_this_folder_callback), + fileinfo); + g_object_set_data_full (G_OBJECT (item), + "files", + fileinfo, + g_object_unref); /* Release our reference when the menu item goes away */ + + items = g_list_append (NULL, item); + return items; +} + +/*--------------------------------------------------------------------------*/ +static void +caja_share_menu_provider_iface_init (CajaMenuProviderIface *iface) +{ + iface->get_file_items = caja_share_get_file_items; +} + +/*--------------------------------------------------------------------------*/ +/* Type registration. Because this type is implemented in a module + * that can be unloaded, we separate type registration from get_type(). + * the type_register() function will be called by the module's + * initialization function. */ +static GType share_type = 0; + +#define CAJA_TYPE_SHARE (caja_share_get_type ()) + +static GType +caja_share_get_type (void) +{ + return share_type; +} + +static void +caja_share_register_type (GTypeModule *module) +{ + static const GTypeInfo info = { + sizeof (CajaShareClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) caja_share_class_init, + NULL, + NULL, + sizeof (CajaShare), + 0, + (GInstanceInitFunc) caja_share_instance_init, + }; + + share_type = g_type_module_register_type (module, + G_TYPE_OBJECT, + "CajaShare", + &info, 0); + + /* onglet share propri�t� */ + static const GInterfaceInfo property_page_provider_iface_info = { + (GInterfaceInitFunc) caja_share_property_page_provider_iface_init, + NULL, + NULL + }; + + g_type_module_add_interface (module, + share_type, + CAJA_TYPE_PROPERTY_PAGE_PROVIDER, + &property_page_provider_iface_info); + + + /* premier page propri�t� ? */ + static const GInterfaceInfo info_provider_iface_info = { + (GInterfaceInitFunc) caja_share_info_provider_iface_init, + NULL, + NULL + }; + + g_type_module_add_interface (module, + share_type, + CAJA_TYPE_INFO_PROVIDER, + &info_provider_iface_info); + + /* Menu right clik */ + static const GInterfaceInfo menu_provider_iface_info = { + (GInterfaceInitFunc) caja_share_menu_provider_iface_init, + NULL, + NULL + }; + + g_type_module_add_interface (module, + share_type, + CAJA_TYPE_MENU_PROVIDER, + &menu_provider_iface_info); + +} + +/* Extension module functions. These functions are defined in + * caja-extensions-types.h, and must be implemented by all + * extensions. */ + +/* Initialization function. In addition to any module-specific + * initialization, any types implemented by the module should + * be registered here. */ +void +caja_module_initialize (GTypeModule *module) +{ + /*g_print ("Initializing mate-file-manager-share extension\n");*/ + + bindtextdomain("mate-file-manager-share", CAJA_SHARE_LOCALEDIR); + bind_textdomain_codeset("mate-file-manager-share", "UTF-8"); + + caja_share_register_type (module); +} + +/* Perform module-specific shutdown. */ +void +caja_module_shutdown (void) +{ + /*g_print ("Shutting down mate-file-manager-share extension\n");*/ + /* FIXME freeing */ +} + +/* List all the extension types. */ +void +caja_module_list_types (const GType **types, + int *num_types) +{ + static GType type_list[1]; + + type_list[0] = CAJA_TYPE_SHARE; + + *types = type_list; + *num_types = 1; +} diff --git a/share/caja-share.h b/share/caja-share.h new file mode 100644 index 0000000..9907ac2 --- /dev/null +++ b/share/caja-share.h @@ -0,0 +1,65 @@ +/* mate-file-manager-share -- Caja File Sharing Extension + * + * Sebastien Estienne <[email protected]> + * + * 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 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + * (C) Copyright 2005 Ethium, Inc. + */ + +#ifndef CAJA_SHARE_H +#define CAJA_SHARE_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +/* Declarations for the Share extension object. This object will be + * instantiated by caja. It implements the GInterfaces + * exported by libcaja. */ + + +typedef struct _CajaShare CajaShare; +typedef struct _CajaShareClass CajaShareClass; + +struct _CajaShare { + GObject parent_slot; +}; + +struct _CajaShareClass { + GObjectClass parent_slot; + + /* No extra class members */ +}; + + +typedef struct _CajaShareData CajaShareData; + +struct _CajaShareData { + gchar *fullpath; + gchar *section; + CajaFileInfo *fileinfo; +}; + +G_END_DECLS + +typedef enum { + CAJA_SHARE_NOT_SHARED, + CAJA_SHARE_SHARED_RO, + CAJA_SHARE_SHARED_RW +} CajaShareStatus; + +#endif + diff --git a/share/mate-file-manager-share.c b/share/mate-file-manager-share.c new file mode 100644 index 0000000..e71e414 --- /dev/null +++ b/share/mate-file-manager-share.c @@ -0,0 +1,1284 @@ +/* mate-file-manager-share -- Caja File Sharing Extension + * + * Sebastien Estienne <[email protected]> + * + * 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 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + * (C) Copyright 2005 Ethium, Inc. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <libcaja-extension/caja-extension-types.h> +#include <libcaja-extension/caja-column-provider.h> +#include <libcaja-extension/caja-extension-types.h> +#include <libcaja-extension/caja-file-info.h> +#include <libcaja-extension/caja-info-provider.h> +#include <libcaja-extension/caja-menu-provider.h> +#include <libcaja-extension/caja-property-page-provider.h> + +#include "mate-file-manager-share.h" + +#include <glib/gi18n-lib.h> + +#include <gio/gio.h> + +#include <gtk/gtk.h> + +#include <string.h> +#include <time.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <stdlib.h> + +#include "shares.h" + + +#define NEED_IF_GUESTOK_MASK (S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) /* need go+rx for guest enabled usershares */ +#define NEED_IF_WRITABLE_MASK (S_IWGRP | S_IWOTH) /* writable usershares need go+w additionally*/ +#define NEED_ALL_MASK (NEED_IF_GUESTOK_MASK | NEED_IF_WRITABLE_MASK) + +static GObjectClass *parent_class; + +/* Structure to hold all the information for a share's property page. If + * you add stuff to this, add it to free_property_page_cb() as well. + */ +typedef struct { + char *path; /* Full path which is being shared */ + CajaFileInfo *fileinfo; /* Caja file to which this page refers */ + + GtkBuilder *xml; + + GtkWidget *main; /* Widget that holds all the rest. Its "PropertyPage" GObject-data points to this PropertyPage structure */ + + GtkWidget *checkbutton_share_folder; + GtkWidget *hbox_share_name; + GtkWidget *hbox_share_comment; + GtkWidget *entry_share_name; + GtkWidget *checkbutton_share_rw_ro; + GtkWidget *checkbutton_share_guest_ok; + GtkWidget *entry_share_comment; + GtkWidget *label_status; + GtkWidget *button_cancel; + GtkWidget *button_apply; + + GtkWidget *standalone_window; + + gboolean was_initially_shared; + gboolean was_writable; + gboolean is_dirty; +} PropertyPage; + +static void property_page_set_warning (PropertyPage *page); +static void property_page_set_error (PropertyPage *page, const char *message); +static void property_page_set_normal (PropertyPage *page); + +static void +property_page_validate_fields (PropertyPage *page) +{ + const char *name; + + name = gtk_entry_get_text (GTK_ENTRY (page->entry_share_name)); + + if (g_utf8_strlen (name, -1) <= 12) + property_page_set_normal (page); + else + property_page_set_warning (page); +} + +static gboolean +message_confirm_missing_permissions (GtkWidget *widget, const char *path, mode_t need_mask) +{ + GtkWidget *toplevel; + GtkWidget *dialog; + char *display_name; + gboolean result; + + toplevel = gtk_widget_get_toplevel (widget); + if (!GTK_IS_WINDOW (toplevel)) + toplevel = NULL; + + display_name = g_filename_display_basename (path); + + dialog = gtk_message_dialog_new (toplevel ? GTK_WINDOW (toplevel) : NULL, + 0, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + _("Caja needs to add some permissions to your folder \"%s\" in order to share it"), + display_name); + + /* FIXME: the following message only mentions "permission by others". We + * should probably be more explicit and mention group/other permissions. + * We'll be able to do that after the period of string freeze. + */ + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + _("The folder \"%s\" needs the following extra permissions for sharing to work:\n" + "%s%s%s" + "Do you want Caja to add these permissions to the folder automatically?"), + display_name, + (need_mask & (S_IRGRP | S_IROTH)) ? _(" - read permission by others\n") : "", + (need_mask & (S_IWGRP | S_IWOTH)) ? _(" - write permission by others\n") : "", + (need_mask & (S_IXGRP | S_IXOTH)) ? _(" - execute permission by others\n") : ""); + g_free (display_name); + + gtk_dialog_add_button (GTK_DIALOG (dialog), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("Add the permissions automatically"), GTK_RESPONSE_ACCEPT); + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); + + result = gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT; + gtk_widget_destroy (dialog); + + return result; +} + +static void +error_when_changing_permissions (GtkWidget *widget, const char *path) +{ + GtkWidget *toplevel; + GtkWidget *dialog; + char *display_name; + + toplevel = gtk_widget_get_toplevel (widget); + if (!GTK_IS_WINDOW (toplevel)) + toplevel = NULL; + + display_name = g_filename_display_basename (path); + + dialog = gtk_message_dialog_new (toplevel ? GTK_WINDOW (toplevel) : NULL, + 0, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + _("Could not change the permissions of folder \"%s\""), + display_name); + g_free (display_name); + + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (dialog); +} + +static char * +get_key_file_path (void) +{ + return g_build_filename (g_get_home_dir (), ".mate2", "mate-file-manager-share-modified-permissions", NULL); +} + +static void +save_key_file (const char *filename, GKeyFile *key_file) +{ + char *contents; + gsize length; + + /* NULL GError */ + contents = g_key_file_to_data (key_file, &length, NULL); + if (!contents) + return; + + /* NULL GError */ + g_file_set_contents (filename, contents, length, NULL); + + g_free (contents); +} + +static void +save_changed_permissions (const char *path, mode_t need_mask) +{ + GKeyFile *key_file; + char *key_file_path; + char str[50]; + + key_file = g_key_file_new (); + key_file_path = get_key_file_path (); + + /* NULL GError + * + * We don't check the return value of this. If the file doesn't exist, we'll + * simply want to create it. + */ + g_key_file_load_from_file (key_file, key_file_path, 0, NULL); + + g_snprintf (str, sizeof (str), "%o", (guint) need_mask); /* octal, baby */ + g_key_file_set_string (key_file, path, "need_mask", str); + + save_key_file (key_file_path, key_file); + + g_key_file_free (key_file); + g_free (key_file_path); +} + +static void +remove_permissions (const char *path, mode_t need_mask) +{ + struct stat st; + mode_t new_mode; + + if (need_mask == 0) + return; + + if (stat (path, &st) != 0) + return; + + new_mode = st.st_mode & ~need_mask; + + /* Bleah, no error checking */ + chmod (path, new_mode); +} + +static void +remove_from_saved_permissions (const char *path, mode_t remove_mask) +{ + GKeyFile *key_file; + char *key_file_path; + + if (remove_mask == 0) + return; + + key_file = g_key_file_new (); + key_file_path = get_key_file_path (); + + if (g_key_file_load_from_file (key_file, key_file_path, 0, NULL)) + { + mode_t need_mask; + mode_t remove_from_current_mask; + char *str; + + need_mask = 0; + + /* NULL GError */ + str = g_key_file_get_string (key_file, path, "need_mask", NULL); + + if (str) + { + guint i; + + if (sscanf (str, "%o", &i) == 1) /* octal */ + need_mask = i; + + g_free (str); + } + + remove_from_current_mask = need_mask & remove_mask; + remove_permissions (path, remove_from_current_mask); + + need_mask &= ~remove_mask; + + if (need_mask == 0) + { + /* NULL GError */ + g_key_file_remove_group (key_file, path, NULL); + } + else + { + char buf[50]; + + g_snprintf (buf, sizeof (buf), "%o", (guint) need_mask); /* octal */ + g_key_file_set_string (key_file, path, "need_mask", buf); + } + + save_key_file (key_file_path, key_file); + } + + g_key_file_free (key_file); + g_free (key_file_path); +} + +static void +restore_saved_permissions (const char *path) +{ + remove_from_saved_permissions (path, NEED_ALL_MASK); +} + +static void +restore_write_permissions (const char *path) +{ + remove_from_saved_permissions (path, NEED_IF_WRITABLE_MASK); +} + +typedef enum { + CONFIRM_CANCEL_OR_ERROR, + CONFIRM_NO_MODIFICATIONS, + CONFIRM_MODIFIED +} ConfirmPermissionsStatus; + +static ConfirmPermissionsStatus +confirm_sharing_permissions (GtkWidget *widget, const char *path, gboolean is_shared, gboolean guest_ok, gboolean is_writable) +{ + struct stat st; + mode_t mode, new_mode, need_mask; + + if (!is_shared) + return CONFIRM_NO_MODIFICATIONS; + + if (stat (path, &st) != 0) + return CONFIRM_NO_MODIFICATIONS; /* We'll just let "net usershare" give back an error if the file disappears */ + + new_mode = mode = st.st_mode; + + if (guest_ok) + new_mode |= NEED_IF_GUESTOK_MASK; + if (is_writable) + new_mode |= NEED_IF_WRITABLE_MASK; + + need_mask = new_mode & ~mode; + + if (need_mask != 0) + { + g_assert (mode != new_mode); + + if (!message_confirm_missing_permissions (widget, path, need_mask)) + return CONFIRM_CANCEL_OR_ERROR; + + if (chmod (path, new_mode) != 0) + { + error_when_changing_permissions (widget, path); + return CONFIRM_CANCEL_OR_ERROR; + } + + save_changed_permissions (path, need_mask); + + return CONFIRM_MODIFIED; + } + else + { + g_assert (mode == new_mode); + return CONFIRM_NO_MODIFICATIONS; + } + + g_assert_not_reached (); + return CONFIRM_CANCEL_OR_ERROR; +} + +static gboolean +property_page_commit (PropertyPage *page) +{ + gboolean is_shared; + ShareInfo share_info; + ConfirmPermissionsStatus status; + GError *error; + gboolean retval; + + is_shared = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_folder)); + + share_info.path = page->path; + share_info.share_name = (char *) gtk_entry_get_text (GTK_ENTRY (page->entry_share_name)); + share_info.comment = (char *) gtk_entry_get_text (GTK_ENTRY (page->entry_share_comment)); + share_info.is_writable = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_rw_ro)); + share_info.guest_ok = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_guest_ok)); + + /* Do we need to unset the write permissions that we added in the past? */ + if (is_shared && page->was_writable && !share_info.is_writable) + restore_write_permissions (page->path); + + status = confirm_sharing_permissions (page->main, page->path, is_shared, share_info.guest_ok, share_info.is_writable); + if (status == CONFIRM_CANCEL_OR_ERROR) + return FALSE; /* the user didn't want us to change his folder's permissions */ + + error = NULL; + retval = shares_modify_share (share_info.path, is_shared ? &share_info : NULL, &error); + + if (!retval) + { + property_page_set_error (page, error->message); + g_error_free (error); + + /* Since the operation failed, we restore things to the way they were */ + if (status == CONFIRM_MODIFIED) + restore_saved_permissions (page->path); + } + else + { + property_page_validate_fields (page); + caja_file_info_invalidate_extension_info (page->fileinfo); + } + + if (!is_shared) + restore_saved_permissions (page->path); + + /* update initially shared state, so that we may undo later on */ + if (retval) + { + page->was_initially_shared = is_shared; + page->is_dirty = FALSE; + } + + return retval; +} + +/*--------------------------------------------------------------------------*/ +static gchar * +get_fullpath_from_fileinfo(CajaFileInfo *fileinfo) +{ + GFile *file; + gchar *fullpath; + + g_assert (fileinfo != NULL); + + file = caja_file_info_get_location(fileinfo); + fullpath = g_file_get_path(file); + g_assert (fullpath != NULL && g_file_is_native(file)); /* In the beginning we checked that this was a local URI */ + g_object_unref(file); + + return(fullpath); +} + + +/*--------------------------------------------------------------------------*/ +static void +property_page_set_warning (PropertyPage *page) +{ + GdkColor colorYellow; + + gtk_label_set_text (GTK_LABEL (page->label_status), _("Share name is too long")); + + gdk_color_parse ("#ECDF62", &colorYellow); + gtk_widget_modify_base (page->entry_share_name, GTK_STATE_NORMAL, &colorYellow); +} + + +static void +property_page_set_error (PropertyPage *page, const char *message) +{ + GdkColor colorRed; + + gtk_label_set_text (GTK_LABEL (page->label_status), message); + + gdk_color_parse ("#C1665A", &colorRed); + gtk_widget_modify_base (page->entry_share_name, GTK_STATE_NORMAL, &colorRed); +} + +static void +property_page_set_normal (PropertyPage *page) +{ + gtk_label_set_text (GTK_LABEL (page->label_status), ""); + gtk_widget_modify_base (page->entry_share_name, GTK_STATE_NORMAL, NULL); +} + +static gboolean +property_page_share_name_is_valid (PropertyPage *page) +{ + const char *newname; + + newname = gtk_entry_get_text (GTK_ENTRY (page->entry_share_name)); + + if (strlen (newname) == 0) + { + property_page_set_error (page, _("The share name cannot be empty")); + return FALSE; + } + else + { + GError *error; + gboolean exists; + + error = NULL; + if (!shares_get_share_name_exists (newname, &exists, &error)) + { + char *str; + + str = g_strdup_printf (_("Error while getting share information: %s"), error->message); + property_page_set_error (page, str); + g_free (str); + g_error_free (error); + + return FALSE; + } + + if (exists) + { + property_page_set_error (page, _("Another share has the same name")); + return FALSE; + } + else + { + property_page_set_normal (page); + return TRUE; + } + } +} + +static void +property_page_set_controls_sensitivity (PropertyPage *page, + gboolean sensitive) +{ + gtk_widget_set_sensitive (page->entry_share_name, sensitive); + gtk_widget_set_sensitive (page->entry_share_comment, sensitive); + gtk_widget_set_sensitive (page->hbox_share_comment, sensitive); + gtk_widget_set_sensitive (page->hbox_share_name, sensitive); + gtk_widget_set_sensitive (page->checkbutton_share_rw_ro, sensitive); + + if (sensitive) + { + gboolean guest_ok_allowed; + shares_supports_guest_ok (&guest_ok_allowed, NULL); + gtk_widget_set_sensitive (page->checkbutton_share_guest_ok, guest_ok_allowed); + } + else + gtk_widget_set_sensitive (page->checkbutton_share_guest_ok, FALSE); +} + +static void +property_page_check_sensitivity (PropertyPage *page) +{ + gboolean enabled; + gboolean apply_is_sensitive; + + enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_folder)); + property_page_set_controls_sensitivity (page, enabled); + + if (enabled) + apply_is_sensitive = page->is_dirty || !page->was_initially_shared; + else + apply_is_sensitive = page->was_initially_shared; + + gtk_widget_set_sensitive (page->button_apply, apply_is_sensitive); + gtk_button_set_label (GTK_BUTTON(page->button_apply), + page->was_initially_shared ? _("Modify _Share") : _("Create _Share")); +} + +static void +modify_share_name_text_entry (GtkEditable *editable, + gpointer user_data) +{ + PropertyPage *page; + + page = user_data; + + page->is_dirty = TRUE; + + /* This function does simple validation on the share name and sets the error + * label; just let it run and ignore the result value. + */ + property_page_share_name_is_valid (page); + + property_page_check_sensitivity (page); +} + +static void +modify_share_comment_text_entry (GtkEditable *editable, + gpointer user_data) +{ + PropertyPage *page; + + page = user_data; + + page->is_dirty = TRUE; + property_page_check_sensitivity (page); +} + +/*--------------------------------------------------------------------------*/ +static void +on_checkbutton_share_folder_toggled (GtkToggleButton *togglebutton, + gpointer user_data) +{ + PropertyPage *page; + + page = user_data; + + property_page_check_sensitivity (page); +} + +static void +on_checkbutton_rw_ro_toggled (GtkToggleButton *togglebutton, + gpointer user_data) +{ + PropertyPage *page; + + page = user_data; + + page->is_dirty = TRUE; + + property_page_check_sensitivity (page); +} + +static void +on_checkbutton_guest_ok_toggled (GtkToggleButton *togglebutton, + gpointer user_data) +{ + PropertyPage *page; + + page = user_data; + + page->is_dirty = TRUE; + + property_page_check_sensitivity (page); +} + +static void +free_property_page_cb (gpointer data) +{ + PropertyPage *page; + + page = data; + + g_free (page->path); + g_object_unref (page->fileinfo); + g_object_unref (page->xml); + + g_free (page); +} + +static void +button_apply_clicked_cb (GtkButton *button, + gpointer data) +{ + PropertyPage *page; + + page = data; + + if (property_page_commit (page)) + { + if (page->standalone_window) + gtk_widget_destroy (page->standalone_window); + else + property_page_check_sensitivity (page); + } +} + +/*--------------------------------------------------------------------------*/ +static PropertyPage * +create_property_page (CajaFileInfo *fileinfo) +{ + PropertyPage *page; + GError *error; + ShareInfo *share_info; + char *share_name; + gboolean free_share_name; + const char *comment; + char *apply_button_label; + + page = g_new0 (PropertyPage, 1); + + page->path = get_fullpath_from_fileinfo(fileinfo); + page->fileinfo = g_object_ref (fileinfo); + + error = NULL; + if (!shares_get_share_info_for_path (page->path, &share_info, &error)) + { + /* We'll assume that there is no share for that path, but we'll still + * bring up an error dialog. + */ + GtkWidget *message; + + message = gtk_message_dialog_new (NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + _("There was an error while getting the sharing information")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (message), "%s", error->message); + gtk_widget_show (message); + + share_info = NULL; + g_error_free (error); + error = NULL; + } + + + page->xml = gtk_builder_new (); + gtk_builder_set_translation_domain (page->xml, "mate-file-manager-share"); + g_assert (gtk_builder_add_from_file (page->xml, + INTERFACES_DIR"/share-dialog.ui", &error)); + + page->main = GTK_WIDGET (gtk_builder_get_object (page->xml, "vbox1")); + g_assert (page->main != NULL); + + g_object_set_data_full (G_OBJECT (page->main), + "PropertyPage", + page, + free_property_page_cb); + + page->checkbutton_share_folder = GTK_WIDGET (gtk_builder_get_object (page->xml,"checkbutton_share_folder")); + page->hbox_share_comment = GTK_WIDGET (gtk_builder_get_object (page->xml,"hbox_share_comment")); + page->hbox_share_name = GTK_WIDGET (gtk_builder_get_object (page->xml,"hbox_share_name")); + page->checkbutton_share_rw_ro = GTK_WIDGET (gtk_builder_get_object (page->xml,"checkbutton_share_rw_ro")); + page->checkbutton_share_guest_ok = GTK_WIDGET (gtk_builder_get_object (page->xml,"checkbutton_share_guest_ok")); + page->entry_share_name = GTK_WIDGET (gtk_builder_get_object (page->xml,"entry_share_name")); + page->entry_share_comment = GTK_WIDGET (gtk_builder_get_object (page->xml,"entry_share_comment")); + page->label_status = GTK_WIDGET (gtk_builder_get_object (page->xml,"label_status")); + page->button_cancel = GTK_WIDGET (gtk_builder_get_object (page->xml,"button_cancel")); + page->button_apply = GTK_WIDGET (gtk_builder_get_object (page->xml,"button_apply")); + + /* Sanity check so that we don't screw up the Glade file */ + g_assert (page->checkbutton_share_folder != NULL + && page->hbox_share_comment != NULL + && page->hbox_share_name != NULL + && page->checkbutton_share_rw_ro != NULL + && page->checkbutton_share_guest_ok != NULL + && page->entry_share_name != NULL + && page->entry_share_comment != NULL + && page->label_status != NULL + && page->button_cancel != NULL + && page->button_apply != NULL); + + if (share_info) + { + page->was_initially_shared = TRUE; + page->was_writable = share_info->is_writable; + } + + /* Share name */ + + if (share_info) + { + share_name = share_info->share_name; + free_share_name = FALSE; + } + else + { + share_name = g_filename_display_basename (page->path); + free_share_name = TRUE; + } + + gtk_entry_set_text (GTK_ENTRY (page->entry_share_name), share_name); + + if (free_share_name) + g_free (share_name); + + /* Comment */ + + if (share_info == NULL || share_info->comment == NULL) + comment = ""; + else + comment = share_info->comment; + + gtk_entry_set_text (GTK_ENTRY (page->entry_share_comment), comment); + + /* Share toggle */ + + if (share_info) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_folder), TRUE); + else + { + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_folder), FALSE); + } + + /* Share name */ + + if (g_utf8_strlen(gtk_entry_get_text (GTK_ENTRY (page->entry_share_name)), -1) > 12) + property_page_set_warning (page); + + /* Permissions */ + if (share_info != NULL && share_info->is_writable) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_rw_ro), TRUE); + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_rw_ro), FALSE); + + /* Guest access */ + if (share_info != NULL && share_info->guest_ok) + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_guest_ok), TRUE); + else + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->checkbutton_share_guest_ok), FALSE); + + /* Apply button */ + + if (share_info) + apply_button_label = _("Modify _Share"); + else + apply_button_label = _("Create _Share"); + + gtk_button_set_label (GTK_BUTTON (page->button_apply), apply_button_label); + gtk_button_set_use_underline (GTK_BUTTON (page->button_apply), TRUE); + gtk_button_set_image (GTK_BUTTON (page->button_apply), gtk_image_new_from_stock (GTK_STOCK_SAVE, GTK_ICON_SIZE_BUTTON)); + + gtk_widget_set_sensitive (page->button_apply, FALSE); + + /* Sensitivity */ + + property_page_check_sensitivity (page); + + /* Signal handlers */ + + g_signal_connect (page->checkbutton_share_folder, "toggled", + G_CALLBACK (on_checkbutton_share_folder_toggled), + page); + + g_signal_connect (page->checkbutton_share_rw_ro, "toggled", + G_CALLBACK (on_checkbutton_rw_ro_toggled), + page); + + g_signal_connect (page->checkbutton_share_guest_ok, "toggled", + G_CALLBACK (on_checkbutton_guest_ok_toggled), + page); + + g_signal_connect (page->entry_share_name, "changed", + G_CALLBACK (modify_share_name_text_entry), + page); + + g_signal_connect (page->entry_share_comment, "changed", + G_CALLBACK (modify_share_comment_text_entry), + page); + + g_signal_connect (page->button_apply, "clicked", + G_CALLBACK (button_apply_clicked_cb), page); + + if (share_info != NULL) + shares_free_share_info (share_info); + + return page; +} + +/* Implementation of the CajaInfoProvider interface */ + +/* caja_info_provider_update_file_info + * This function is called by Caja when it wants the extension to + * fill in data about the file. It passes a CajaFileInfo object, + * which the extension can use to read data from the file, and which + * the extension should add data to. + * + * If the data can be added immediately (without doing blocking IO), + * the extension can do so, and return CAJA_OPERATION_COMPLETE. + * In this case the 'update_complete' and 'handle' parameters can be + * ignored. + * + * If waiting for the deata would block the UI, the extension should + * perform the task asynchronously, and return + * CAJA_OPERATION_IN_PROGRESS. The function must also set the + * 'handle' pointer to a value unique to the object, and invoke the + * 'update_complete' closure when the update is done. + * + * If the extension encounters an error, it should return + * CAJA_OPERATION_FAILED. + */ +typedef struct { + gboolean cancelled; + CajaInfoProvider *provider; + CajaFileInfo *file; + GClosure *update_complete; +} CajaShareHandle; + +static CajaShareStatus +get_share_status_and_free_share_info (ShareInfo *share_info) +{ + CajaShareStatus result; + + if (!share_info) + result = CAJA_SHARE_NOT_SHARED; + else + { + if (share_info->is_writable) + result = CAJA_SHARE_SHARED_RW; + else + result = CAJA_SHARE_SHARED_RO; + + shares_free_share_info (share_info); + } + + return result; +} + + +/*--------------------------------------------------------------------------*/ +static void +get_share_info_for_file_info (CajaFileInfo *file, ShareInfo **share_info, gboolean *is_shareable) +{ + char *uri; + char *local_path = NULL; + GFile *f; + + *share_info = NULL; + *is_shareable = FALSE; + + uri = caja_file_info_get_uri (file); + f = caja_file_info_get_location(file); + if (!uri) + goto out; + +#define NETWORK_SHARE_PREFIX "network:///share-" + + if (g_str_has_prefix (uri, NETWORK_SHARE_PREFIX)) + { + const char *share_name; + + share_name = uri + strlen (NETWORK_SHARE_PREFIX); + + /* FIXME: NULL GError */ + if (!shares_get_share_info_for_share_name (share_name, share_info, NULL)) + { + *share_info = NULL; + *is_shareable = TRUE; /* it *has* the prefix, anyway... we are just unsynchronized with what mate-vfs thinks */ + } + else + { + *is_shareable = TRUE; + } + + goto out; + } + + if (!caja_file_info_is_directory(file)) + goto out; + + local_path = g_file_get_path(f); + if (!local_path || !g_file_is_native(f)) + goto out; + + /* FIXME: NULL GError */ + if (!shares_get_share_info_for_path (local_path, share_info, NULL)) + goto out; + + *is_shareable = TRUE; + + out: + + g_object_unref(f); + g_free (uri); + g_free (local_path); +} + +/*--------------------------------------------------------------------------*/ +static CajaShareStatus +file_get_share_status_file(CajaFileInfo *file) +{ + ShareInfo *share_info; + gboolean is_shareable; + + get_share_info_for_file_info (file, &share_info, &is_shareable); + + if (!is_shareable) + return CAJA_SHARE_NOT_SHARED; + + return get_share_status_and_free_share_info (share_info); +} + +static CajaOperationResult +caja_share_update_file_info (CajaInfoProvider *provider, + CajaFileInfo *file, + GClosure *update_complete, + CajaOperationHandle **handle) +{ +/* gchar *share_status = NULL; */ + + switch (file_get_share_status_file (file)) { + + case CAJA_SHARE_SHARED_RO: + caja_file_info_add_emblem (file, "shared"); +/* share_status = _("shared (read only)"); */ + break; + + case CAJA_SHARE_SHARED_RW: + caja_file_info_add_emblem (file, "shared"); +/* share_status = _("shared (read and write)"); */ + break; + + case CAJA_SHARE_NOT_SHARED: +/* share_status = _("not shared"); */ + break; + + default: + g_assert_not_reached (); + break; + } + +/* caja_file_info_add_string_attribute (file, */ +/* "CajaShare::share_status", */ +/* share_status); */ + return CAJA_OPERATION_COMPLETE; +} + + +static void +caja_share_cancel_update (CajaInfoProvider *provider, + CajaOperationHandle *handle) +{ + CajaShareHandle *share_handle; + + share_handle = (CajaShareHandle*)handle; + share_handle->cancelled = TRUE; +} + +static void +caja_share_info_provider_iface_init (CajaInfoProviderIface *iface) +{ + iface->update_file_info = caja_share_update_file_info; + iface->cancel_update = caja_share_cancel_update; +} + +/*--------------------------------------------------------------------------*/ +/* caja_property_page_provider_get_pages + * + * This function is called by Caja when it wants property page + * items from the extension. + * + * This function is called in the main thread before a property page + * is shown, so it should return quickly. + * + * The function should return a GList of allocated CajaPropertyPage + * items. + */ +static GList * +caja_share_get_property_pages (CajaPropertyPageProvider *provider, + GList *files) +{ + PropertyPage *page; + GList *pages; + CajaPropertyPage *np_page; + CajaFileInfo *fileinfo; + ShareInfo *share_info; + gboolean is_shareable; + + /* Only show the property page if 1 file is selected */ + if (!files || files->next != NULL) { + return NULL; + } + + fileinfo = CAJA_FILE_INFO (files->data); + + get_share_info_for_file_info (fileinfo, &share_info, &is_shareable); + if (!is_shareable) + return NULL; + + page = create_property_page (fileinfo); + gtk_widget_hide (page->button_cancel); + + if (share_info) + shares_free_share_info (share_info); + + pages = NULL; + np_page = caja_property_page_new + ("CajaShare::property_page", + gtk_label_new (_("Share")), + page->main); + pages = g_list_append (pages, np_page); + + return pages; +} + +/*--------------------------------------------------------------------------*/ +static void +caja_share_property_page_provider_iface_init (CajaPropertyPageProviderIface *iface) +{ + iface->get_pages = caja_share_get_property_pages; +} + +/*--------------------------------------------------------------------------*/ +static void +caja_share_instance_init (CajaShare *share) +{ +} + +/*--------------------------------------------------------------------------*/ +static void +caja_share_class_init (CajaShareClass *class) +{ + parent_class = g_type_class_peek_parent (class); +} + +/* caja_menu_provider_get_file_items + * + * This function is called by Caja when it wants context menu + * items from the extension. + * + * This function is called in the main thread before a context menu + * is shown, so it should return quickly. + * + * The function should return a GList of allocated CajaMenuItem + * items. + */ + +static void +button_cancel_clicked_cb (GtkButton *button, gpointer data) +{ + GtkWidget *window; + + window = GTK_WIDGET (data); + gtk_widget_destroy (window); +} + +static void +share_this_folder_callback (CajaMenuItem *item, + gpointer user_data) +{ + CajaFileInfo *fileinfo; + PropertyPage *page; + GtkWidget * window; + + fileinfo = CAJA_FILE_INFO (user_data); + g_assert (fileinfo != NULL); + + window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + gtk_window_set_title (GTK_WINDOW (window), _("Folder Sharing")); + page = create_property_page (fileinfo); + page->standalone_window = window; + g_signal_connect (page->button_cancel, "clicked", + G_CALLBACK (button_cancel_clicked_cb), window); + + gtk_container_add (GTK_CONTAINER (window), page->main); + gtk_widget_show (window); +} + +static GList * +caja_share_get_file_items (CajaMenuProvider *provider, + GtkWidget *window, + GList *files) +{ + GList *items; + CajaMenuItem *item; + CajaFileInfo *fileinfo; + ShareInfo *share_info; + gboolean is_shareable; + + /* Only show the property page if 1 file is selected */ + if (!files || files->next != NULL) { + return NULL; + } + + fileinfo = CAJA_FILE_INFO (files->data); + + get_share_info_for_file_info (fileinfo, &share_info, &is_shareable); + + if (!is_shareable) + return NULL; + + if (share_info) + shares_free_share_info (share_info); + + /* We don't own a reference to the file info to keep it around, so acquire one */ + g_object_ref (fileinfo); + + /* FMQ: change the label to "Share with Windows users"? */ + item = caja_menu_item_new ("CajaShare::share", + _("Sharing Options"), + _("Share this Folder"), + "folder-remote"); + g_signal_connect (item, "activate", + G_CALLBACK (share_this_folder_callback), + fileinfo); + g_object_set_data_full (G_OBJECT (item), + "files", + fileinfo, + g_object_unref); /* Release our reference when the menu item goes away */ + + items = g_list_append (NULL, item); + return items; +} + +/*--------------------------------------------------------------------------*/ +static void +caja_share_menu_provider_iface_init (CajaMenuProviderIface *iface) +{ + iface->get_file_items = caja_share_get_file_items; +} + +/*--------------------------------------------------------------------------*/ +/* Type registration. Because this type is implemented in a module + * that can be unloaded, we separate type registration from get_type(). + * the type_register() function will be called by the module's + * initialization function. */ +static GType share_type = 0; + +#define CAJA_TYPE_SHARE (caja_share_get_type ()) + +static GType +caja_share_get_type (void) +{ + return share_type; +} + +static void +caja_share_register_type (GTypeModule *module) +{ + static const GTypeInfo info = { + sizeof (CajaShareClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) caja_share_class_init, + NULL, + NULL, + sizeof (CajaShare), + 0, + (GInstanceInitFunc) caja_share_instance_init, + }; + + share_type = g_type_module_register_type (module, + G_TYPE_OBJECT, + "CajaShare", + &info, 0); + + /* onglet share propri�t� */ + static const GInterfaceInfo property_page_provider_iface_info = { + (GInterfaceInitFunc) caja_share_property_page_provider_iface_init, + NULL, + NULL + }; + + g_type_module_add_interface (module, + share_type, + CAJA_TYPE_PROPERTY_PAGE_PROVIDER, + &property_page_provider_iface_info); + + + /* premier page propri�t� ? */ + static const GInterfaceInfo info_provider_iface_info = { + (GInterfaceInitFunc) caja_share_info_provider_iface_init, + NULL, + NULL + }; + + g_type_module_add_interface (module, + share_type, + CAJA_TYPE_INFO_PROVIDER, + &info_provider_iface_info); + + /* Menu right clik */ + static const GInterfaceInfo menu_provider_iface_info = { + (GInterfaceInitFunc) caja_share_menu_provider_iface_init, + NULL, + NULL + }; + + g_type_module_add_interface (module, + share_type, + CAJA_TYPE_MENU_PROVIDER, + &menu_provider_iface_info); + +} + +/* Extension module functions. These functions are defined in + * caja-extensions-types.h, and must be implemented by all + * extensions. */ + +/* Initialization function. In addition to any module-specific + * initialization, any types implemented by the module should + * be registered here. */ +void +caja_module_initialize (GTypeModule *module) +{ + /*g_print ("Initializing mate-file-manager-share extension\n");*/ + + bindtextdomain(GETTEXT_PACKAGE, MATELOCALEDIR); + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); + + caja_share_register_type (module); +} + +/* Perform module-specific shutdown. */ +void +caja_module_shutdown (void) +{ + /*g_print ("Shutting down mate-file-manager-share extension\n");*/ + /* FIXME freeing */ +} + +/* List all the extension types. */ +void +caja_module_list_types (const GType **types, + int *num_types) +{ + static GType type_list[1]; + + type_list[0] = CAJA_TYPE_SHARE; + + *types = type_list; + *num_types = 1; +} diff --git a/share/mate-file-manager-share.h b/share/mate-file-manager-share.h new file mode 100644 index 0000000..9907ac2 --- /dev/null +++ b/share/mate-file-manager-share.h @@ -0,0 +1,65 @@ +/* mate-file-manager-share -- Caja File Sharing Extension + * + * Sebastien Estienne <[email protected]> + * + * 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 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + * + * (C) Copyright 2005 Ethium, Inc. + */ + +#ifndef CAJA_SHARE_H +#define CAJA_SHARE_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +/* Declarations for the Share extension object. This object will be + * instantiated by caja. It implements the GInterfaces + * exported by libcaja. */ + + +typedef struct _CajaShare CajaShare; +typedef struct _CajaShareClass CajaShareClass; + +struct _CajaShare { + GObject parent_slot; +}; + +struct _CajaShareClass { + GObjectClass parent_slot; + + /* No extra class members */ +}; + + +typedef struct _CajaShareData CajaShareData; + +struct _CajaShareData { + gchar *fullpath; + gchar *section; + CajaFileInfo *fileinfo; +}; + +G_END_DECLS + +typedef enum { + CAJA_SHARE_NOT_SHARED, + CAJA_SHARE_SHARED_RO, + CAJA_SHARE_SHARED_RW +} CajaShareStatus; + +#endif + diff --git a/share/share-dialog.ui b/share/share-dialog.ui new file mode 100644 index 0000000..caf96d3 --- /dev/null +++ b/share/share-dialog.ui @@ -0,0 +1,301 @@ +<?xml version="1.0"?> +<interface> + <!-- interface-requires gtk+ 2.12 --> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="border_width">12</property> + <property name="orientation">vertical</property> + <property name="spacing">12</property> + <child> + <object class="GtkHBox" id="hbox6"> + <property name="visible">True</property> + <child> + <object class="GtkImage" id="logo_image"> + <property name="visible">True</property> + <property name="pixel_size">64</property> + <property name="icon_name">folder-remote</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="logo_label"> + <property name="visible">True</property> + <property name="label" translatable="yes"><big><b>Folder Sharing</b></big></property> + <property name="use_markup">True</property> + <property name="justify">center</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator1"> + <property name="visible">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="n_rows">5</property> + <property name="n_columns">2</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <object class="GtkHBox" id="hbox_share_name"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label1"> + <property name="width_request">20</property> + <property name="visible">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_share_name"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Share _name:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_share_name</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_share_name"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="checkbutton_share_folder"> + <property name="label" translatable="yes">Share this _folder</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="right_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox_share_comment"> + <property name="visible">True</property> + <child> + <object class="GtkLabel" id="label2"> + <property name="width_request">20</property> + <property name="visible">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_share_comment"> + <property name="visible">True</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Co_mment:</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">entry_share_comment</property> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="entry_share_comment"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="checkbutton_share_rw_ro"> + <property name="label" translatable="yes">_Allow others to create and delete files in this folder</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="checkbutton_share_guest_ok"> + <property name="label" translatable="yes">_Guest access (for people without a user account)</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="right_attach">2</property> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"></property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label_status"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="wrap">True</property> + <property name="selectable">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator2"/> + <packing> + <property name="expand">False</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="treeview_share_list"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + <packing> + <property name="padding">1</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator3"> + <property name="sensitive">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="position">6</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="spacing">5</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button_cancel"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="button_apply"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <child> + <object class="GtkImage" id="image1"> + <property name="visible">True</property> + <property name="stock">gtk-save</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="position">7</property> + </packing> + </child> + </object> +</interface> diff --git a/share/shares.c b/share/shares.c new file mode 100644 index 0000000..cd609f3 --- /dev/null +++ b/share/shares.c @@ -0,0 +1,1023 @@ +#include <config.h> +#include <string.h> +#include <time.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <glib/gi18n-lib.h> +#include "shares.h" + +#undef DEBUG_SHARES +#ifdef DEBUG_SHARES +# define NET_USERSHARE_ARGV0 "debug-net-usershare" +#else +# define NET_USERSHARE_ARGV0 "net" +#endif + +static GHashTable *path_share_info_hash; +static GHashTable *share_name_share_info_hash; + +#define NUM_CALLS_BETWEEN_TIMESTAMP_UPDATES 100 +#define TIMESTAMP_THRESHOLD 10 /* seconds */ +static int refresh_timestamp_update_counter; +static time_t refresh_timestamp; + +#define KEY_PATH "path" +#define KEY_COMMENT "comment" +#define KEY_ACL "usershare_acl" +#define KEY_GUEST_OK "guest_ok" +#define GROUP_ALLOW_GUESTS "global" +#define KEY_ALLOW_GUESTS "usershare allow guests" + +/* Debugging flags */ +static gboolean throw_error_on_refresh; +static gboolean throw_error_on_add; +static gboolean throw_error_on_modify; +static gboolean throw_error_on_remove; + + + +/* Interface to "net usershare" */ + +static gboolean +net_usershare_run (int argc, char **argv, GKeyFile **ret_key_file, GError **error) +{ + int real_argc; + int i; + char **real_argv; + gboolean retval; + char *stdout_contents; + char *stderr_contents; + int exit_status; + int exit_code; + GKeyFile *key_file; + GError *real_error; + + g_assert (argc > 0); + g_assert (argv != NULL); + g_assert (error == NULL || *error == NULL); + + if (ret_key_file) + *ret_key_file = NULL; + + /* Build command line */ + + real_argc = 2 + argc + 1; /* "net" "usershare" [argv] NULL */ + real_argv = g_new (char *, real_argc); + + real_argv[0] = NET_USERSHARE_ARGV0; + real_argv[1] = "usershare"; + + for (i = 0; i < argc; i++) { + g_assert (argv[i] != NULL); + real_argv[i + 2] = argv[i]; + } + + real_argv[real_argc - 1] = NULL; + + /* Launch */ + + stdout_contents = NULL; + stderr_contents = NULL; + /* + { + char **p; + + g_message ("------------------------------------------"); + + for (p = real_argv; *p; p++) + g_message ("spawn arg \"%s\"", *p); + + g_message ("end of spawn args; SPAWNING\n"); + } + */ + real_error = NULL; + retval = g_spawn_sync (NULL, /* cwd */ + real_argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH, + NULL, /* GSpawnChildSetupFunc */ + NULL, /* user_data */ + &stdout_contents, + &stderr_contents, + &exit_status, + &real_error); + + /* g_message ("returned from spawn: %s: %s", retval ? "SUCCESS" : "FAIL", retval ? "" : real_error->message); */ + + if (!retval) { + g_propagate_error (error, real_error); + goto out; + } + + if (!WIFEXITED (exit_status)) { + g_message ("WIFEXITED(%d) was false!", exit_status); + retval = FALSE; + + if (WIFSIGNALED (exit_status)) { + int signal_num; + + signal_num = WTERMSIG (exit_status); + g_message ("Child got signal %d", signal_num); + + g_set_error (error, + SHARES_ERROR, + SHARES_ERROR_FAILED, + _("%s %s %s returned with signal %d"), + real_argv[0], + real_argv[1], + real_argv[2], + signal_num); + } else + g_set_error (error, + SHARES_ERROR, + SHARES_ERROR_FAILED, + _("%s %s %s failed for an unknown reason"), + real_argv[0], + real_argv[1], + real_argv[2]); + + goto out; + } + + exit_code = WEXITSTATUS (exit_status); + + /* g_message ("exit code %d", exit_code); */ + if (exit_code != 0) { + char *str; + char *message; + + /* stderr_contents is in the system locale encoding, not UTF-8 */ + + str = g_locale_to_utf8 (stderr_contents, -1, NULL, NULL, NULL); + + if (str && str[0]) + message = g_strdup_printf (_("'net usershare' returned error %d: %s"), exit_code, str); + else + message = g_strdup_printf (_("'net usershare' returned error %d"), exit_code); + + g_free (str); + + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + "%s", + message); + + g_free (message); + + retval = FALSE; + goto out; + } + + if (ret_key_file) { + /* g_message ("caller wants GKeyFile"); */ + + *ret_key_file = NULL; + + /* FIXME: [email protected] says the output of "net usershare" is nearly always + * in UTF-8, but that it can be configured in the master smb.conf. We assume + * UTF-8 for now. + */ + + if (!g_utf8_validate (stdout_contents, -1, NULL)) { + g_message ("stdout of net usershare was not in valid UTF-8"); + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + _("the output of 'net usershare' is not in valid UTF-8 encoding")); + retval = FALSE; + goto out; + } + + key_file = g_key_file_new (); + + real_error = NULL; + if (!g_key_file_load_from_data (key_file, stdout_contents, -1, 0, &real_error)) { + g_message ("Error when parsing key file {\n%s\n}: %s", stdout_contents, real_error->message); + g_propagate_error (error, real_error); + g_key_file_free (key_file); + retval = FALSE; + goto out; + } + + retval = TRUE; + *ret_key_file = key_file; + } else + retval = TRUE; + + /* g_message ("success from calling net usershare and parsing its output"); */ + + out: + g_free (real_argv); + g_free (stdout_contents); + g_free (stderr_contents); + + /* g_message ("------------------------------------------"); */ + + return retval; +} + + + +/* Internals */ + +static void +ensure_hashes (void) +{ + if (path_share_info_hash == NULL) { + g_assert (share_name_share_info_hash == NULL); + + path_share_info_hash = g_hash_table_new (g_str_hash, g_str_equal); + share_name_share_info_hash = g_hash_table_new (g_str_hash, g_str_equal); + } else + g_assert (share_name_share_info_hash != NULL); +} + +static ShareInfo * +lookup_share_by_path (const char *path) +{ + ensure_hashes (); + return g_hash_table_lookup (path_share_info_hash, path); +} + +static ShareInfo * +lookup_share_by_share_name (const char *share_name) +{ + ensure_hashes (); + return g_hash_table_lookup (share_name_share_info_hash, share_name); +} + +static void +add_share_info_to_hashes (ShareInfo *info) +{ + ensure_hashes (); + g_hash_table_insert (path_share_info_hash, info->path, info); + g_hash_table_insert (share_name_share_info_hash, info->share_name, info); +} + +static void +remove_share_info_from_hashes (ShareInfo *info) +{ + ensure_hashes (); + g_hash_table_remove (path_share_info_hash, info->path); + g_hash_table_remove (share_name_share_info_hash, info->share_name); +} + +static gboolean +remove_from_path_hash_cb (gpointer key, + gpointer value, + gpointer data) +{ + ShareInfo *info; + + info = value; + shares_free_share_info (info); + + return TRUE; +} + +static gboolean +remove_from_share_name_hash_cb (gpointer key, + gpointer value, + gpointer data) +{ + /* The ShareInfo was already freed in remove_from_path_hash_cb() */ + return TRUE; +} + +static void +free_all_shares (void) +{ + ensure_hashes (); + g_hash_table_foreach_remove (path_share_info_hash, remove_from_path_hash_cb, NULL); + g_hash_table_foreach_remove (share_name_share_info_hash, remove_from_share_name_hash_cb, NULL); +} + +static char * +get_string_from_key_file (GKeyFile *key_file, const char *group, const char *key) +{ + GError *error; + char *str; + + error = NULL; + str = NULL; + + if (g_key_file_has_key (key_file, group, key, &error)) { + str = g_key_file_get_string (key_file, group, key, &error); + if (!str) { + g_assert (!g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_NOT_FOUND) + && !g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)); + + g_error_free (error); + } + } else { + g_assert (!g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND)); + g_error_free (error); + } + + return str; +} + +static void +add_key_group_to_hashes (GKeyFile *key_file, const char *group) +{ + char *path; + char *comment; + char *acl; + gboolean is_writable; + char *guest_ok_str; + gboolean guest_ok; + ShareInfo *info; + ShareInfo *old_info; + + /* Remove the old share based on the name */ + + old_info = lookup_share_by_share_name (group); + if (old_info) { + remove_share_info_from_hashes (old_info); + shares_free_share_info (old_info); + } + + /* Start parsing, and remove the old share based on the path */ + + path = get_string_from_key_file (key_file, group, KEY_PATH); + if (!path) { + g_message ("group '%s' doesn't have a '%s' key! Ignoring group.", group, KEY_PATH); + return; + } + + old_info = lookup_share_by_path (path); + if (old_info) { + remove_share_info_from_hashes (old_info); + shares_free_share_info (old_info); + } + + /* Finish parsing */ + + comment = get_string_from_key_file (key_file, group, KEY_COMMENT); + + acl = get_string_from_key_file (key_file, group, KEY_ACL); + if (acl) { + if (strstr (acl, "Everyone:R")) + is_writable = FALSE; + else if (strstr (acl, "Everyone:F")) + is_writable = TRUE; + else { + g_message ("unknown format for key '%s/%s' as it contains '%s'. Assuming that the share is read-only", + group, KEY_ACL, acl); + is_writable = FALSE; + } + + g_free (acl); + } else { + g_message ("group '%s' doesn't have a '%s' key! Assuming that the share is read-only.", group, KEY_ACL); + is_writable = FALSE; + } + + guest_ok_str = get_string_from_key_file (key_file, group, KEY_GUEST_OK); + if (guest_ok_str) { + if (strcmp (guest_ok_str, "n") == 0) + guest_ok = FALSE; + else if (strcmp (guest_ok_str, "y") == 0) + guest_ok = TRUE; + else { + g_message ("unknown format for key '%s/%s' as it contains '%s'. Assuming that the share is not guest accessible.", + group, KEY_GUEST_OK, guest_ok_str); + guest_ok = FALSE; + } + + g_free (guest_ok_str); + } else { + g_message ("group '%s' doesn't have a '%s' key! Assuming that the share is not guest accessible.", group, KEY_GUEST_OK); + guest_ok = FALSE; + } + + g_assert (path != NULL); + g_assert (group != NULL); + + info = g_new (ShareInfo, 1); + info->path = path; + info->share_name = g_strdup (group); + info->comment = comment; + info->is_writable = is_writable; + info->guest_ok = guest_ok; + + add_share_info_to_hashes (info); +} + +static void +replace_shares_from_key_file (GKeyFile *key_file) +{ + gsize num_groups; + char **group_names; + gsize i; + + group_names = g_key_file_get_groups (key_file, &num_groups); + + /* FIXME: In add_key_group_to_hashes(), we simply ignore key groups + * which have invalid data (i.e. no path). We could probably accumulate a + * GError with the list of invalid groups and propagate it upwards. + */ + for (i = 0; i < num_groups; i++) { + g_assert (group_names[i] != NULL); + add_key_group_to_hashes (key_file, group_names[i]); + } + + g_strfreev (group_names); +} + +static gboolean +refresh_shares (GError **error) +{ + GKeyFile *key_file; + char *argv[1]; + GError *real_error; + + free_all_shares (); + + if (throw_error_on_refresh) { + g_set_error (error, + SHARES_ERROR, + SHARES_ERROR_FAILED, + _("Failed")); + return FALSE; + } + + argv[0] = "info"; + + real_error = NULL; + if (!net_usershare_run (G_N_ELEMENTS (argv), argv, &key_file, &real_error)) { + g_message ("Called \"net usershare info\" but it failed: %s", real_error->message); + g_propagate_error (error, real_error); + return FALSE; + } + + g_assert (key_file != NULL); + + replace_shares_from_key_file (key_file); + g_key_file_free (key_file); + + return TRUE; +} + +static gboolean +refresh_if_needed (GError **error) +{ + gboolean retval; + + if (refresh_timestamp_update_counter == 0) { + time_t new_timestamp; + + refresh_timestamp_update_counter = NUM_CALLS_BETWEEN_TIMESTAMP_UPDATES; + + new_timestamp = time (NULL); + if (new_timestamp - refresh_timestamp > TIMESTAMP_THRESHOLD) { + /* g_message ("REFRESHING SHARES"); */ + retval = refresh_shares (error); + } else + retval = TRUE; + + refresh_timestamp = new_timestamp; + } else { + refresh_timestamp_update_counter--; + retval = TRUE; + } + + return retval; +} + +static ShareInfo * +copy_share_info (ShareInfo *info) +{ + ShareInfo *copy; + + if (!info) + return NULL; + + copy = g_new (ShareInfo, 1); + copy->path = g_strdup (info->path); + copy->share_name = g_strdup (info->share_name); + copy->comment = g_strdup (info->comment); + copy->is_writable = info->is_writable; + copy->guest_ok = info->guest_ok; + + return copy; +} + +/** + * shares_supports_guest_ok: + * @supports_guest_ok_ret: Location to store whether "usershare allow guests" + * is enabled. + * @error: Location to store error, or #NULL. + * + * Determines whether the option "usershare allow guests" is enabled in samba + * config as shown by testparm. + * + * Return value: #TRUE if if the info could be queried successfully, #FALSE + * otherwise. If this function returns #FALSE, an error code will be returned + * in the @error argument, and *@ret_info_list will be set to #FALSE. + **/ +gboolean +shares_supports_guest_ok (gboolean *supports_guest_ok_ret, GError **error) +{ + gboolean retval; + gboolean result; + char *stdout_contents; + char *stderr_contents; + int exit_status; + int exit_code; + + *supports_guest_ok_ret = FALSE; + + result = g_spawn_command_line_sync ("testparm -s --parameter-name='usershare allow guests'", + &stdout_contents, + &stderr_contents, + &exit_status, + error); + if (!result) + return FALSE; + + retval = FALSE; + + if (!WIFEXITED (exit_status)) { + if (WIFSIGNALED (exit_status)) { + int signal_num; + + signal_num = WTERMSIG (exit_status); + g_set_error (error, + SHARES_ERROR, + SHARES_ERROR_FAILED, + _("Samba's testparm returned with signal %d"), + signal_num); + } else + g_set_error (error, + SHARES_ERROR, + SHARES_ERROR_FAILED, + _("Samba's testparm failed for an unknown reason")); + + goto out; + } + + exit_code = WEXITSTATUS (exit_status); + if (exit_code != 0) { + char *str; + char *message; + + /* stderr_contents is in the system locale encoding, not UTF-8 */ + + str = g_locale_to_utf8 (stderr_contents, -1, NULL, NULL, NULL); + + if (str && str[0]) + message = g_strdup_printf (_("Samba's testparm returned error %d: %s"), exit_code, str); + else + message = g_strdup_printf (_("Samba's testparm returned error %d"), exit_code); + + g_free (str); + + g_set_error (error, + G_SPAWN_ERROR, + G_SPAWN_ERROR_FAILED, + "%s", + message); + + g_free (message); + + goto out; + } + + retval = TRUE; + *supports_guest_ok_ret = (g_ascii_strncasecmp (stdout_contents, "Yes", 3) == 0); + + out: + g_free (stdout_contents); + g_free (stderr_contents); + + return retval; +} + +static gboolean +add_share (ShareInfo *info, GError **error) +{ + char *argv[7]; + int argc; + ShareInfo *copy; + GKeyFile *key_file; + GError *real_error; + gboolean supports_success; + gboolean supports_guest_ok; + gboolean net_usershare_success; + + /* g_message ("add_share() start"); */ + + if (throw_error_on_add) { + g_set_error (error, + SHARES_ERROR, + SHARES_ERROR_FAILED, + _("Failed")); + g_message ("add_share() end FAIL"); + return FALSE; + } + + supports_success = shares_supports_guest_ok (&supports_guest_ok, error); + if (!supports_success) + return FALSE; + + argv[0] = "add"; + argv[1] = "-l"; + argv[2] = info->share_name; + argv[3] = info->path; + argv[4] = info->comment; + argv[5] = info->is_writable ? "Everyone:F" : g_strdup_printf ("Everyone:R,%s:F", g_get_user_name ()); + + if (supports_guest_ok) { + argv[6] = info->guest_ok ? "guest_ok=y" : "guest_ok=n"; + argc = 7; + } else + argc = 6; + + real_error = NULL; + net_usershare_success = net_usershare_run (argc, argv, &key_file, &real_error); + if (!info->is_writable) g_free (argv[5]); + + if (!net_usershare_success) { + g_message ("Called \"net usershare add\" but it failed: %s", real_error->message); + g_propagate_error (error, real_error); + return FALSE; + } + + replace_shares_from_key_file (key_file); + + copy = copy_share_info (info); + add_share_info_to_hashes (copy); + + /* g_message ("add_share() end SUCCESS"); */ + + return TRUE; +} + +static gboolean +remove_share (const char *path, GError **error) +{ + ShareInfo *old_info; + char *argv[2]; + GError *real_error; + + /* g_message ("remove_share() start"); */ + + if (throw_error_on_remove) { + g_set_error (error, + SHARES_ERROR, + SHARES_ERROR_FAILED, + "Failed"); + g_message ("remove_share() end FAIL"); + return FALSE; + } + + old_info = lookup_share_by_path (path); + if (!old_info) { + char *display_name; + + display_name = g_filename_display_name (path); + g_set_error (error, + SHARES_ERROR, + SHARES_ERROR_NONEXISTENT, + _("Cannot remove the share for path %s: that path is not shared"), + display_name); + g_free (display_name); + + g_message ("remove_share() end FAIL: path %s was not in our hashes", path); + return FALSE; + } + + argv[0] = "delete"; + argv[1] = old_info->share_name; + + real_error = NULL; + if (!net_usershare_run (G_N_ELEMENTS (argv), argv, NULL, &real_error)) { + g_message ("Called \"net usershare delete\" but it failed: %s", real_error->message); + g_propagate_error (error, real_error); + g_message ("remove_share() end FAIL"); + return FALSE; + } + + remove_share_info_from_hashes (old_info); + shares_free_share_info (old_info); + + /* g_message ("remove_share() end SUCCESS"); */ + + return TRUE; +} + +static gboolean +modify_share (const char *old_path, ShareInfo *info, GError **error) +{ + ShareInfo *old_info; + + /* g_message ("modify_share() start"); */ + + old_info = lookup_share_by_path (old_path); + if (old_info == NULL) { + /*g_message ("modify_share() end; calling add_share() instead");*/ + return add_share (info, error); + } + + g_assert (old_info != NULL); + + if (strcmp (info->path, old_info->path) != 0) { + g_set_error (error, + SHARES_ERROR, + SHARES_ERROR_FAILED, + _("Cannot change the path of an existing share; please remove the old share first and add a new one")); + g_message ("modify_share() end FAIL: tried to change the path in a share!"); + return FALSE; + } + + if (throw_error_on_modify) { + g_set_error (error, + SHARES_ERROR, + SHARES_ERROR_FAILED, + "Failed"); + g_message ("modify_share() end FAIL"); + return FALSE; + } + + /* Although "net usershare add" will modify an existing share if it has the same share name + * as the one that gets passed in, our semantics are different. We have a one-to-one mapping + * between paths and share names; "net usershare" supports a one-to-many mapping from paths + * to share names. So, we must first remove the old share and then add the new/modified one. + */ + + if (!remove_share (old_path, error)) { + g_message ("modify_share() end FAIL: error when removing old share"); + return FALSE; + } + + /* g_message ("modify_share() end: will call add_share() with the new share info"); */ + return add_share (info, error); +} + + + +/* Public API */ + +GQuark +shares_error_quark (void) +{ + static GQuark quark; + + if (quark == 0) + quark = g_quark_from_string ("mate-file-manager-shares-error-quark"); /* not from_static_string since we are a module */ + + return quark; +} + +/** + * shares_free_share_info: + * @info: A #ShareInfo structure. + * + * Frees a #ShareInfo structure. + **/ +void +shares_free_share_info (ShareInfo *info) +{ + g_assert (info != NULL); + + g_free (info->path); + g_free (info->share_name); + g_free (info->comment); + g_free (info); +} + +/** + * shares_get_path_is_shared: + * @path: A full path name ("/foo/bar/baz") in file system encoding. + * @ret_is_shared: Location to store result value (#TRUE if the path is shared, #FALSE otherwise) + * @error: Location to store error, or #NULL. + * + * Checks whether a path is shared through Samba. + * + * Return value: #TRUE if the info could be queried successfully, #FALSE + * otherwise. If this function returns #FALSE, an error code will be returned in the + * @error argument, and *@ret_is_shared will be set to #FALSE. + **/ +gboolean +shares_get_path_is_shared (const char *path, gboolean *ret_is_shared, GError **error) +{ + g_assert (ret_is_shared != NULL); + g_assert (error == NULL || *error == NULL); + + if (!refresh_if_needed (error)) { + *ret_is_shared = FALSE; + return FALSE; + } + + *ret_is_shared = (lookup_share_by_path (path) != NULL); + + return TRUE; +} + +/** + * shares_get_share_info_for_path: + * @path: A full path name ("/foo/bar/baz") in file system encoding. + * @ret_share_info: Location to store result with the share's info - on return, + * will be non-NULL if the path is indeed shared, or #NULL if the path is not + * shared. You must free the non-NULL value with shares_free_share_info(). + * @error: Location to store error, or #NULL. + * + * Queries the information for a shared path: its share name, its read-only status, etc. + * + * Return value: #TRUE if the info could be queried successfully, #FALSE + * otherwise. If this function returns #FALSE, an error code will be returned in the + * @error argument, and *@ret_share_info will be set to #NULL. + **/ +gboolean +shares_get_share_info_for_path (const char *path, ShareInfo **ret_share_info, GError **error) +{ + ShareInfo *info; + + g_assert (path != NULL); + g_assert (ret_share_info != NULL); + g_assert (error == NULL || *error == NULL); + + if (!refresh_if_needed (error)) { + *ret_share_info = NULL; + return FALSE; + } + + info = lookup_share_by_path (path); + *ret_share_info = copy_share_info (info); + + return TRUE; +} + +/** + * shares_get_share_name_exists: + * @share_name: Name of a share. + * @ret_exists: Location to store return value; #TRUE if the share name exists, #FALSE otherwise. + * + * Queries whether a share name already exists in the user's list of shares. + * + * Return value: #TRUE if the info could be queried successfully, #FALSE + * otherwise. If this function returns #FALSE, an error code will be returned in the + * @error argument, and *@ret_exists will be set to #FALSE. + **/ +gboolean +shares_get_share_name_exists (const char *share_name, gboolean *ret_exists, GError **error) +{ + g_assert (share_name != NULL); + g_assert (ret_exists != NULL); + g_assert (error == NULL || *error == NULL); + + if (!refresh_if_needed (error)) { + *ret_exists = FALSE; + return FALSE; + } + + *ret_exists = (lookup_share_by_share_name (share_name) != NULL); + + return TRUE; +} + +/** + * shares_get_share_info_for_share_name: + * @share_name: Name of a share. + * @ret_share_info: Location to store result with the share's info - on return, + * will be non-NULL if there is a share for the specified name, or #NULL if no + * share has such name. You must free the non-NULL value with + * shares_free_share_info(). + * @error: Location to store error, or #NULL. + * + * Queries the information for the share which has a specific name. + * + * Return value: #TRUE if the info could be queried successfully, #FALSE + * otherwise. If this function returns #FALSE, an error code will be returned in the + * @error argument, and *@ret_share_info will be set to #NULL. + **/ +gboolean +shares_get_share_info_for_share_name (const char *share_name, ShareInfo **ret_share_info, GError **error) +{ + ShareInfo *info; + + g_assert (share_name != NULL); + g_assert (ret_share_info != NULL); + g_assert (error == NULL || *error == NULL); + + if (!refresh_if_needed (error)) { + *ret_share_info = NULL; + return FALSE; + } + + info = lookup_share_by_share_name (share_name); + *ret_share_info = copy_share_info (info); + + return TRUE; +} + +/** + * shares_modify_share: + * @old_path: Path of the share to modify, or %NULL. + * @info: Info of the share to modify/add, or %NULL to delete a share. + * @error: Location to store error, or #NULL. + * + * Can add, modify, or delete shares. To add a share, pass %NULL for @old_path, + * and a non-null @info. To modify a share, pass a non-null @old_path and + * non-null @info; in this case, @info->path must have the same contents as + * @old_path. To remove a share, pass a non-NULL @old_path and a %NULL @info. + * + * Return value: TRUE if the share could be modified, FALSE otherwise. If this returns + * FALSE, then the error information will be placed in @error. + **/ +gboolean +shares_modify_share (const char *old_path, ShareInfo *info, GError **error) +{ + g_assert ((old_path == NULL && info != NULL) + || (old_path != NULL && info == NULL) + || (old_path != NULL && info != NULL)); + g_assert (error == NULL || *error == NULL); + + if (!refresh_if_needed (error)) + return FALSE; + + if (old_path == NULL) + return add_share (info, error); + else if (info == NULL) + return remove_share (old_path, error); + else + return modify_share (old_path, info, error); +} + +static void +copy_to_slist_cb (gpointer key, gpointer value, gpointer data) +{ + ShareInfo *info; + ShareInfo *copy; + GSList **list; + + info = value; + list = data; + + copy = copy_share_info (info); + *list = g_slist_prepend (*list, copy); +} + +/** + * shares_get_share_info_list: + * @ret_info_list: Location to store the return value, which is a list + * of #ShareInfo structures. Free this with shares_free_share_info_list(). + * @error: Location to store error, or #NULL. + * + * Gets the list of shared folders and their information. + * + * Return value: #TRUE if the info could be queried successfully, #FALSE + * otherwise. If this function returns #FALSE, an error code will be returned in the + * @error argument, and *@ret_info_list will be set to #NULL. + **/ +gboolean +shares_get_share_info_list (GSList **ret_info_list, GError **error) +{ + g_assert (ret_info_list != NULL); + g_assert (error == NULL || *error == NULL); + + if (!refresh_if_needed (error)) { + *ret_info_list = NULL; + return FALSE; + } + + *ret_info_list = NULL; + g_hash_table_foreach (path_share_info_hash, copy_to_slist_cb, ret_info_list); + + return TRUE; +} + +/** + * shares_free_share_info_list: + * @list: List of #ShareInfo structures, or %NULL. + * + * Frees a list of #ShareInfo structures as returned by shares_get_share_info_list(). + **/ +void +shares_free_share_info_list (GSList *list) +{ + GSList *l; + + for (l = list; l; l = l->next) { + ShareInfo *info; + + info = l->data; + shares_free_share_info (l->data); + } + + g_slist_free (list); +} + +void +shares_set_debug (gboolean error_on_refresh, + gboolean error_on_add, + gboolean error_on_modify, + gboolean error_on_remove) +{ + throw_error_on_refresh = error_on_refresh; + throw_error_on_add = error_on_add; + throw_error_on_modify = error_on_modify; + throw_error_on_remove = error_on_remove; +} diff --git a/share/shares.h b/share/shares.h new file mode 100644 index 0000000..beec95f --- /dev/null +++ b/share/shares.h @@ -0,0 +1,47 @@ +#ifndef SHARES_H +#define SHARES_H + +#include <glib.h> + +typedef struct { + char *path; + char *share_name; + char *comment; + gboolean is_writable; + gboolean guest_ok; +} ShareInfo; + +#define SHARES_ERROR (shares_error_quark ()) + +typedef enum { + SHARES_ERROR_FAILED, + SHARES_ERROR_NONEXISTENT +} SharesError; + +GQuark shares_error_quark (void); + +void shares_free_share_info (ShareInfo *info); + +gboolean shares_get_path_is_shared (const char *path, gboolean *ret_is_shared, GError **error); + +gboolean shares_get_share_info_for_path (const char *path, ShareInfo **ret_share_info, GError **error); + +gboolean shares_get_share_name_exists (const char *share_name, gboolean *ret_exists, GError **error); + +gboolean shares_get_share_info_for_share_name (const char *share_name, ShareInfo **ret_share_info, GError **error); + +gboolean shares_modify_share (const char *old_path, ShareInfo *info, GError **error); + +gboolean shares_get_share_info_list (GSList **ret_info_list, GError **error); + +void shares_free_share_info_list (GSList *list); + +gboolean shares_supports_guest_ok (gboolean *supports_guest_ok_ret, + GError **error); + +void shares_set_debug (gboolean error_on_refresh, + gboolean error_on_add, + gboolean error_on_modify, + gboolean error_on_remove); + +#endif |