diff options
Diffstat (limited to 'trunk/daemon/gvfsbackenddav.c')
-rw-r--r-- | trunk/daemon/gvfsbackenddav.c | 2335 |
1 files changed, 2335 insertions, 0 deletions
diff --git a/trunk/daemon/gvfsbackenddav.c b/trunk/daemon/gvfsbackenddav.c new file mode 100644 index 00000000..aee90348 --- /dev/null +++ b/trunk/daemon/gvfsbackenddav.c @@ -0,0 +1,2335 @@ +/* GIO - GLib Input, Output and Streaming Library + * + * Copyright (C) 2008 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: Christian Kellner <gicmo@gnome.org> + */ + +#include <config.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> + +#include <glib/gstdio.h> +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include <libsoup/soup.h> + +/* LibXML2 includes */ +#include <libxml/parser.h> +#include <libxml/tree.h> +#include <libxml/xpath.h> +#include <libxml/xpathInternals.h> + +#include "gvfsbackenddav.h" +#include "gvfskeyring.h" + +#include "gvfsjobmount.h" +#include "gvfsjobopenforread.h" +#include "gvfsjobread.h" +#include "gvfsjobseekread.h" +#include "gvfsjobopenforwrite.h" +#include "gvfsjobwrite.h" +#include "gvfsjobseekwrite.h" +#include "gvfsjobsetdisplayname.h" +#include "gvfsjobqueryinfo.h" +#include "gvfsjobqueryfsinfo.h" +#include "gvfsjobqueryattributes.h" +#include "gvfsjobenumerate.h" +#include "gvfsdaemonprotocol.h" + +#include "soup-input-stream.h" +#include "soup-output-stream.h" + +#ifdef HAVE_AVAHI +#include "gvfsdnssdutils.h" +#include "gvfsdnssdresolver.h" +#endif + +typedef struct _MountAuthData MountAuthData; + +static void mount_auth_info_free (MountAuthData *info); + + +#ifdef HAVE_AVAHI +static void dns_sd_resolver_changed (GVfsDnsSdResolver *resolver, GVfsBackendDav *dav_backend); +#endif + +typedef struct _AuthInfo { + + /* for server authentication */ + char *username; + char *password; + char *realm; + + GPasswordSave pw_save; + +} AuthInfo; + +struct _MountAuthData { + + SoupSession *session; + GMountSource *mount_source; + + AuthInfo server_auth; + AuthInfo proxy_auth; + +}; + +struct _GVfsBackendDav +{ + GVfsBackendHttp parent_instance; + + MountAuthData auth_info; + +#ifdef HAVE_AVAHI + /* only set if we're handling a [dav|davs]+sd:// mounts */ + GVfsDnsSdResolver *resolver; +#endif +}; + +G_DEFINE_TYPE (GVfsBackendDav, g_vfs_backend_dav, G_VFS_TYPE_BACKEND_HTTP); + +static void +g_vfs_backend_dav_finalize (GObject *object) +{ + GVfsBackendDav *dav_backend; + + dav_backend = G_VFS_BACKEND_DAV (object); + +#ifdef HAVE_AVAHI + if (dav_backend->resolver != NULL) + { + g_signal_handlers_disconnect_by_func (dav_backend->resolver, dns_sd_resolver_changed, dav_backend); + g_object_unref (dav_backend->resolver); + } +#endif + + mount_auth_info_free (&(dav_backend->auth_info)); + + if (G_OBJECT_CLASS (g_vfs_backend_dav_parent_class)->finalize) + (*G_OBJECT_CLASS (g_vfs_backend_dav_parent_class)->finalize) (object); +} + +static void +g_vfs_backend_dav_init (GVfsBackendDav *backend) +{ + g_vfs_backend_set_user_visible (G_VFS_BACKEND (backend), TRUE); +} + +/* ************************************************************************* */ +/* Small utility functions */ + +static inline gboolean +sm_has_header (SoupMessage *msg, const char *header) +{ + return soup_message_headers_get (msg->response_headers, header) != NULL; +} + +static char * +path_get_parent_dir (const char *path) +{ + char *parent; + size_t len; + + len = strlen (path); + + while (len > 0 && path[len - 1] == '/') + len--; + + if (len == 0) + return NULL; + + parent = g_strrstr_len (path, len, "/"); + + if (parent == NULL) + return NULL; + + return g_strndup (path, (parent - path) + 1); +} + +/* message utility functions */ + +static void +message_add_destination_header (SoupMessage *msg, + SoupURI *uri) +{ + char *string; + + string = soup_uri_to_string (uri, FALSE); + soup_message_headers_append (msg->request_headers, + "Destination", + string); + g_free (string); +} +static void +message_add_overwrite_header (SoupMessage *msg, + gboolean overwrite) +{ + soup_message_headers_append (msg->request_headers, + "Overwrite", + overwrite ? "T" : "F"); +} + +static void +message_add_redirect_header (SoupMessage *msg, + GFileQueryInfoFlags flags) +{ + const char *header_redirect; + + /* RFC 4437 */ + if (flags & G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS) + header_redirect = "F"; + else + header_redirect = "T"; + + soup_message_headers_append (msg->request_headers, + "Apply-To-Redirect-Ref", + header_redirect); +} + +static inline gboolean +str_equal (const char *a, const char *b, gboolean insensitive) +{ + if (a == NULL || b == NULL) + return a == b; + + return insensitive ? !g_ascii_strcasecmp (a, b) : !strcmp (a, b); +} + +static gboolean +path_equal (const char *a, const char *b, gboolean relax) +{ + gboolean res; + size_t a_len, b_len; + + if (relax == FALSE) + return str_equal (a, b, FALSE); + + if (a == NULL || b == NULL) + return a == b; + + a_len = strlen (a); + b_len = strlen (b); + + while (a_len > 0 && a[a_len - 1] == '/') + a_len--; + + while (b_len > 0 && b[b_len - 1] == '/') + b_len--; + + if (a_len == b_len) + res = ! strncmp (a, b, a_len); + else + res = FALSE; + + return res; +} + +/* Like soup_uri_equal */ +static gboolean +dav_uri_match (SoupURI *a, SoupURI *b, gboolean relax) +{ + if (a->scheme != b->scheme || + a->port != b->port || + ! str_equal (a->user, b->user, FALSE) || + ! str_equal (a->password, b->password, FALSE) || + ! str_equal (a->host, b->host, TRUE) || + ! path_equal (a->path, b->path, relax) || + ! str_equal (a->query, b->query, FALSE) || + ! str_equal (a->fragment, b->fragment, FALSE)) + return FALSE; + return TRUE; +} + +static gboolean +message_should_apply_redir_ref (SoupMessage *msg) +{ + const char *header; + + header = soup_message_headers_get (msg->request_headers, + "Apply-To-Redirect-Ref"); + + if (header == NULL || g_ascii_strcasecmp (header, "T")) + return FALSE; + + return TRUE; +} + +/* redirection */ +static void +redirect_handler (SoupMessage *msg, gpointer user_data) +{ + SoupSession *session = user_data; + const char *new_loc; + SoupURI *new_uri; + SoupURI *old_uri; + guint status; + gboolean redirect; + + status = msg->status_code; + new_loc = soup_message_headers_get (msg->response_headers, "Location"); + + /* If we don't have a location to redirect to, just fail */ + if (new_loc == NULL) + return; + + if (!SOUP_STATUS_IS_REDIRECTION(status)) + return; + + new_uri = soup_uri_new_with_base (soup_message_get_uri (msg), new_loc); + if (new_uri == NULL) + { + soup_message_set_status_full (msg, + SOUP_STATUS_MALFORMED, + "Invalid Redirect URL"); + return; + } + + old_uri = soup_message_get_uri (msg); + + /* copy over username and password to new_uri */ + soup_uri_set_user(new_uri, old_uri->user); + soup_uri_set_password(new_uri, old_uri->password); + + /* Check if this is a trailing slash redirect (i.e. /a/b to /a/b/), + * redirect it right away + */ + redirect = dav_uri_match (new_uri, old_uri, TRUE); + + if (redirect == TRUE) + { + const char *dest; + + dest = soup_message_headers_get (msg->request_headers, + "Destination"); + + if (dest && g_str_has_suffix (dest, "/") == FALSE) + { + char *new_dest = g_strconcat (dest, "/", NULL); + soup_message_headers_replace (msg->request_headers, + "Destination", + new_dest); + g_free (new_dest); + } + } + else if (message_should_apply_redir_ref (msg)) + { + + + if (status == SOUP_STATUS_MOVED_PERMANENTLY || + status == SOUP_STATUS_TEMPORARY_REDIRECT) + { + + /* Only corss-site redirect safe methods */ + if (msg->method == SOUP_METHOD_GET && + msg->method == SOUP_METHOD_HEAD && + msg->method == SOUP_METHOD_OPTIONS && + msg->method == SOUP_METHOD_PROPFIND) + redirect = TRUE; + } + +#if 0 + else if (msg->status_code == SOUP_STATUS_SEE_OTHER || + msg->status_code == SOUP_STATUS_FOUND) + { + /* Redirect using a GET */ + g_object_set (msg, + SOUP_MESSAGE_METHOD, SOUP_METHOD_GET, + NULL); + soup_message_set_request (msg, NULL, + SOUP_MEMORY_STATIC, NULL, 0); + soup_message_headers_set_encoding (msg->request_headers, + SOUP_ENCODING_NONE); + } +#endif + /* ELSE: + * + * Two possibilities: + * + * 1) It's a non-redirecty 3xx response (300, 304, + * 305, 306) + * 2) It's some newly-defined 3xx response (308+) + * + * We ignore both of these cases. In the first, + * redirecting would be explicitly wrong, and in the + * last case, we have no clue if the 3xx response is + * supposed to be redirecty or non-redirecty. Plus, + * 2616 says unrecognized status codes should be + * treated as the equivalent to the x00 code, and we + * don't redirect on 300, so therefore we shouldn't + * redirect on 308+ either. + */ + } + + if (redirect) + { + soup_message_set_uri (msg, new_uri); + soup_session_requeue_message (session, msg); + } + + soup_uri_free (new_uri); +} + +static guint +g_vfs_backend_dav_send_message (GVfsBackend *backend, SoupMessage *message) +{ + GVfsBackendHttp *http_backend; + SoupSession *session; + + http_backend = G_VFS_BACKEND_HTTP (backend); + session = http_backend->session; + + /* We have our own custom redirect handler */ + soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT); + + soup_message_add_header_handler (message, "got_body", "Location", + G_CALLBACK (redirect_handler), session); + + return http_backend_send_message (backend, message); +} + +/* ************************************************************************* */ +/* generic xml parsing functions */ + +static inline gboolean +node_has_name (xmlNodePtr node, const char *name) +{ + g_return_val_if_fail (node != NULL, FALSE); + + return ! strcmp ((char *) node->name, name); +} + +static inline gboolean +node_has_name_ns (xmlNodePtr node, const char *name, const char *ns_href) +{ + gboolean has_name; + gboolean has_ns; + + g_return_val_if_fail (node != NULL, FALSE); + + has_name = has_ns = TRUE; + + if (name) + has_name = node->name && ! strcmp ((char *) node->name, name); + + if (ns_href) + has_ns = node->ns && node->ns->href && + ! g_ascii_strcasecmp ((char *) node->ns->href, ns_href); + + return has_name && has_ns; +} + +static inline gboolean +node_is_element (xmlNodePtr node) +{ + return node->type == XML_ELEMENT_NODE && node->name != NULL; +} + + +static inline gboolean +node_is_element_with_name (xmlNodePtr node, const char *name) +{ + return node->type == XML_ELEMENT_NODE && + node->name != NULL && + ! strcmp ((char *) node->name, name); +} + +static const char * +node_get_content (xmlNodePtr node) +{ + if (node == NULL) + return NULL; + + switch (node->type) + { + case XML_ELEMENT_NODE: + return node_get_content (node->children); + break; + case XML_TEXT_NODE: + return (const char *) node->content; + break; + default: + return NULL; + } +} + +typedef struct _xmlNodeIter { + + xmlNodePtr cur_node; + xmlNodePtr next_node; + + const char *name; + const char *ns_href; + + void *user_data; + +} xmlNodeIter; + +static xmlNodePtr +xml_node_iter_next (xmlNodeIter *iter) +{ + xmlNodePtr node; + + while ((node = iter->next_node)) + { + iter->next_node = node->next; + + if (node->type == XML_ELEMENT_NODE) { + if (node_has_name_ns (node, iter->name, iter->ns_href)) + break; + } + } + + iter->cur_node = node; + return node; +} + +static void * +xml_node_iter_get_user_data (xmlNodeIter *iter) +{ + return iter->user_data; +} + +static xmlNodePtr +xml_node_iter_get_current (xmlNodeIter *iter) +{ + return iter->cur_node; +} + +static gint +http_to_gio_error(guint status_code) +{ + switch (status_code) + { + case SOUP_STATUS_NOT_FOUND: + return G_IO_ERROR_NOT_FOUND; + break; + case SOUP_STATUS_UNAUTHORIZED: + case SOUP_STATUS_PAYMENT_REQUIRED: + case SOUP_STATUS_FORBIDDEN: + return G_IO_ERROR_PERMISSION_DENIED; + break; + case SOUP_STATUS_REQUEST_TIMEOUT: + return G_IO_ERROR_TIMED_OUT; + break; + case SOUP_STATUS_CANT_RESOLVE: + return G_IO_ERROR_HOST_NOT_FOUND; + break; + case SOUP_STATUS_NOT_IMPLEMENTED: + return G_IO_ERROR_NOT_SUPPORTED; + break; + case SOUP_STATUS_INSUFFICIENT_STORAGE: + return G_IO_ERROR_NO_SPACE; + break; + } + + return G_IO_ERROR_FAILED; +} + +static xmlDocPtr +parse_xml (SoupMessage *msg, + xmlNodePtr *root, + const char *name, + GError **error) +{ + xmlDocPtr doc; + + if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) + { + g_set_error (error, G_IO_ERROR, http_to_gio_error (msg->status_code), + _("HTTP Error: %s"), msg->reason_phrase); + return NULL; + } + + doc = xmlReadMemory (msg->response_body->data, + msg->response_body->length, + "response.xml", + NULL, + XML_PARSE_NONET | + XML_PARSE_NOWARNING | + XML_PARSE_NOBLANKS | + XML_PARSE_NSCLEAN | + XML_PARSE_NOCDATA | + XML_PARSE_COMPACT); + if (doc == NULL) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Could not parse response")); + return NULL; + } + + *root = xmlDocGetRootElement (doc); + + if (*root == NULL || (*root)->children == NULL) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Empty response")); + xmlFreeDoc (doc); + return NULL; + } + + if (strcmp ((char *) (*root)->name, name)) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Unexpected reply from server")); + xmlFreeDoc (doc); + return NULL; + } + + return doc; +} + +/* ************************************************************************* */ +/* Multistatus parsing code */ + +typedef struct _Multistatus Multistatus; +typedef struct _MsResponse MsResponse; +typedef struct _MsPropstat MsPropstat; + +struct _Multistatus { + + xmlDocPtr doc; + xmlNodePtr root; + + SoupURI *target; + +}; + +struct _MsResponse { + + Multistatus *multistatus; + + xmlNodePtr href; + xmlNodePtr first_propstat; +}; + +struct _MsPropstat { + + Multistatus *multistatus; + + xmlNodePtr prop_node; + guint status_code; + +}; + + +static gboolean +multistatus_parse (SoupMessage *msg, Multistatus *multistatus, GError **error) +{ + xmlDocPtr doc; + xmlNodePtr root; + + doc = parse_xml (msg, &root, "multistatus", error); + + if (doc == NULL) + return FALSE; + + multistatus->doc = doc; + multistatus->root = root; + multistatus->target = soup_message_get_uri (msg); + + return TRUE; +} + +static void +multistatus_free (Multistatus *multistatus) +{ + xmlFreeDoc (multistatus->doc); +} + +static void +multistatus_get_response_iter (Multistatus *multistatus, xmlNodeIter *iter) +{ + iter->cur_node = multistatus->root->children; + iter->next_node = multistatus->root->children; + iter->name = "response"; + iter->ns_href = "DAV:"; + iter->user_data = multistatus; +} + +static gboolean +multistatus_get_response (xmlNodeIter *resp_iter, MsResponse *response) +{ + Multistatus *multistatus; + xmlNodePtr resp_node; + xmlNodePtr iter; + xmlNodePtr href; + xmlNodePtr propstat; + + multistatus = xml_node_iter_get_user_data (resp_iter); + resp_node = xml_node_iter_get_current (resp_iter); + + if (resp_node == NULL) + return FALSE; + + propstat = NULL; + href = NULL; + + for (iter = resp_node->children; iter; iter = iter->next) + { + if (! node_is_element (iter)) + { + continue; + } + else if (node_has_name_ns (iter, "href", "DAV:")) + { + href = iter; + } + else if (node_has_name_ns (iter, "propstat", "DAV:")) + { + if (propstat == NULL) + propstat = iter; + } + + if (href && propstat) + break; + } + + if (href == NULL) + return FALSE; + + response->href = href; + response->multistatus = multistatus; + response->first_propstat = propstat; + + return resp_node != NULL; +} + +static char * +ms_response_get_basename (MsResponse *response) +{ + const char *text; + text = node_get_content (response->href); + + return http_uri_get_basename (text); + +} + +static gboolean +ms_response_is_target (MsResponse *response) +{ + const char *text; + const char *path; + SoupURI *target; + SoupURI *uri; + gboolean res; + + uri = NULL; + path = NULL; + target = response->multistatus->target; + text = node_get_content (response->href); + + if (text == NULL) + return FALSE; + + uri = soup_uri_new_with_base (target, text); + + if (uri == NULL) + return FALSE; + + res = dav_uri_match (uri, target, TRUE); + + soup_uri_free (uri); + + return res; +} + +static void +ms_response_get_propstat_iter (MsResponse *response, xmlNodeIter *iter) +{ + iter->cur_node = response->first_propstat; + iter->next_node = response->first_propstat; + iter->name = "propstat"; + iter->ns_href = "DAV:"; + iter->user_data = response; +} + +static guint +ms_response_get_propstat (xmlNodeIter *cur_node, MsPropstat *propstat) +{ + MsResponse *response; + xmlNodePtr pstat_node; + xmlNodePtr iter; + xmlNodePtr prop; + xmlNodePtr status; + const char *status_text; + gboolean res; + guint code; + + response = xml_node_iter_get_user_data (cur_node); + pstat_node = xml_node_iter_get_current (cur_node); + + if (pstat_node == NULL) + return 0; + + status = NULL; + prop = NULL; + + for (iter = pstat_node->children; iter; iter = iter->next) + { + if (!node_is_element (iter)) + { + continue; + } + else if (node_has_name_ns (iter, "status", "DAV:")) + { + status = iter; + } + else if (node_has_name_ns (iter, "prop", "DAV:")) + { + prop = iter; + } + + if (status && prop) + break; + } + + status_text = node_get_content (status); + + if (status_text == NULL || prop == NULL) + return 0; + + res = soup_headers_parse_status_line ((char *) status_text, + NULL, + &code, + NULL); + + if (res == FALSE) + return 0; + + propstat->prop_node = prop; + propstat->status_code = code; + propstat->multistatus = response->multistatus; + + return code; +} + +static GFileType +parse_resourcetype (xmlNodePtr rt) +{ + xmlNodePtr node; + GFileType type; + + for (node = rt->children; node; node = node->next) + { + if (node_is_element (node)) + break; + } + + if (node == NULL) + return G_FILE_TYPE_REGULAR; + + if (! strcmp ((char *) node->name, "collection")) + type = G_FILE_TYPE_DIRECTORY; + else if (! strcmp ((char *) node->name, "redirectref")) + type = G_FILE_TYPE_SYMBOLIC_LINK; + else + type = G_FILE_TYPE_UNKNOWN; + + return type; +} + +static inline void +file_info_set_content_type (GFileInfo *info, const char *type) +{ + g_file_info_set_content_type (info, type); + g_file_info_set_attribute_string (info, + G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE, + type); + +} + +static void +ms_response_to_file_info (MsResponse *response, + GFileInfo *info) +{ + xmlNodeIter iter; + MsPropstat propstat; + xmlNodePtr node; + guint status; + char *basename; + const char *text; + GTimeVal tv; + GFileType file_type; + char *mime_type; + GIcon *icon; + gboolean have_display_name; + + basename = ms_response_get_basename (response); + g_file_info_set_name (info, basename); + g_file_info_set_edit_name (info, basename); + + file_type = G_FILE_TYPE_UNKNOWN; + mime_type = NULL; + + have_display_name = FALSE; + ms_response_get_propstat_iter (response, &iter); + while (xml_node_iter_next (&iter)) + { + status = ms_response_get_propstat (&iter, &propstat); + + if (! SOUP_STATUS_IS_SUCCESSFUL (status)) + continue; + + for (node = propstat.prop_node->children; node; node = node->next) + { + if (! node_is_element (node)) + continue; /* TODO: check namespace, parse user data nodes*/ + + text = node_get_content (node); + + if (node_has_name (node, "resourcetype")) + { + file_type = parse_resourcetype (node); + g_file_info_set_file_type (info, file_type); + } + else if (node_has_name (node, "displayname")) + { + g_file_info_set_display_name (info, text); + have_display_name = TRUE; + } + else if (node_has_name (node, "getetag")) + { + g_file_info_set_attribute_string (info, G_FILE_ATTRIBUTE_ETAG_VALUE, + text); + } + else if (node_has_name (node, "creationdate")) + { + if (! g_time_val_from_iso8601 (text, &tv)) + continue; + + g_file_info_set_attribute_uint64 (info, + G_FILE_ATTRIBUTE_TIME_CREATED, + tv.tv_sec); + } + else if (node_has_name (node, "getcontenttype")) + { + mime_type = g_strdup (text); + } + else if (node_has_name (node, "getcontentlength")) + { + gint64 size; + size = g_ascii_strtoll (text, NULL, 10); + g_file_info_set_size (info, size); + } + else if (node_has_name (node, "getlastmodified")) + { + SoupDate *sd; + GTimeVal tv; + sd = soup_date_new_from_string(text); + if (sd) + { + soup_date_to_timeval (sd, &tv); + g_file_info_set_modification_time (info, &tv); + soup_date_free (sd); + } + } + } + } + + if (file_type == G_FILE_TYPE_DIRECTORY) + { + icon = g_themed_icon_new ("folder"); + file_info_set_content_type (info, "inode/directory"); + } + else + { + if (mime_type == NULL) + mime_type = g_content_type_guess (basename, NULL, 0, NULL); + + icon = g_content_type_get_icon (mime_type); + + if (G_IS_THEMED_ICON (icon)) + g_themed_icon_append_name (G_THEMED_ICON (icon), "text-x-generic"); + + file_info_set_content_type (info, mime_type); + } + + if (have_display_name == FALSE) + g_file_info_set_display_name (info, basename); + + g_file_info_set_icon (info, icon); + g_object_unref (icon); + g_free (mime_type); + g_free (basename); + +} + +static GFileType +ms_response_to_file_type (MsResponse *response) +{ + xmlNodeIter prop_iter; + MsPropstat propstat; + GFileType file_type; + guint status; + + file_type = G_FILE_TYPE_UNKNOWN; + + ms_response_get_propstat_iter (response, &prop_iter); + while (xml_node_iter_next (&prop_iter)) + { + xmlNodePtr iter; + + status = ms_response_get_propstat (&prop_iter, &propstat); + + if (! SOUP_STATUS_IS_SUCCESSFUL (status)) + continue; + + for (iter = propstat.prop_node->children; iter; iter = iter->next) + { + if (node_is_element (iter) && + node_has_name_ns (iter, "resourcetype", "DAV:")) + break; + } + + if (iter) + { + file_type = parse_resourcetype (iter); + break; + } + } + + return file_type; +} + +#define PROPSTAT_XML_BEGIN \ + "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" \ + " <D:propfind xmlns:D=\"DAV:\">\n" + +#define PROPSTAT_XML_ALLPROP " <D:allprop/>\n" +#define PROPSTAT_XML_PROP_BEGIN " <D:prop>\n" +#define PROPSTAT_XML_PROP_END " </D:prop>\n" + +#define PROPSTAT_XML_END \ + " </D:propfind>" + +typedef struct _PropName { + + const char *name; + const char *namespace; + +} PropName; + + +static SoupMessage * +propfind_request_new (GVfsBackend *backend, + const char *filename, + guint depth, + const PropName *properties) +{ + SoupMessage *msg; + SoupURI *uri; + const char *header_depth; + GString *body; + + uri = http_backend_uri_for_filename (backend, filename, depth > 0); + msg = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, uri); + soup_uri_free (uri); + + if (msg == NULL) + return NULL; + + if (depth == 0) + header_depth = "0"; + else if (depth == 1) + header_depth = "1"; + else + header_depth = "infinity"; + + soup_message_headers_append (msg->request_headers, "Depth", header_depth); + + body = g_string_new (PROPSTAT_XML_BEGIN); + + if (properties != NULL) + { + const PropName *prop; + g_string_append (body, PROPSTAT_XML_PROP_BEGIN); + + for (prop = properties; prop->name; prop++) + { + if (prop->namespace != NULL) + g_string_append_printf (body, "<%s xmlns=\"%s\"/>\n", + prop->name, + prop->namespace); + else + g_string_append_printf (body, "<D:%s/>\n", prop->name); + } + g_string_append (body, PROPSTAT_XML_PROP_END); + } + else + g_string_append (body, PROPSTAT_XML_ALLPROP); + + + g_string_append (body, PROPSTAT_XML_END); + + soup_message_set_request (msg, "application/xml", + SOUP_MEMORY_TAKE, + body->str, + body->len); + + g_string_free (body, FALSE); + + return msg; +} + +static SoupMessage * +stat_location_begin (SoupURI *uri, + gboolean count_children) +{ + SoupMessage *msg; + const char *depth; + static const char *stat_profind_body = + PROPSTAT_XML_BEGIN + PROPSTAT_XML_PROP_BEGIN + "<D:resourcetype/>\n" + PROPSTAT_XML_PROP_END + PROPSTAT_XML_END; + + msg = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, uri); + + if (count_children) + depth = "1"; + else + depth = "0"; + + soup_message_headers_append (msg->request_headers, "Depth", depth); + + soup_message_set_request (msg, "application/xml", + SOUP_MEMORY_STATIC, + stat_profind_body, + strlen (stat_profind_body)); + return msg; +} + +static gboolean +stat_location_finish (SoupMessage *msg, + GFileType *target_type, + guint *num_children) +{ + Multistatus ms; + xmlNodeIter iter; + gboolean res; + GError *error; + guint child_count; + GFileType file_type; + + if (msg->status_code != 207) + return FALSE; + + res = multistatus_parse (msg, &ms, &error); + + if (res == FALSE) + return FALSE; + + res = FALSE; + child_count = 0; + file_type = G_FILE_TYPE_UNKNOWN; + + multistatus_get_response_iter (&ms, &iter); + while (xml_node_iter_next (&iter)) + { + MsResponse response; + + if (! multistatus_get_response (&iter, &response)) + continue; + + if (ms_response_is_target (&response)) + { + file_type = ms_response_to_file_type (&response); + res = TRUE; + } + else + child_count++; + } + + if (res) + { + if (target_type) + *target_type = file_type; + + if (num_children) + *num_children = child_count; + } + + multistatus_free (&ms); + return res; +} + +static gboolean +stat_location (GVfsBackend *backend, + SoupURI *uri, + GFileType *target_type, + guint *num_children, + GError **error) +{ + SoupMessage *msg; + guint status; + gboolean count_children; + gboolean res; + + count_children = num_children != NULL; + msg = stat_location_begin (uri, count_children); + + if (msg == NULL) + return FALSE; + + status = g_vfs_backend_dav_send_message (backend, msg); + + if (status != 207) + { + g_set_error_literal (error, + G_IO_ERROR, + http_error_code_from_status (status), + msg->reason_phrase); + + return FALSE; + } + + res = stat_location_finish (msg, target_type, num_children); + + if (res == FALSE) + g_set_error_literal (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + _("Response invalid")); + + return res; +} + + +/* ************************************************************************* */ +/* Authentication */ + +static void +mount_auth_info_free (MountAuthData *data) +{ + if (data->mount_source) + g_object_unref (data->mount_source); + + g_free (data->server_auth.username); + g_free (data->server_auth.password); + g_free (data->server_auth.realm); + + g_free (data->proxy_auth.username); + g_free (data->proxy_auth.password); + +} + +static void +soup_authenticate_from_data (SoupSession *session, + SoupMessage *msg, + SoupAuth *auth, + gboolean retrying, + gpointer user_data) +{ + MountAuthData *data; + AuthInfo *info; + + g_debug ("+ soup_authenticate_from_data (%s) \n", + retrying ? "retrying" : "first auth"); + + if (retrying) + return; + + data = (MountAuthData *) user_data; + + if (soup_auth_is_for_proxy (auth)) + info = &data->proxy_auth; + else + info = &data->server_auth; + + soup_auth_authenticate (auth, info->username, info->password); +} + +static void +soup_authenticate_interactive (SoupSession *session, + SoupMessage *msg, + SoupAuth *auth, + gboolean retrying, + gpointer user_data) +{ + MountAuthData *data; + AuthInfo *info; + GAskPasswordFlags pw_ask_flags; + GPasswordSave pw_save; + const char *realm; + gboolean res; + gboolean aborted; + gboolean is_proxy; + gboolean have_auth; + char *new_username; + char *new_password; + char *prompt; + + g_debug ("+ soup_authenticate_interactive (%s) \n", + retrying ? "retrying" : "first auth"); + + data = (MountAuthData *) user_data; + + new_username = NULL; + new_password = NULL; + realm = NULL; + pw_ask_flags = G_ASK_PASSWORD_NEED_PASSWORD; + + is_proxy = soup_auth_is_for_proxy (auth); + realm = soup_auth_get_realm (auth); + + if (is_proxy) + info = &(data->proxy_auth); + else + info = &(data->server_auth); + + if (realm && info->realm == NULL) + info->realm = g_strdup (realm); + else if (realm && info->realm && !g_str_equal (realm, info->realm)) + return; + + have_auth = info->username && info->password; + + if (have_auth == FALSE && g_vfs_keyring_is_available ()) + { + SoupURI *uri; + SoupURI *uri_free = NULL; + + pw_ask_flags |= G_ASK_PASSWORD_SAVING_SUPPORTED; + + if (is_proxy) + { + g_object_get (session, SOUP_SESSION_PROXY_URI, &uri_free, NULL); + uri = uri_free; + } + else + uri = soup_message_get_uri (msg); + + res = g_vfs_keyring_lookup_password (info->username, + uri->host, + NULL, + "http", + realm, + is_proxy ? "proxy" : "basic", + uri->port, + &new_username, + NULL, + &new_password); + + if (res == TRUE) + { + have_auth = TRUE; + g_free (info->username); + g_free (info->password); + info->username = new_username; + info->password = new_password; + } + + if (uri_free) + soup_uri_free (uri_free); + } + + if (retrying == FALSE && have_auth) + { + soup_auth_authenticate (auth, info->username, info->password); + return; + } + + if (is_proxy == FALSE) + { + if (realm == NULL) + realm = _("WebDAV share"); + + prompt = g_strdup_printf (_("Enter password for %s"), realm); + } + else + prompt = g_strdup (_("Please enter proxy password")); + + if (info->username == NULL) + pw_ask_flags |= G_ASK_PASSWORD_NEED_USERNAME; + + res = g_mount_source_ask_password (data->mount_source, + prompt, + info->username, + NULL, + pw_ask_flags, + &aborted, + &new_password, + &new_username, + NULL, + NULL, + &pw_save); + + if (res && !aborted) + { + soup_auth_authenticate (auth, new_username, new_password); + + g_free (info->username); + g_free (info->password); + info->username = new_username; + info->password = new_password; + info->pw_save = pw_save; + } + else + soup_session_cancel_message (session, msg, SOUP_STATUS_CANCELLED); + + g_debug ("- soup_authenticate \n"); + g_free (prompt); +} + +static void +keyring_save_authinfo (AuthInfo *info, + SoupURI *uri, + gboolean is_proxy) +{ + const char *type = is_proxy ? "proxy" : "basic"; + + g_vfs_keyring_save_password (info->username, + uri->host, + NULL, + "http", + info->realm, + type, + uri->port, + info->password, + info->pw_save); +} + +/* ************************************************************************* */ + +static SoupURI * +g_mount_spec_to_dav_uri (GMountSpec *spec) +{ + SoupURI *uri; + const char *host; + const char *user; + const char *port; + const char *ssl; + gint port_num; + + host = g_mount_spec_get (spec, "host"); + user = g_mount_spec_get (spec, "user"); + port = g_mount_spec_get (spec, "port"); + ssl = g_mount_spec_get (spec, "ssl"); + + if (host == NULL || *host == 0) + return NULL; + + uri = soup_uri_new (NULL); + + if (ssl != NULL && (strcmp (ssl, "true") == 0)) + soup_uri_set_scheme (uri, SOUP_URI_SCHEME_HTTPS); + else + soup_uri_set_scheme (uri, SOUP_URI_SCHEME_HTTP); + + soup_uri_set_user (uri, user); + + if (port && (port_num = atoi (port))) + soup_uri_set_port (uri, port_num); + + soup_uri_set_host (uri, host); + soup_uri_set_path (uri, spec->mount_prefix); + + return uri; +} + +static GMountSpec * +g_mount_spec_from_dav_uri (GVfsBackendDav *dav_backend, + SoupURI *uri) +{ + GMountSpec *spec; + const char *ssl; + +#ifdef HAVE_AVAHI + if (dav_backend->resolver != NULL) + { + const char *type; + const char *service_type; + + service_type = g_vfs_dns_sd_resolver_get_service_type (dav_backend->resolver); + if (strcmp (service_type, "_webdavs._tcp") == 0) + type = "davs+sd"; + else + type = "dav+sd"; + + spec = g_mount_spec_new (type); + g_mount_spec_set (spec, + "host", + g_vfs_dns_sd_resolver_get_encoded_triple (dav_backend->resolver)); + return spec; + } +#endif + + spec = g_mount_spec_new ("dav"); + + g_mount_spec_set (spec, "host", uri->host); + + if (uri->scheme == SOUP_URI_SCHEME_HTTPS) + ssl = "true"; + else + ssl = "false"; + + g_mount_spec_set (spec, "ssl", ssl); + + if (uri->user) + g_mount_spec_set (spec, "user", uri->user); + + if (! soup_uri_uses_default_port (uri)) + { + char *port = g_strdup_printf ("%u", uri->port); + g_mount_spec_set (spec, "port", port); + g_free (port); + } + + g_mount_spec_set_mount_prefix (spec, uri->path); + + return spec; +} + +#ifdef HAVE_AVAHI +static SoupURI * +dav_uri_from_dns_sd_resolver (GVfsBackendDav *dav_backend) +{ + SoupURI *uri; + char *user; + char *path; + char *address; + const char *service_type; + guint port; + + service_type = g_vfs_dns_sd_resolver_get_service_type (dav_backend->resolver); + address = g_vfs_dns_sd_resolver_get_address (dav_backend->resolver); + port = g_vfs_dns_sd_resolver_get_port (dav_backend->resolver); + user = g_vfs_dns_sd_resolver_lookup_txt_record (dav_backend->resolver, "u"); /* mandatory */ + path = g_vfs_dns_sd_resolver_lookup_txt_record (dav_backend->resolver, "path"); /* optional */ + + /* TODO: According to http://www.dns-sd.org/ServiceTypes.html + * there's also a TXT record "p" for password. Handle this. + */ + + uri = soup_uri_new (NULL); + + if (strcmp (service_type, "_webdavs._tcp") == 0) + soup_uri_set_scheme (uri, SOUP_URI_SCHEME_HTTPS); + else + soup_uri_set_scheme (uri, SOUP_URI_SCHEME_HTTP); + + soup_uri_set_user (uri, user); + + soup_uri_set_port (uri, port); + + soup_uri_set_host (uri, address); + + if (path != NULL) + soup_uri_set_path (uri, path); + else + soup_uri_set_path (uri, "/"); + + + g_free (address); + g_free (user); + g_free (path); + + return uri; +} +#endif + +#ifdef HAVE_AVAHI +static void +dns_sd_resolver_changed (GVfsDnsSdResolver *resolver, + GVfsBackendDav *dav_backend) +{ + /* TODO: handle when DNS-SD data changes */ +} +#endif + +/* ************************************************************************* */ +/* Backend Functions */ +static void +do_mount (GVfsBackend *backend, + GVfsJobMount *job, + GMountSpec *mount_spec, + GMountSource *mount_source, + gboolean is_automount) +{ + GVfsBackendDav *dav_backend = G_VFS_BACKEND_DAV (backend); + MountAuthData *data; + SoupSession *session; + SoupMessage *msg_opts; + SoupMessage *msg_stat; + SoupURI *mount_base; + gulong signal_id; + guint status; + gboolean is_success; + gboolean is_webdav; + gboolean res; + char *last_good_path; + char *display_name; + const char *host; + const char *type; + + g_debug ("+ mount\n"); + + host = g_mount_spec_get (mount_spec, "host"); + type = g_mount_spec_get (mount_spec, "type"); + +#ifdef HAVE_AVAHI + /* resolve DNS-SD style URIs */ + if ((strcmp (type, "dav+sd") == 0 || strcmp (type, "davs+sd") == 0) && host != NULL) + { + GError *error; + + dav_backend->resolver = g_vfs_dns_sd_resolver_new_for_encoded_triple (host, "u"); + + error = NULL; + if (!g_vfs_dns_sd_resolver_resolve_sync (dav_backend->resolver, + NULL, + &error)) + { + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); + g_error_free (error); + return; + } + g_signal_connect (dav_backend->resolver, + "changed", + (GCallback) dns_sd_resolver_changed, + dav_backend); + + mount_base = dav_uri_from_dns_sd_resolver (dav_backend); + } + else +#endif + { + mount_base = g_mount_spec_to_dav_uri (mount_spec); + } + + if (mount_base == NULL) + { + g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, + G_IO_ERROR_INVALID_ARGUMENT, + _("Invalid mount spec")); + return; + } + + session = G_VFS_BACKEND_HTTP (backend)->session; + G_VFS_BACKEND_HTTP (backend)->mount_base = mount_base; + + data = &(G_VFS_BACKEND_DAV (backend)->auth_info); + data->mount_source = g_object_ref (mount_source); + data->server_auth.username = g_strdup (mount_base->user); + data->server_auth.pw_save = G_PASSWORD_SAVE_NEVER; + data->proxy_auth.pw_save = G_PASSWORD_SAVE_NEVER; + + signal_id = g_signal_connect (session, "authenticate", + G_CALLBACK (soup_authenticate_interactive), + data); + + last_good_path = NULL; + msg_opts = soup_message_new_from_uri (SOUP_METHOD_OPTIONS, mount_base); + msg_stat = stat_location_begin (mount_base, FALSE); + + do { + status = g_vfs_backend_dav_send_message (backend, msg_opts); + + is_success = SOUP_STATUS_IS_SUCCESSFUL (status); + is_webdav = is_success && sm_has_header (msg_opts, "DAV"); + + soup_message_headers_clear (msg_opts->response_headers); + soup_message_body_truncate (msg_opts->response_body); + + if (is_webdav) + { + GFileType file_type; + SoupURI *cur_uri; + + cur_uri = soup_message_get_uri (msg_opts); + soup_message_set_uri (msg_stat, cur_uri); + + g_vfs_backend_dav_send_message (backend, msg_stat); + res = stat_location_finish (msg_stat, &file_type, NULL); + + if (res && file_type == G_FILE_TYPE_DIRECTORY) + { + g_free (last_good_path); + last_good_path = mount_base->path; + } + + mount_base->path = path_get_parent_dir (mount_base->path); + soup_message_set_uri (msg_opts, mount_base); + + soup_message_headers_clear (msg_stat->response_headers); + soup_message_body_truncate (msg_stat->response_body); + } + + } while (is_webdav && mount_base->path != NULL); + + /* we have reached the end of paths we are allowed to + * chdir up to (or couldn't chdir up at all) */ + + /* check if we at all have a good path */ + if (last_good_path == NULL) + { + + /* TODO: set correct error in case of cancellation */ + if (!is_success) + g_vfs_job_failed (G_VFS_JOB (job), + G_IO_ERROR, G_IO_ERROR_FAILED, + _("HTTP Error: %s"), msg_opts->reason_phrase); + else if (!is_webdav) + g_vfs_job_failed (G_VFS_JOB (job), + G_IO_ERROR, G_IO_ERROR_FAILED, + _("Not a WebDAV enabled share")); + else + g_vfs_job_failed (G_VFS_JOB (job), + G_IO_ERROR, G_IO_ERROR_FAILED, + _("Not a WebDAV enabled share")); + + /* TODO: We leak a bunch of stuff here :-( */ + /* TODO: STRING CHANGE: change to: Could not find an enclosing directory */ + return; + } + + /* Success! We are mounted */ + /* Save the auth info in the keyring */ + + keyring_save_authinfo (&(data->server_auth), mount_base, FALSE); + /* TODO: save proxy auth */ + + /* Set the working path in mount path */ + g_free (mount_base->path); + mount_base->path = last_good_path; + + /* dup the mountspec, but only copy known fields */ + mount_spec = g_mount_spec_from_dav_uri (dav_backend, mount_base); + + g_vfs_backend_set_mount_spec (backend, mount_spec); + g_vfs_backend_set_icon_name (backend, "folder-remote"); + +#ifdef HAVE_AVAHI + if (dav_backend->resolver != NULL) + display_name = g_strdup (g_vfs_dns_sd_resolver_get_service_name (dav_backend->resolver)); + else +#endif + display_name = g_strdup_printf (_("WebDAV on %s"), mount_base->host); + g_vfs_backend_set_display_name (backend, display_name); + g_free (display_name); + + /* cleanup */ + g_mount_spec_unref (mount_spec); + g_object_unref (msg_opts); + g_object_unref (msg_stat); + + /* switch the signal handler */ + g_signal_handler_disconnect (session, signal_id); + g_signal_connect (session, "authenticate", + G_CALLBACK (soup_authenticate_from_data), + data); + + /* also auth the workaround async session we need for SoupInputStream */ + g_signal_connect (G_VFS_BACKEND_HTTP (backend)->session_async, "authenticate", + G_CALLBACK (soup_authenticate_from_data), + data); + + g_vfs_job_succeeded (G_VFS_JOB (job)); + g_debug ("- mount\n"); +} + +static PropName ls_propnames[] = { + {"creationdate", NULL}, + {"displayname", NULL}, + {"getcontentlength", NULL}, + {"getcontenttype", NULL}, + {"getetag", NULL}, + {"getlastmodified", NULL}, + {"resourcetype", NULL}, + {NULL, NULL} +}; + +/* *** query_info () *** */ +static void +do_query_info (GVfsBackend *backend, + GVfsJobQueryInfo *job, + const char *filename, + GFileQueryInfoFlags flags, + GFileInfo *info, + GFileAttributeMatcher *matcher) +{ + SoupMessage *msg; + Multistatus ms; + xmlNodeIter iter; + gboolean res; + GError *error; + + error = NULL; + + g_debug ("Query info %s\n", filename); + + msg = propfind_request_new (backend, filename, 0, ls_propnames); + + if (msg == NULL) + { + g_vfs_job_failed (G_VFS_JOB (job), + G_IO_ERROR, G_IO_ERROR_FAILED, + _("Could not create request")); + + return; + } + + message_add_redirect_header (msg, flags); + + g_vfs_backend_dav_send_message (backend, msg); + + res = multistatus_parse (msg, &ms, &error); + + if (res == FALSE) + { + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); + g_error_free (error); + g_object_unref (msg); + return; + } + + res = FALSE; + multistatus_get_response_iter (&ms, &iter); + + while (xml_node_iter_next (&iter)) + { + MsResponse response; + + if (! multistatus_get_response (&iter, &response)) + continue; + + if (ms_response_is_target (&response)) + { + ms_response_to_file_info (&response, job->file_info); + res = TRUE; + } + } + + multistatus_free (&ms); + g_object_unref (msg); + + if (res) + g_vfs_job_succeeded (G_VFS_JOB (job)); + else + g_vfs_job_failed (G_VFS_JOB (job), + G_IO_ERROR, G_IO_ERROR_FAILED, + _("Response invalid")); + +} + + +/* *** enumerate *** */ +static void +do_enumerate (GVfsBackend *backend, + GVfsJobEnumerate *job, + const char *filename, + GFileAttributeMatcher *matcher, + GFileQueryInfoFlags flags) +{ + SoupMessage *msg; + Multistatus ms; + xmlNodeIter iter; + gboolean res; + GError *error; + + error = NULL; + + g_debug ("+ do_enumerate: %s\n", filename); + + msg = propfind_request_new (backend, filename, 1, ls_propnames); + + if (msg == NULL) + { + g_vfs_job_failed (G_VFS_JOB (job), + G_IO_ERROR, G_IO_ERROR_FAILED, + _("Could not create request")); + + return; + } + + message_add_redirect_header (msg, flags); + + g_vfs_backend_dav_send_message (backend, msg); + + res = multistatus_parse (msg, &ms, &error); + + if (res == FALSE) + { + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); + g_error_free (error); + g_object_unref (msg); + return; + } + + multistatus_get_response_iter (&ms, &iter); + + while (xml_node_iter_next (&iter)) + { + MsResponse response; + GFileInfo *info; + + if (! multistatus_get_response (&iter, &response)) + continue; + + if (ms_response_is_target (&response)) + continue; + + info = g_file_info_new (); + ms_response_to_file_info (&response, info); + g_vfs_job_enumerate_add_info (job, info); + } + + multistatus_free (&ms); + g_object_unref (msg); + + g_vfs_job_succeeded (G_VFS_JOB (job)); /* should that be called earlier? */ + g_vfs_job_enumerate_done (G_VFS_JOB_ENUMERATE (job)); +} + +/* ************************************************************************* */ +/* */ + +/* *** create () *** */ +static void +try_create_tested_existence (SoupSession *session, SoupMessage *msg, + gpointer user_data) +{ + GVfsJob *job = G_VFS_JOB (user_data); + GVfsBackendHttp *op_backend = job->backend_data; + GOutputStream *stream; + SoupMessage *put_msg; + SoupURI *uri; + + if (SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) + { + g_vfs_job_failed (job, + G_IO_ERROR, + G_IO_ERROR_EXISTS, + _("Target file already exists")); + return; + } + /* TODO: other errors */ + + uri = soup_message_get_uri (msg); + put_msg = soup_message_new_from_uri (SOUP_METHOD_PUT, uri); + + /* + * Doesn't work with apache > 2.2.9 + * soup_message_headers_append (put_msg->request_headers, "If-None-Match", "*"); + */ + stream = soup_output_stream_new (op_backend->session, put_msg, -1); + g_object_unref (put_msg); + + g_vfs_job_open_for_write_set_handle (G_VFS_JOB_OPEN_FOR_WRITE (job), stream); + g_vfs_job_succeeded (job); +} + +static gboolean +try_create (GVfsBackend *backend, + GVfsJobOpenForWrite *job, + const char *filename, + GFileCreateFlags flags) +{ + SoupMessage *msg; + SoupURI *uri; + + /* TODO: if SoupOutputStream supported chunked requests, we could + * use a PUT with "If-None-Match: *" and "Expect: 100-continue" + */ + uri = http_backend_uri_for_filename (backend, filename, FALSE); + msg = soup_message_new_from_uri (SOUP_METHOD_HEAD, uri); + soup_uri_free (uri); + + g_vfs_job_set_backend_data (G_VFS_JOB (job), backend, NULL); + + http_backend_queue_message (backend, msg, try_create_tested_existence, job); + return TRUE; +} + +/* *** replace () *** */ +static void +open_for_replace_succeeded (GVfsBackendHttp *op_backend, GVfsJob *job, + SoupURI *uri, const char *etag) +{ + SoupMessage *put_msg; + GOutputStream *stream; + + put_msg = soup_message_new_from_uri (SOUP_METHOD_PUT, uri); + + if (etag) + soup_message_headers_append (put_msg->request_headers, "If-Match", etag); + + stream = soup_output_stream_new (op_backend->session, put_msg, -1); + g_object_unref (put_msg); + + g_vfs_job_open_for_write_set_handle (G_VFS_JOB_OPEN_FOR_WRITE (job), stream); + g_vfs_job_succeeded (job); +} + +static void +try_replace_checked_etag (SoupSession *session, SoupMessage *msg, + gpointer user_data) +{ + GVfsJob *job = G_VFS_JOB (user_data); + GVfsBackendHttp *op_backend = job->backend_data; + + if (msg->status_code == SOUP_STATUS_PRECONDITION_FAILED) + { + g_vfs_job_failed (G_VFS_JOB (job), + G_IO_ERROR, + G_IO_ERROR_WRONG_ETAG, + _("The file was externally modified")); + return; + } + /* TODO: other errors */ + + open_for_replace_succeeded (op_backend, job, soup_message_get_uri (msg), + soup_message_headers_get (msg->request_headers, "If-Match")); +} + +static gboolean +try_replace (GVfsBackend *backend, + GVfsJobOpenForWrite *job, + const char *filename, + const char *etag, + gboolean make_backup, + GFileCreateFlags flags) +{ + GVfsBackendHttp *op_backend; + SoupURI *uri; + + /* TODO: if SoupOutputStream supported chunked requests, we could + * use a PUT with "If-Match: ..." and "Expect: 100-continue" + */ + + op_backend = G_VFS_BACKEND_HTTP (backend); + + if (make_backup) + { + g_vfs_job_failed (G_VFS_JOB (job), + G_IO_ERROR, + G_IO_ERROR_CANT_CREATE_BACKUP, + _("Backup file creation failed")); + return TRUE; + } + + + + uri = http_backend_uri_for_filename (backend, filename, FALSE); + + if (etag) + { + SoupMessage *msg; + + msg = soup_message_new_from_uri (SOUP_METHOD_HEAD, uri); + soup_uri_free (uri); + soup_message_headers_append (msg->request_headers, "If-Match", etag); + + g_vfs_job_set_backend_data (G_VFS_JOB (job), op_backend, NULL); + soup_session_queue_message (op_backend->session, msg, + try_replace_checked_etag, job); + return TRUE; + } + + open_for_replace_succeeded (op_backend, G_VFS_JOB (job), uri, NULL); + soup_uri_free (uri); + return TRUE; +} + +/* *** write () *** */ +static void +write_ready (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GOutputStream *stream; + GVfsJob *job; + GError *error; + gssize nwrote; + + stream = G_OUTPUT_STREAM (source_object); + error = NULL; + job = G_VFS_JOB (user_data); + + nwrote = g_output_stream_write_finish (stream, result, &error); + + if (nwrote < 0) + { + g_vfs_job_failed_literal (G_VFS_JOB (job), + error->domain, + error->code, + error->message); + + g_error_free (error); + return; + } + + g_vfs_job_write_set_written_size (G_VFS_JOB_WRITE (job), nwrote); + g_vfs_job_succeeded (job); +} + +static gboolean +try_write (GVfsBackend *backend, + GVfsJobWrite *job, + GVfsBackendHandle handle, + char *buffer, + gsize buffer_size) +{ + GOutputStream *stream; + + stream = G_OUTPUT_STREAM (handle); + + g_output_stream_write_async (stream, + buffer, + buffer_size, + G_PRIORITY_DEFAULT, + G_VFS_JOB (job)->cancellable, + write_ready, + job); + return TRUE; +} + +/* *** close_write () *** */ +static void +close_write_ready (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GOutputStream *stream; + GVfsJob *job; + GError *error; + gboolean res; + + error = NULL; + job = G_VFS_JOB (user_data); + stream = G_OUTPUT_STREAM (source_object); + res = g_output_stream_close_finish (stream, + result, + &error); + if (res == FALSE) + { + g_vfs_job_failed_literal (G_VFS_JOB (job), + error->domain, + error->code, + error->message); + + g_error_free (error); + } + else + g_vfs_job_succeeded (job); + + g_object_unref (stream); +} + +static gboolean +try_close_write (GVfsBackend *backend, + GVfsJobCloseWrite *job, + GVfsBackendHandle handle) +{ + GOutputStream *stream; + + stream = G_OUTPUT_STREAM (handle); + + g_output_stream_close_async (stream, + G_PRIORITY_DEFAULT, + G_VFS_JOB (job)->cancellable, + close_write_ready, + job); + + return TRUE; +} + +static void +do_make_directory (GVfsBackend *backend, + GVfsJobMakeDirectory *job, + const char *filename) +{ + SoupMessage *msg; + SoupURI *uri; + guint status; + + uri = http_backend_uri_for_filename (backend, filename, TRUE); + msg = soup_message_new_from_uri (SOUP_METHOD_MKCOL, uri); + soup_uri_free (uri); + + status = g_vfs_backend_dav_send_message (backend, msg); + + if (! SOUP_STATUS_IS_SUCCESSFUL (status)) + if (status == SOUP_STATUS_METHOD_NOT_ALLOWED) + g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, + G_IO_ERROR_EXISTS, + _("Target file already exists")); + else + g_vfs_job_failed_literal (G_VFS_JOB (job), G_IO_ERROR, + http_error_code_from_status (status), + msg->reason_phrase); + else + g_vfs_job_succeeded (G_VFS_JOB (job)); + + g_object_unref (msg); +} + +static void +do_delete (GVfsBackend *backend, + GVfsJobDelete *job, + const char *filename) +{ + SoupMessage *msg; + SoupURI *uri; + GFileType file_type; + gboolean res; + guint num_children; + guint status; + GError *error; + + error = NULL; + + uri = http_backend_uri_for_filename (backend, filename, FALSE); + res = stat_location (backend, uri, &file_type, &num_children, &error); + + if (res == FALSE) + { + g_vfs_job_failed_from_error (G_VFS_JOB (job), error); + g_error_free (error); + return; + } + + if (file_type == G_FILE_TYPE_DIRECTORY && num_children) + { + g_vfs_job_failed (G_VFS_JOB (job), + G_IO_ERROR, G_IO_ERROR_NOT_EMPTY, + _("Directory not empty")); + return; + } + + msg = soup_message_new_from_uri (SOUP_METHOD_DELETE, uri); + + status = g_vfs_backend_dav_send_message (backend, msg); + + if (!SOUP_STATUS_IS_SUCCESSFUL (status)) + g_vfs_job_failed_literal (G_VFS_JOB (job), + G_IO_ERROR, + http_error_code_from_status (status), + msg->reason_phrase); + else + g_vfs_job_succeeded (G_VFS_JOB (job)); + + soup_uri_free (uri); + g_object_unref (msg); +} + +static void +do_set_display_name (GVfsBackend *backend, + GVfsJobSetDisplayName *job, + const char *filename, + const char *display_name) +{ + SoupMessage *msg; + SoupURI *source; + SoupURI *target; + char *target_path; + char *dirname; + guint status; + + source = http_backend_uri_for_filename (backend, filename, FALSE); + msg = soup_message_new_from_uri (SOUP_METHOD_MOVE, source); + + dirname = g_path_get_dirname (filename); + target_path = g_build_filename (dirname, display_name, NULL); + target = http_backend_uri_for_filename (backend, target_path, FALSE); + + message_add_destination_header (msg, target); + message_add_overwrite_header (msg, FALSE); + + status = g_vfs_backend_dav_send_message (backend, msg); + + /* + * The precondition of SOUP_STATUS_PRECONDITION_FAILED (412) in + * this case was triggered by the "Overwrite: F" header which + * means that the target already exists. + * Also if we get a REDIRECTION it means that there was no + * "Location" header, since otherwise that would have triggered + * our redirection handler. This probably means we are dealing + * with an web dav implementation (like mod_dav) that also sends + * redirects for the destionaion (i.e. "Destination: /foo" header) + * which very likely means that the target also exists (and is a + * directory). That or the webdav server is broken. + * We could find out by doing another stat and but I think this is + * such a corner case that we are totally fine with returning + * G_IO_ERROR_EXISTS. + * */ + + if (SOUP_STATUS_IS_SUCCESSFUL (status)) + { + g_debug ("new target_path: %s\n", target_path); + g_vfs_job_set_display_name_set_new_path (job, target_path); + g_vfs_job_succeeded (G_VFS_JOB (job)); + } + else if (status == SOUP_STATUS_PRECONDITION_FAILED || + SOUP_STATUS_IS_REDIRECTION (status)) + g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, + G_IO_ERROR_EXISTS, + _("Target file already exists")); + else + g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR, + http_error_code_from_status (status), + "%s", msg->reason_phrase); + + g_object_unref (msg); + g_free (dirname); + g_free (target_path); + soup_uri_free (target); + soup_uri_free (source); +} + +static gboolean +try_unmount (GVfsBackend *backend, + GVfsJobUnmount *job) +{ + _exit (0); +} + +/* ************************************************************************* */ +/* */ +static void +g_vfs_backend_dav_class_init (GVfsBackendDavClass *klass) +{ + GObjectClass *gobject_class; + GVfsBackendClass *backend_class; + + gobject_class = G_OBJECT_CLASS (klass); + gobject_class->finalize = g_vfs_backend_dav_finalize; + + backend_class = G_VFS_BACKEND_CLASS (klass); + + backend_class->try_mount = NULL; + backend_class->mount = do_mount; + backend_class->try_query_info = NULL; + backend_class->query_info = do_query_info; + backend_class->enumerate = do_enumerate; + backend_class->try_create = try_create; + backend_class->try_replace = try_replace; + backend_class->try_write = try_write; + backend_class->try_close_write = try_close_write; + backend_class->make_directory = do_make_directory; + backend_class->delete = do_delete; + backend_class->set_display_name = do_set_display_name; + backend_class->try_unmount = try_unmount; +} |