/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ /* gvfs - extensions for gio * * Copyright (C) 2006-2009 Red Hat, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General * Public License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * Author: David Zeuthen */ #include #include #include #include #include #include #include #include "ggdudrive.h" #include "ggduvolume.h" #include "ggdumount.h" /* for BUFSIZ */ #include typedef struct MountOpData MountOpData; static void cancel_pending_mount_op (MountOpData *data); static void g_gdu_volume_mount_unix_mount_point (GGduVolume *volume, GMountMountFlags flags, GMountOperation *mount_operation, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data); struct _GGduVolume { GObject parent; GVolumeMonitor *volume_monitor; /* owned by volume monitor */ GGduMount *mount; /* owned by volume monitor */ GGduDrive *drive; /* owned by volume monitor */ /* only set if constructed via new() */ GduVolume *gdu_volume; /* only set if constructed via new_for_unix_mount_point() */ GUnixMountPoint *unix_mount_point; /* if the volume is encrypted, this is != NULL when unlocked */ GduVolume *cleartext_gdu_volume; /* If a mount operation is in progress, then pending_mount_op is != NULL. This * is used to cancel the operation to make possible authentication dialogs go * away. */ MountOpData *pending_mount_op; /* the following members need to be set upon construction, see constructors and update_volume() */ GIcon *icon; GFile *activation_root; gchar *name; gchar *device_file; dev_t dev; gchar *uuid; gboolean can_mount; gboolean should_automount; }; static void g_gdu_volume_volume_iface_init (GVolumeIface *iface); G_DEFINE_TYPE_EXTENDED (GGduVolume, g_gdu_volume, G_TYPE_OBJECT, 0, G_IMPLEMENT_INTERFACE (G_TYPE_VOLUME, g_gdu_volume_volume_iface_init)) static void gdu_volume_changed (GduPresentable *presentable, GGduVolume *volume); static void gdu_volume_job_changed (GduPresentable *presentable, GGduVolume *volume); static void gdu_cleartext_volume_removed (GduPresentable *presentable, GGduVolume *volume); static void gdu_cleartext_volume_changed (GduPresentable *presentable, GGduVolume *volume); static void gdu_cleartext_volume_job_changed (GduPresentable *presentable, GGduVolume *volume); static void mount_with_mount_operation (MountOpData *data); static gboolean g_gdu_drive_device_is_flash(GduDevice *drive_device) { const gchar *drive_media; if (drive_device == NULL) return FALSE; drive_media = gdu_device_drive_get_media (drive_device); return g_str_has_prefix (drive_media, "flash"); } static void g_gdu_volume_finalize (GObject *object) { GGduVolume *volume; volume = G_GDU_VOLUME (object); if (volume->mount != NULL) g_gdu_mount_unset_volume (volume->mount, volume); if (volume->drive != NULL) g_gdu_drive_unset_volume (volume->drive, volume); if (volume->gdu_volume != NULL) { g_signal_handlers_disconnect_by_func (volume->gdu_volume, gdu_volume_changed, volume); g_signal_handlers_disconnect_by_func (volume->gdu_volume, gdu_volume_job_changed, volume); g_object_unref (volume->gdu_volume); } if (volume->unix_mount_point != NULL) { g_unix_mount_point_free (volume->unix_mount_point); } if (volume->cleartext_gdu_volume != NULL) { g_signal_handlers_disconnect_by_func (volume->cleartext_gdu_volume, gdu_cleartext_volume_removed, volume); g_signal_handlers_disconnect_by_func (volume->cleartext_gdu_volume, gdu_cleartext_volume_changed, volume); g_signal_handlers_disconnect_by_func (volume->cleartext_gdu_volume, gdu_cleartext_volume_job_changed, volume); g_object_unref (volume->cleartext_gdu_volume); } if (volume->icon != NULL) g_object_unref (volume->icon); if (volume->activation_root != NULL) g_object_unref (volume->activation_root); g_free (volume->name); g_free (volume->device_file); g_free (volume->uuid); if (G_OBJECT_CLASS (g_gdu_volume_parent_class)->finalize) (*G_OBJECT_CLASS (g_gdu_volume_parent_class)->finalize) (object); } static void g_gdu_volume_class_init (GGduVolumeClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gobject_class->finalize = g_gdu_volume_finalize; } static void g_gdu_volume_init (GGduVolume *gdu_volume) { } static void emit_changed (GGduVolume *volume) { g_signal_emit_by_name (volume, "changed"); g_signal_emit_by_name (volume->volume_monitor, "volume_changed", volume); } static gboolean update_volume (GGduVolume *volume) { GduDevice *device; GduPool *pool = NULL; time_t now; gboolean changed; gboolean old_can_mount; gboolean old_should_automount; gchar *old_name; gchar *old_device_file; dev_t old_dev; GIcon *old_icon; gboolean keep_cleartext_volume; /* save old values */ old_can_mount = volume->can_mount; old_should_automount = volume->should_automount; old_name = g_strdup (volume->name); old_device_file = g_strdup (volume->device_file); old_dev = volume->dev; old_icon = volume->icon != NULL ? g_object_ref (volume->icon) : NULL; /* ---------------------------------------------------------------------------------------------------- */ /* if the volume is a fstab mount point, get the data from there */ if (volume->unix_mount_point != NULL) { struct stat_buf; volume->can_mount = TRUE; volume->should_automount = FALSE; g_free (volume->device_file); volume->device_file = g_strdup (g_unix_mount_point_get_device_path (volume->unix_mount_point)); volume->dev = 0; if (volume->icon != NULL) g_object_unref (volume->icon); if (g_strcmp0 (g_unix_mount_point_get_fs_type (volume->unix_mount_point), "nfs") == 0) volume->icon = g_themed_icon_new_with_default_fallbacks ("folder-remote"); else volume->icon = g_unix_mount_point_guess_icon (volume->unix_mount_point); g_free (volume->name); volume->name = g_unix_mount_point_guess_name (volume->unix_mount_point); //volume->can_eject = g_unix_mount_point_guess_can_eject (volume->unix_mount_point); goto update_done; } /* in with the new */ device = gdu_presentable_get_device (GDU_PRESENTABLE (volume->gdu_volume)); if (device != NULL) pool = gdu_device_get_pool (device); keep_cleartext_volume = FALSE; if (device != NULL && gdu_device_is_luks (device)) { const gchar *holder_objpath; holder_objpath = gdu_device_luks_get_holder (device); if (holder_objpath != NULL && g_strcmp0 (holder_objpath, "/") != 0) { GduDevice *cleartext_device; cleartext_device = gdu_pool_get_by_object_path (pool, holder_objpath); if (cleartext_device != NULL) { GduVolume *cleartext_gdu_volume; cleartext_gdu_volume = GDU_VOLUME (gdu_pool_get_volume_by_device (pool, cleartext_device)); if (cleartext_gdu_volume != volume->cleartext_gdu_volume) { if (volume->cleartext_gdu_volume != NULL) { g_signal_handlers_disconnect_by_func (volume->cleartext_gdu_volume, gdu_cleartext_volume_removed, volume); g_signal_handlers_disconnect_by_func (volume->cleartext_gdu_volume, gdu_cleartext_volume_changed, volume); g_signal_handlers_disconnect_by_func (volume->cleartext_gdu_volume, gdu_cleartext_volume_job_changed, volume); g_object_unref (volume->cleartext_gdu_volume); } volume->cleartext_gdu_volume = g_object_ref (cleartext_gdu_volume); g_signal_connect (volume->cleartext_gdu_volume, "removed", G_CALLBACK (gdu_cleartext_volume_removed), volume); g_signal_connect (volume->cleartext_gdu_volume, "changed", G_CALLBACK (gdu_cleartext_volume_changed), volume); g_signal_connect (volume->cleartext_gdu_volume, "job-changed", G_CALLBACK (gdu_cleartext_volume_job_changed), volume); } g_object_unref (cleartext_gdu_volume); g_object_unref (cleartext_device); keep_cleartext_volume = TRUE; } } } if (!keep_cleartext_volume) { if (volume->cleartext_gdu_volume != NULL) { g_signal_handlers_disconnect_by_func (volume->cleartext_gdu_volume, gdu_cleartext_volume_removed, volume); g_signal_handlers_disconnect_by_func (volume->cleartext_gdu_volume, gdu_cleartext_volume_changed, volume); g_signal_handlers_disconnect_by_func (volume->cleartext_gdu_volume, gdu_cleartext_volume_job_changed, volume); g_object_unref (volume->cleartext_gdu_volume); volume->cleartext_gdu_volume = NULL; } } /* Use data from cleartext LUKS volume if it is unlocked */ if (volume->cleartext_gdu_volume != NULL) { GduDevice *luks_cleartext_volume_device; luks_cleartext_volume_device = gdu_presentable_get_device (GDU_PRESENTABLE (volume->cleartext_gdu_volume)); if (volume->icon != NULL) g_object_unref (volume->icon); volume->icon = gdu_presentable_get_icon (GDU_PRESENTABLE (volume->cleartext_gdu_volume)); g_free (volume->name); volume->name = gdu_presentable_get_name (GDU_PRESENTABLE (volume->cleartext_gdu_volume)); g_free (volume->device_file); if (luks_cleartext_volume_device != NULL) { volume->device_file = g_strdup (gdu_device_get_device_file (luks_cleartext_volume_device)); volume->dev = gdu_device_get_dev (luks_cleartext_volume_device); } else { volume->device_file = NULL; volume->dev = 0; } volume->can_mount = TRUE; volume->should_automount = FALSE; if (luks_cleartext_volume_device != NULL) g_object_unref (luks_cleartext_volume_device); } else { gchar *activation_uri; if (volume->icon != NULL) g_object_unref (volume->icon); volume->icon = gdu_presentable_get_icon (GDU_PRESENTABLE (volume->gdu_volume)); g_free (volume->name); if (_is_pc_floppy_drive (device)) volume->name = g_strdup (_("Floppy Disk")); else volume->name = gdu_presentable_get_name (GDU_PRESENTABLE (volume->gdu_volume)); /* special case the name and icon for audio discs */ activation_uri = volume->activation_root != NULL ? g_file_get_uri (volume->activation_root) : NULL; if (activation_uri != NULL && g_str_has_prefix (activation_uri, "cdda://")) { if (volume->icon != NULL) g_object_unref (volume->icon); volume->icon = g_themed_icon_new_with_default_fallbacks ("media-optical-audio"); g_free (volume->name); volume->name = g_strdup (_("Audio Disc")); } g_free (volume->device_file); if (device != NULL) { volume->device_file = g_strdup (gdu_device_get_device_file (device)); volume->dev = gdu_device_get_dev (device); } else { volume->device_file = NULL; volume->dev = 0; } volume->can_mount = TRUE; /* Only automount filesystems from drives of known types/interconnects: * * - USB * - Firewire * - sdio * - optical discs * * The mantra here is "be careful" - we really don't want to * automount fs'es from all devices in a SAN etc - We REALLY * need to be CAREFUL here. * * Sidebar: Actually, a surprisingly large number of admins like * to log into GNOME as root (thus having all polkit * authorizations) and if weren't careful we'd * automount all mountable devices from the box. See * the enterprise distro bug trackers for details. */ volume->should_automount = FALSE; if (volume->drive != NULL) { GduPresentable *drive_presentable; drive_presentable = g_gdu_drive_get_presentable (volume->drive); if (drive_presentable != NULL) { GduDevice *drive_device; drive_device = gdu_presentable_get_device (drive_presentable); if (drive_device != NULL) { if (gdu_device_is_drive (drive_device)) { const gchar *connection_interface; connection_interface = gdu_device_drive_get_connection_interface (drive_device); if (g_strcmp0 (connection_interface, "usb") == 0 || g_strcmp0 (connection_interface, "firewire") == 0 || g_strcmp0 (connection_interface, "sdio") == 0 || g_gdu_drive_device_is_flash (drive_device) || gdu_device_is_optical_disc (drive_device)) { volume->should_automount = TRUE; } } g_object_unref (drive_device); } } /* If a volume (partition) appear _much later_ than when media was inserted it * can only be because the media was repartitioned. We don't want to automount * such volumes. */ now = time (NULL); if (now - g_gdu_drive_get_time_of_last_media_insertion (volume->drive) > 5) volume->should_automount = FALSE; } /* Respect the presentation hint for whether the volume should be automounted - normally * nopolicy is only FALSE for "physical" devices - e.g. only "physical" devices will * be set to be automounted. */ if (device != NULL && gdu_device_get_presentation_nopolicy (device)) volume->should_automount = FALSE; g_free (activation_uri); } /* if there's an automount hint, use it */ if (device != NULL) { if (g_strcmp0 (gdu_device_get_automount_hint (device), "always") == 0) volume->should_automount = TRUE; else if (g_strcmp0 (gdu_device_get_automount_hint (device), "never") == 0) volume->should_automount = FALSE; } if (pool != NULL) g_object_unref (pool); if (device != NULL) g_object_unref (device); /* ---------------------------------------------------------------------------------------------------- */ update_done: /* compute whether something changed */ changed = !((old_can_mount == volume->can_mount) && (old_should_automount == volume->should_automount) && (g_strcmp0 (old_name, volume->name) == 0) && (g_strcmp0 (old_device_file, volume->device_file) == 0) && (old_dev == volume->dev) && g_icon_equal (old_icon, volume->icon) ); /* free old values */ g_free (old_name); g_free (old_device_file); if (old_icon != NULL) g_object_unref (old_icon); /*g_debug ("in update_volume(), changed=%d", changed);*/ return changed; } static void gdu_volume_changed (GduPresentable *presentable, GGduVolume *volume) { /*g_debug ("volume: presentable_changed: %p: %s", volume, gdu_presentable_get_id (GDU_PRESENTABLE (presentable)));*/ if (update_volume (volume)) emit_changed (volume); } static void gdu_volume_job_changed (GduPresentable *presentable, GGduVolume *volume) { /*g_debug ("volume: presentable_job_changed %p: %s", volume, gdu_presentable_get_id (GDU_PRESENTABLE (presentable)));*/ if (update_volume (volume)) emit_changed (volume); } static void gdu_cleartext_volume_removed (GduPresentable *presentable, GGduVolume *volume) { /*g_debug ("cleartext volume: presentable_removed: %p: %s", volume, gdu_presentable_get_id (GDU_PRESENTABLE (presentable)));*/ if (update_volume (volume)) emit_changed (volume); } static void gdu_cleartext_volume_changed (GduPresentable *presentable, GGduVolume *volume) { /*g_debug ("cleartext volume: presentable_changed: %p: %s", volume, gdu_presentable_get_id (GDU_PRESENTABLE (presentable)));*/ if (update_volume (volume)) emit_changed (volume); } static void gdu_cleartext_volume_job_changed (GduPresentable *presentable, GGduVolume *volume) { /*g_debug ("cleartext volume: presentable_job_changed %p: %s", volume, gdu_presentable_get_id (GDU_PRESENTABLE (presentable)));*/ if (update_volume (volume)) emit_changed (volume); } GGduVolume * g_gdu_volume_new_for_unix_mount_point (GVolumeMonitor *volume_monitor, GUnixMountPoint *unix_mount_point) { GGduVolume *volume; volume = g_object_new (G_TYPE_GDU_VOLUME, NULL); volume->volume_monitor = volume_monitor; g_object_add_weak_pointer (G_OBJECT (volume_monitor), (gpointer) &(volume->volume_monitor)); volume->unix_mount_point = unix_mount_point; update_volume (volume); return volume; } GGduVolume * g_gdu_volume_new (GVolumeMonitor *volume_monitor, GduVolume *gdu_volume, GGduDrive *drive, GFile *activation_root) { GGduVolume *volume; volume = g_object_new (G_TYPE_GDU_VOLUME, NULL); volume->volume_monitor = volume_monitor; g_object_add_weak_pointer (G_OBJECT (volume_monitor), (gpointer) &(volume->volume_monitor)); volume->gdu_volume = g_object_ref (gdu_volume); volume->activation_root = activation_root != NULL ? g_object_ref (activation_root) : NULL; g_signal_connect (volume->gdu_volume, "changed", G_CALLBACK (gdu_volume_changed), volume); g_signal_connect (volume->gdu_volume, "job-changed", G_CALLBACK (gdu_volume_job_changed), volume); volume->drive = drive; if (drive != NULL) g_gdu_drive_set_volume (drive, volume); update_volume (volume); return volume; } void g_gdu_volume_removed (GGduVolume *volume) { if (volume->pending_mount_op != NULL) cancel_pending_mount_op (volume->pending_mount_op); if (volume->mount != NULL) { g_gdu_mount_unset_volume (volume->mount, volume); volume->mount = NULL; } if (volume->drive != NULL) { g_gdu_drive_unset_volume (volume->drive, volume); volume->drive = NULL; } } void g_gdu_volume_set_mount (GGduVolume *volume, GGduMount *mount) { if (volume->mount != mount) { if (volume->mount != NULL) g_gdu_mount_unset_volume (volume->mount, volume); volume->mount = mount; emit_changed (volume); } } void g_gdu_volume_unset_mount (GGduVolume *volume, GGduMount *mount) { if (volume->mount == mount) { volume->mount = NULL; emit_changed (volume); } } void g_gdu_volume_set_drive (GGduVolume *volume, GGduDrive *drive) { if (volume->drive != drive) { if (volume->drive != NULL) g_gdu_drive_unset_volume (volume->drive, volume); volume->drive = drive; emit_changed (volume); } } void g_gdu_volume_unset_drive (GGduVolume *volume, GGduDrive *drive) { if (volume->drive == drive) { volume->drive = NULL; emit_changed (volume); } } static GIcon * g_gdu_volume_get_icon (GVolume *_volume) { GGduVolume *volume = G_GDU_VOLUME (_volume); return volume->icon != NULL ? g_object_ref (volume->icon) : NULL; } static char * g_gdu_volume_get_name (GVolume *_volume) { GGduVolume *volume = G_GDU_VOLUME (_volume); return g_strdup (volume->name); } static char * g_gdu_volume_get_uuid (GVolume *_volume) { GGduVolume *volume = G_GDU_VOLUME (_volume); return g_strdup (volume->uuid); } static gboolean g_gdu_volume_can_mount (GVolume *_volume) { GGduVolume *volume = G_GDU_VOLUME (_volume); return volume->can_mount; } static gboolean g_gdu_volume_can_eject (GVolume *_volume) { GGduVolume *volume = G_GDU_VOLUME (_volume); gboolean can_eject; can_eject = FALSE; if (volume->drive != NULL) can_eject = g_drive_can_eject (G_DRIVE (volume->drive)); return can_eject; } static gboolean g_gdu_volume_should_automount (GVolume *_volume) { GGduVolume *volume = G_GDU_VOLUME (_volume); return volume->should_automount; } static GDrive * g_gdu_volume_get_drive (GVolume *volume) { GGduVolume *gdu_volume = G_GDU_VOLUME (volume); GDrive *drive; drive = NULL; if (gdu_volume->drive != NULL) drive = g_object_ref (gdu_volume->drive); return drive; } static GMount * g_gdu_volume_get_mount (GVolume *volume) { GGduVolume *gdu_volume = G_GDU_VOLUME (volume); GMount *mount; mount = NULL; if (gdu_volume->mount != NULL) mount = g_object_ref (gdu_volume->mount); return mount; } /* ---------------------------------------------------------------------------------------------------- */ static gchar ** get_mount_options (GduDevice *device, gboolean allow_user_interaction) { GPtrArray *p; p = g_ptr_array_new (); /* one day we might read this from user settings */ if (g_strcmp0 (gdu_device_id_get_usage (device), "filesystem") == 0 && g_strcmp0 (gdu_device_id_get_type (device), "vfat") == 0) { g_ptr_array_add (p, g_strdup ("flush")); } if (!allow_user_interaction) { g_ptr_array_add (p, g_strdup ("auth_no_user_interaction")); } g_ptr_array_add (p, NULL); return (gchar **) g_ptr_array_free (p, FALSE); } /* ---------------------------------------------------------------------------------------------------- */ struct MountOpData { GGduVolume *volume; GduDevice *device_to_mount; GSimpleAsyncResult *simple; GCancellable *cancellable; gulong cancelled_handler_id; GMountOperation *mount_operation; gulong mount_operation_reply_handler_id; gboolean is_cancelled; }; static void mount_op_data_unref (MountOpData *data) { g_object_unref (data->volume); if (data->device_to_mount != NULL) g_object_unref (data->device_to_mount); g_object_unref (data->simple); if (data->cancelled_handler_id != 0) g_signal_handler_disconnect (data->cancellable, data->cancelled_handler_id); if (data->cancellable != NULL) g_object_unref (data->cancellable); if (data->mount_operation_reply_handler_id != 0) g_signal_handler_disconnect (data->mount_operation, data->mount_operation_reply_handler_id); if (data->mount_operation != NULL) g_object_unref (data->mount_operation); g_free (data); } static void cancel_pending_mount_op (MountOpData *data) { /* we are no longer pending */ data->volume->pending_mount_op = NULL; data->is_cancelled = TRUE; /* send an ::aborted signal to make the dialog go away */ if (data->mount_operation != NULL) g_signal_emit_by_name (data->mount_operation, "aborted"); /* complete the operation (sends reply to caller) */ g_simple_async_result_set_error (data->simple, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED, "Operation was cancelled"); g_simple_async_result_complete (data->simple); } static void mount_cb (GduDevice *device, gchar *mount_point, GError *error, gpointer user_data) { MountOpData *data = user_data; /* if we've already aborted due to device removal / cancellation, just bail out */ if (data->is_cancelled) goto bailout; if (error != NULL) { /* be quiet if the DeviceKit-disks daemon is inhibited */ if (error->code == GDU_ERROR_INHIBITED) { error->domain = G_IO_ERROR; error->code = G_IO_ERROR_FAILED_HANDLED; } g_simple_async_result_set_from_error (data->simple, error); g_error_free (error); } else { g_free (mount_point); } g_simple_async_result_complete (data->simple); bailout: data->volume->pending_mount_op = NULL; mount_op_data_unref (data); } static void mount_cleartext_device (MountOpData *data, const gchar *object_path_of_cleartext_device) { GduPool *pool; /* if we've already aborted due to device removal / cancellation, just bail out */ if (data->is_cancelled) { mount_op_data_unref (data); goto bailout; } pool = gdu_presentable_get_pool (GDU_PRESENTABLE (data->volume->gdu_volume)); data->device_to_mount = gdu_pool_get_by_object_path (pool, object_path_of_cleartext_device); if (data->device_to_mount == NULL) { g_simple_async_result_set_error (data->simple, G_IO_ERROR, G_IO_ERROR_FAILED, "Successfully unlocked encrypted volume but cleartext device does not exist"); g_simple_async_result_complete (data->simple); data->volume->pending_mount_op = NULL; mount_op_data_unref (data); } else { gchar **mount_options; mount_options = get_mount_options (data->device_to_mount, data->mount_operation != NULL); gdu_device_op_filesystem_mount (data->device_to_mount, mount_options, mount_cb, data); g_strfreev (mount_options); } g_object_unref (pool); bailout: ; } static void unlock_from_keyring_cb (GduDevice *device, char *object_path_of_cleartext_device, GError *error, gpointer user_data) { MountOpData *data = user_data; /* if we've already aborted due to device removal / cancellation, just bail out */ if (data->is_cancelled) { mount_op_data_unref (data); goto bailout; } if (error != NULL) { /*g_debug ("keyring password didn't work: %s", error->message);*/ /* The password we retrieved from the keyring didn't work. So go ahead and prompt * the user. */ mount_with_mount_operation (data); g_error_free (error); } else { mount_cleartext_device (data, object_path_of_cleartext_device); g_free (object_path_of_cleartext_device); } bailout: ; } static void unlock_cb (GduDevice *device, gchar *object_path_of_cleartext_device, GError *error, gpointer user_data) { MountOpData *data = user_data; /* if we've already aborted due to device removal / cancellation, just bail out */ if (data->is_cancelled) { mount_op_data_unref (data); goto bailout; } if (error != NULL) { /* be quiet if the daemon is inhibited */ if (error->code == GDU_ERROR_INHIBITED) { error->domain = G_IO_ERROR; error->code = G_IO_ERROR_FAILED_HANDLED; } g_simple_async_result_set_from_error (data->simple, error); g_error_free (error); g_simple_async_result_complete (data->simple); data->volume->pending_mount_op = NULL; mount_op_data_unref (data); } else { GPasswordSave password_save; const gchar *password; password_save = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (device), "password-save")); password = g_object_get_data (G_OBJECT (device), "password"); if (password != NULL) { switch (password_save) { case G_PASSWORD_SAVE_FOR_SESSION: gdu_util_save_secret (device, password, TRUE); break; case G_PASSWORD_SAVE_PERMANENTLY: gdu_util_save_secret (device, password, FALSE); break; default: /* do nothing */ break; } } /* now we have a cleartext device; update the GVolume details to show that */ if (update_volume (data->volume)) emit_changed (data->volume); mount_cleartext_device (data, object_path_of_cleartext_device); g_free (object_path_of_cleartext_device); } bailout: /* scrub the password */ g_object_set_data (G_OBJECT (device), "password-save", NULL); g_object_set_data (G_OBJECT (device), "password", NULL); } static void scrub_n_free_string (char *password) { memset (password, '\0', strlen (password)); g_free (password); } static void mount_operation_reply (GMountOperation *mount_operation, GMountOperationResult result, gpointer user_data) { MountOpData *data = user_data; GduDevice *device; const gchar *password; /* if we've already aborted due to device removal, just bail out */ if (data->is_cancelled) { mount_op_data_unref (data); goto out; } /* we got what we wanted; don't listen to any other signals from the mount operation */ if (data->mount_operation_reply_handler_id != 0) { g_signal_handler_disconnect (data->mount_operation, data->mount_operation_reply_handler_id); data->mount_operation_reply_handler_id = 0; } if (result != G_MOUNT_OPERATION_HANDLED) { if (result == G_MOUNT_OPERATION_ABORTED) { /* The user aborted the operation so consider it "handled" */ g_simple_async_result_set_error (data->simple, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED, "Password dialog aborted (user should never see this error since it is G_IO_ERROR_FAILED_HANDLED)"); } else { g_simple_async_result_set_error (data->simple, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, "Expected G_MOUNT_OPERATION_HANDLED but got %d", result); } g_simple_async_result_complete (data->simple); data->volume->pending_mount_op = NULL; mount_op_data_unref (data); goto out; } password = g_mount_operation_get_password (mount_operation); device = gdu_presentable_get_device (GDU_PRESENTABLE (data->volume->gdu_volume)); g_object_set_data (G_OBJECT (device), "password-save", GINT_TO_POINTER (g_mount_operation_get_password_save (mount_operation))); g_object_set_data_full (G_OBJECT (device), "password", g_strdup (password), (GDestroyNotify) scrub_n_free_string); gdu_device_op_luks_unlock (device, password, unlock_cb, data); g_object_unref (device); out: ; } static void mount_with_mount_operation (MountOpData *data) { gchar *message; gchar *drive_name; GduPresentable *toplevel; GduDevice *device; device = NULL; drive_name = NULL; message = NULL; toplevel = NULL; /* if we've already aborted due to device removal, just bail out */ if (data->is_cancelled) { mount_op_data_unref (data); goto out; } if (data->mount_operation == NULL) { g_simple_async_result_set_error (data->simple, G_IO_ERROR, G_IO_ERROR_FAILED, "Password required to access the encrypted data"); g_simple_async_result_complete (data->simple); data->volume->pending_mount_op = NULL; mount_op_data_unref (data); goto out; } device = gdu_presentable_get_device (GDU_PRESENTABLE (data->volume->gdu_volume)); toplevel = gdu_presentable_get_enclosing_presentable (GDU_PRESENTABLE (data->volume->gdu_volume)); /* handle logical partitions enclosed by an extented partition */ if (GDU_IS_VOLUME (toplevel)) { GduPresentable *temp; temp = toplevel; toplevel = gdu_presentable_get_enclosing_presentable (toplevel); g_object_unref (temp); if (!GDU_IS_DRIVE (toplevel)) { g_object_unref (toplevel); toplevel = NULL; } } if (toplevel != NULL) drive_name = gdu_presentable_get_name (toplevel); if (drive_name != NULL) { if (gdu_device_is_partition (device)) { message = g_strdup_printf (_("Enter a password to unlock the volume\n" "The device ā€œ%sā€ contains encrypted data on partition %d."), drive_name, gdu_device_partition_get_number (device)); } else { message = g_strdup_printf (_("Enter a password to unlock the volume\n" "The device ā€œ%sā€ contains encrypted data."), drive_name); } } else { message = g_strdup_printf (_("Enter a password to unlock the volume\n" "The device %s contains encrypted data."), gdu_device_get_device_file (device)); } data->mount_operation_reply_handler_id = g_signal_connect (data->mount_operation, "reply", G_CALLBACK (mount_operation_reply), data); g_signal_emit_by_name (data->mount_operation, "ask-password", message, NULL, NULL, G_ASK_PASSWORD_NEED_PASSWORD | G_ASK_PASSWORD_SAVING_SUPPORTED); out: g_free (drive_name); g_free (message); if (device != NULL) g_object_unref (device); if (toplevel != NULL) g_object_unref (toplevel); } static void cancelled_cb (GCancellable *cancellable, GGduVolume *volume) { if (volume->pending_mount_op != NULL) { cancel_pending_mount_op (volume->pending_mount_op); } } static void g_gdu_volume_mount (GVolume *_volume, GMountMountFlags flags, GMountOperation *mount_operation, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GGduVolume *volume = G_GDU_VOLUME (_volume); GSimpleAsyncResult *simple; GduDevice *device; GduPool *pool; const gchar *usage; const gchar *type; MountOpData *data; pool = NULL; device = NULL; /* for fstab mounts, call the native mount command */ if (volume->unix_mount_point != NULL) { g_gdu_volume_mount_unix_mount_point (volume, flags, mount_operation, cancellable, callback, user_data); goto out; } if (volume->pending_mount_op != NULL) { simple = g_simple_async_result_new_error (G_OBJECT (volume), callback, user_data, G_IO_ERROR, G_IO_ERROR_FAILED, "A mount operation is already pending"); g_simple_async_result_complete (simple); g_object_unref (simple); goto out; } device = gdu_presentable_get_device (GDU_PRESENTABLE (volume->gdu_volume)); if (device == NULL) { simple = g_simple_async_result_new_error (G_OBJECT (volume), callback, user_data, G_IO_ERROR, G_IO_ERROR_FAILED, "Underlying device missing"); g_simple_async_result_complete (simple); g_object_unref (simple); goto out; } pool = gdu_device_get_pool (device); /* Makes no sense to mount * * - blank discs since these already have a burn:/// mount * - other things that are already mounted * * Unfortunately Nautilus will try to do this anyway. For now, just return success for * such requests. */ if (gdu_device_optical_disc_get_is_blank (device) || gdu_device_is_mounted (device)) { simple = g_simple_async_result_new (G_OBJECT (volume), callback, user_data, g_gdu_volume_mount); g_simple_async_result_complete (simple); g_object_unref (simple); goto out; } data = g_new0 (MountOpData, 1); data->volume = g_object_ref (volume); data->simple = g_simple_async_result_new (G_OBJECT (volume), callback, user_data, g_gdu_volume_mount); data->cancellable = cancellable != NULL ? g_object_ref (cancellable) : NULL; data->mount_operation = mount_operation != NULL ? g_object_ref (mount_operation) : NULL; if (data->cancellable != NULL) data->cancelled_handler_id = g_signal_connect (data->cancellable, "cancelled", G_CALLBACK (cancelled_cb), volume); volume->pending_mount_op = data; /* if the device is already unlocked, just attempt to mount it */ if (volume->cleartext_gdu_volume != NULL) { GduDevice *luks_cleartext_volume_device; const gchar *object_path_of_cleartext_device; luks_cleartext_volume_device = gdu_presentable_get_device (GDU_PRESENTABLE (volume->cleartext_gdu_volume)); if (luks_cleartext_volume_device != NULL) { object_path_of_cleartext_device = gdu_device_get_object_path (luks_cleartext_volume_device); mount_cleartext_device (data, object_path_of_cleartext_device); g_object_unref (luks_cleartext_volume_device); } goto out; } usage = gdu_device_id_get_usage (device); type = gdu_device_id_get_type (device); if (g_strcmp0 (usage, "crypto") == 0 && g_strcmp0 (type, "crypto_LUKS") == 0) { gchar *password; /* if we have the secret in the keyring, try with that first */ password = gdu_util_get_secret (device); if (password != NULL) { gdu_device_op_luks_unlock (device, password, unlock_from_keyring_cb, data); scrub_n_free_string (password); goto out; } /* don't put up a password dialog if the daemon is inhibited */ if (gdu_pool_is_daemon_inhibited (pool)) { g_simple_async_result_set_error (data->simple, G_IO_ERROR, G_IO_ERROR_FAILED_HANDLED, "Daemon is currently inhibited"); g_simple_async_result_complete (data->simple); volume->pending_mount_op = NULL; mount_op_data_unref (data); goto out; } mount_with_mount_operation (data); } else { gchar **mount_options; data->device_to_mount = g_object_ref (device); mount_options = get_mount_options (data->device_to_mount, data->mount_operation != NULL); gdu_device_op_filesystem_mount (data->device_to_mount, mount_options, mount_cb, data); g_strfreev (mount_options); } out: if (pool != NULL) g_object_unref (pool); if (device != NULL) g_object_unref (device); } static gboolean g_gdu_volume_mount_finish (GVolume *volume, GAsyncResult *result, GError **error) { GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result); //g_warn_if_fail (g_simple_async_result_get_source_tag (simple) == g_gdu_volume_mount); return !g_simple_async_result_propagate_error (simple, error); } /* ---------------------------------------------------------------------------------------------------- */ typedef struct { GGduVolume *volume; GAsyncReadyCallback callback; gpointer user_data; GCancellable *cancellable; int error_fd; GIOChannel *error_channel; guint error_channel_source_id; GString *error_string; guint wait_for_mount_timeout_id; gulong wait_for_mount_changed_signal_handler_id; } MountPointOp; static void mount_point_op_free (MountPointOp *data) { if (data->error_channel_source_id > 0) g_source_remove (data->error_channel_source_id); if (data->error_channel != NULL) g_io_channel_unref (data->error_channel); if (data->error_string != NULL) g_string_free (data->error_string, TRUE); if (data->error_fd > 0) close (data->error_fd); g_free (data); } static void mount_point_op_changed_cb (GVolume *volume, gpointer user_data) { MountPointOp *data = user_data; GSimpleAsyncResult *simple; /* keep waiting if the mount hasn't appeared */ if (data->volume->mount == NULL) goto out; simple = g_simple_async_result_new (G_OBJECT (data->volume), data->callback, data->user_data, NULL); /* complete in idle to make sure the mount is added before we return */ g_simple_async_result_complete_in_idle (simple); g_object_unref (simple); g_signal_handler_disconnect (data->volume, data->wait_for_mount_changed_signal_handler_id); g_source_remove (data->wait_for_mount_timeout_id); mount_point_op_free (data); out: ; } static gboolean mount_point_op_never_appeared_cb (gpointer user_data) { MountPointOp *data = user_data; GSimpleAsyncResult *simple; simple = g_simple_async_result_new_error (G_OBJECT (data->volume), data->callback, data->user_data, G_IO_ERROR, G_IO_ERROR_FAILED, "Timeout waiting for mount to appear"); g_simple_async_result_complete (simple); g_object_unref (simple); g_signal_handler_disconnect (data->volume, data->wait_for_mount_changed_signal_handler_id); g_source_remove (data->wait_for_mount_timeout_id); mount_point_op_free (data); return FALSE; } static void mount_point_op_cb (GPid pid, gint status, gpointer user_data) { MountPointOp *data = user_data; GSimpleAsyncResult *simple; g_spawn_close_pid (pid); if (WEXITSTATUS (status) != 0) { GError *error; error = g_error_new_literal (G_IO_ERROR, G_IO_ERROR_FAILED, data->error_string->str); simple = g_simple_async_result_new_from_error (G_OBJECT (data->volume), data->callback, data->user_data, error); g_error_free (error); g_simple_async_result_complete (simple); g_object_unref (simple); mount_point_op_free (data); } else { /* wait for the GMount to appear - this is to honor this requirement * * "If the mount operation succeeded, g_volume_get_mount() on * volume is guaranteed to return the mount right after calling * this function; there's no need to listen for the * 'mount-added' signal on GVolumeMonitor." * * So we set up a signal handler waiting for it to appear. We also set up * a timer for handling the case when it never appears. */ if (data->volume->mount == NULL) { /* no need to ref, GSimpleAsyncResult has a ref on data->volume */ data->wait_for_mount_timeout_id = g_timeout_add (5 * 1000, mount_point_op_never_appeared_cb, data); data->wait_for_mount_changed_signal_handler_id = g_signal_connect (data->volume, "changed", G_CALLBACK (mount_point_op_changed_cb), data); } else { /* have the mount already, finish up */ simple = g_simple_async_result_new (G_OBJECT (data->volume), data->callback, data->user_data, NULL); g_simple_async_result_complete (simple); g_object_unref (simple); mount_point_op_free (data); } } } static gboolean mount_point_op_read_error (GIOChannel *channel, GIOCondition condition, gpointer user_data) { MountPointOp *data = user_data; gchar buf[BUFSIZ]; gsize bytes_read; GError *error; GIOStatus status; error = NULL; read: status = g_io_channel_read_chars (channel, buf, sizeof (buf), &bytes_read, &error); if (status == G_IO_STATUS_NORMAL) { g_string_append_len (data->error_string, buf, bytes_read); if (bytes_read == sizeof (buf)) goto read; } else if (status == G_IO_STATUS_EOF) { g_string_append_len (data->error_string, buf, bytes_read); } else if (status == G_IO_STATUS_ERROR) { if (data->error_string->len > 0) g_string_append (data->error_string, "\n"); g_string_append (data->error_string, error->message); g_error_free (error); return FALSE; } return TRUE; } static void g_gdu_volume_mount_unix_mount_point (GGduVolume *volume, GMountMountFlags flags, GMountOperation *mount_operation, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { MountPointOp *data; GPid child_pid; GError *error; const gchar *argv[] = {"mount", NULL, NULL}; argv[1] = g_unix_mount_point_get_mount_path (volume->unix_mount_point); data = g_new0 (MountPointOp, 1); data->volume = volume; data->callback = callback; data->user_data = user_data; data->cancellable = cancellable; error = NULL; if (!g_spawn_async_with_pipes (NULL, /* working dir */ (gchar **) argv, NULL, /* envp */ G_SPAWN_DO_NOT_REAP_CHILD|G_SPAWN_SEARCH_PATH, NULL, /* child_setup */ NULL, /* user_data for child_setup */ &child_pid, NULL, /* standard_input */ NULL, /* standard_output */ &(data->error_fd), &error)) { g_assert (error != NULL); goto handle_error; } data->error_string = g_string_new (""); data->error_channel = g_io_channel_unix_new (data->error_fd); g_io_channel_set_flags (data->error_channel, G_IO_FLAG_NONBLOCK, &error); if (error != NULL) goto handle_error; data->error_channel_source_id = g_io_add_watch (data->error_channel, G_IO_IN, mount_point_op_read_error, data); g_child_watch_add (child_pid, mount_point_op_cb, data); handle_error: if (error != NULL) { GSimpleAsyncResult *simple; simple = g_simple_async_result_new_from_error (G_OBJECT (data->volume), data->callback, data->user_data, error); g_simple_async_result_complete (simple); g_object_unref (simple); mount_point_op_free (data); } } /* ---------------------------------------------------------------------------------------------------- */ typedef struct { GObject *object; GAsyncReadyCallback callback; gpointer user_data; } EjectWrapperOp; static void eject_wrapper_callback (GObject *source_object, GAsyncResult *res, gpointer user_data) { EjectWrapperOp *data = user_data; data->callback (data->object, res, data->user_data); g_object_unref (data->object); g_free (data); } static void g_gdu_volume_eject_with_operation (GVolume *volume, GMountUnmountFlags flags, GMountOperation *mount_operation, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GGduVolume *gdu_volume = G_GDU_VOLUME (volume); GGduDrive *drive; drive = NULL; if (gdu_volume->drive != NULL) drive = g_object_ref (gdu_volume->drive); if (drive != NULL) { EjectWrapperOp *data; data = g_new0 (EjectWrapperOp, 1); data->object = g_object_ref (volume); data->callback = callback; data->user_data = user_data; g_drive_eject_with_operation (G_DRIVE (drive), flags, mount_operation, cancellable, eject_wrapper_callback, data); g_object_unref (drive); } else { GSimpleAsyncResult *simple; simple = g_simple_async_result_new_error (G_OBJECT (volume), callback, user_data, G_IO_ERROR, G_IO_ERROR_FAILED, _("Operation not supported by backend")); g_simple_async_result_complete (simple); g_object_unref (simple); } } static gboolean g_gdu_volume_eject_with_operation_finish (GVolume *volume, GAsyncResult *result, GError **error) { GGduVolume *gdu_volume = G_GDU_VOLUME (volume); gboolean res; res = TRUE; if (gdu_volume->drive != NULL) { res = g_drive_eject_with_operation_finish (G_DRIVE (gdu_volume->drive), result, error); } else { g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error); res = FALSE; } return res; } static void g_gdu_volume_eject (GVolume *volume, GMountUnmountFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_gdu_volume_eject_with_operation (volume, flags, NULL, cancellable, callback, user_data); } static gboolean g_gdu_volume_eject_finish (GVolume *volume, GAsyncResult *result, GError **error) { return g_gdu_volume_eject_with_operation_finish (volume, result, error); } static char * g_gdu_volume_get_identifier (GVolume *_volume, const char *kind) { GGduVolume *volume = G_GDU_VOLUME (_volume); GduDevice *device; const gchar *label; const gchar *uuid; gchar *id; id = NULL; if (volume->gdu_volume != NULL) { device = gdu_presentable_get_device (GDU_PRESENTABLE (volume->gdu_volume)); if (device != NULL) { label = gdu_device_id_get_label (device); uuid = gdu_device_id_get_uuid (device); g_object_unref (device); if (strcmp (kind, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE) == 0) id = g_strdup (volume->device_file); else if (strcmp (kind, G_VOLUME_IDENTIFIER_KIND_LABEL) == 0) id = strlen (label) > 0 ? g_strdup (label) : NULL; else if (strcmp (kind, G_VOLUME_IDENTIFIER_KIND_UUID) == 0) id = strlen (uuid) > 0 ? g_strdup (uuid) : NULL; } } return id; } static char ** g_gdu_volume_enumerate_identifiers (GVolume *_volume) { GGduVolume *volume = G_GDU_VOLUME (_volume); GduDevice *device; GPtrArray *p; const gchar *label; const gchar *uuid; p = g_ptr_array_new (); label = NULL; if (volume->gdu_volume != NULL) { device = gdu_presentable_get_device (GDU_PRESENTABLE (volume->gdu_volume)); if (device != NULL) { label = gdu_device_id_get_label (device); uuid = gdu_device_id_get_uuid (device); g_object_unref (device); g_ptr_array_add (p, g_strdup (G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE)); if (strlen (label) > 0) g_ptr_array_add (p, g_strdup (G_VOLUME_IDENTIFIER_KIND_LABEL)); if (strlen (uuid) > 0) g_ptr_array_add (p, g_strdup (G_VOLUME_IDENTIFIER_KIND_UUID)); } } g_ptr_array_add (p, NULL); return (gchar **) g_ptr_array_free (p, FALSE); } static GFile * g_gdu_volume_get_activation_root (GVolume *_volume) { GGduVolume *volume = G_GDU_VOLUME (_volume); return volume->activation_root != NULL ? g_object_ref (volume->activation_root) : NULL; } static void g_gdu_volume_volume_iface_init (GVolumeIface *iface) { iface->get_name = g_gdu_volume_get_name; iface->get_icon = g_gdu_volume_get_icon; iface->get_uuid = g_gdu_volume_get_uuid; iface->get_drive = g_gdu_volume_get_drive; iface->get_mount = g_gdu_volume_get_mount; iface->can_mount = g_gdu_volume_can_mount; iface->can_eject = g_gdu_volume_can_eject; iface->should_automount = g_gdu_volume_should_automount; iface->mount_fn = g_gdu_volume_mount; iface->mount_finish = g_gdu_volume_mount_finish; iface->eject = g_gdu_volume_eject; iface->eject_finish = g_gdu_volume_eject_finish; iface->eject_with_operation = g_gdu_volume_eject_with_operation; iface->eject_with_operation_finish = g_gdu_volume_eject_with_operation_finish; iface->get_identifier = g_gdu_volume_get_identifier; iface->enumerate_identifiers = g_gdu_volume_enumerate_identifiers; iface->get_activation_root = g_gdu_volume_get_activation_root; } gboolean g_gdu_volume_has_dev (GGduVolume *volume, dev_t dev) { dev_t _dev; _dev = volume->dev; if (volume->cleartext_gdu_volume != NULL) { GduDevice *luks_cleartext_volume_device; luks_cleartext_volume_device = gdu_presentable_get_device (GDU_PRESENTABLE (volume->cleartext_gdu_volume)); if (luks_cleartext_volume_device != NULL) { _dev = gdu_device_get_dev (luks_cleartext_volume_device); g_object_unref (luks_cleartext_volume_device); } } return _dev == dev; } gboolean g_gdu_volume_has_device_file (GGduVolume *volume, const gchar *device_file) { const gchar *_device_file; _device_file = volume->device_file; if (volume->cleartext_gdu_volume != NULL) { GduDevice *luks_cleartext_volume_device; luks_cleartext_volume_device = gdu_presentable_get_device (GDU_PRESENTABLE (volume->cleartext_gdu_volume)); if (luks_cleartext_volume_device != NULL) { _device_file = gdu_device_get_device_file (luks_cleartext_volume_device); g_object_unref (luks_cleartext_volume_device); } } return g_strcmp0 (_device_file, device_file) == 0; } gboolean g_gdu_volume_has_mount_path (GGduVolume *volume, const char *mount_path) { GduDevice *device; GduPresentable *presentable; gboolean ret; ret = FALSE; presentable = g_gdu_volume_get_presentable_with_cleartext (volume); if (presentable != NULL) { device = gdu_presentable_get_device (presentable); if (device != NULL) { ret = g_strcmp0 (gdu_device_get_mount_path (device), mount_path) == 0; g_object_unref (device); } } return ret; } gboolean g_gdu_volume_has_uuid (GGduVolume *volume, const char *uuid) { const gchar *_uuid; _uuid = volume->uuid; if (volume->cleartext_gdu_volume != NULL) { GduDevice *luks_cleartext_volume_device; luks_cleartext_volume_device = gdu_presentable_get_device (GDU_PRESENTABLE (volume->cleartext_gdu_volume)); if (luks_cleartext_volume_device != NULL) { _uuid = gdu_device_id_get_uuid (luks_cleartext_volume_device); g_object_unref (luks_cleartext_volume_device); } } return g_strcmp0 (_uuid, uuid) == 0; } GduPresentable * g_gdu_volume_get_presentable (GGduVolume *volume) { return GDU_PRESENTABLE (volume->gdu_volume); } GduPresentable * g_gdu_volume_get_presentable_with_cleartext (GGduVolume *volume) { GduVolume *ret; ret = volume->cleartext_gdu_volume; if (ret == NULL) ret = volume->gdu_volume; return GDU_PRESENTABLE (ret); } GUnixMountPoint * g_gdu_volume_get_unix_mount_point (GGduVolume *volume) { return volume->unix_mount_point; } gboolean g_gdu_volume_has_presentable (GGduVolume *volume, GduPresentable *presentable) { return volume->gdu_volume != NULL && g_strcmp0 (gdu_presentable_get_id (GDU_PRESENTABLE (volume->gdu_volume)), gdu_presentable_get_id (presentable)) == 0; }