From 1ec1db30459c02e09c6fe54365edd89b0d6a3597 Mon Sep 17 00:00:00 2001 From: rfc2822 Date: Fri, 31 Oct 2014 13:45:47 +0100 Subject: [PATCH] Ensure trailing slashes are always used for collections + tests --- src/at/bitfire/davdroid/URIUtils.java | 30 +++++++- .../davdroid/resource/DavResourceFinder.java | 6 +- src/at/bitfire/davdroid/webdav/DavProp.java | 9 ++- .../davdroid/webdav/WebDavResource.java | 73 ++++++++++--------- test/robohydra/plugins/dav-invalid/index.js | 2 +- .../bitfire/davdroid/test/URIUtilsTest.java | 13 ++++ .../davdroid/webdav/WebDavResourceTest.java | 4 +- 7 files changed, 92 insertions(+), 45 deletions(-) diff --git a/src/at/bitfire/davdroid/URIUtils.java b/src/at/bitfire/davdroid/URIUtils.java index 235b3e05..37cf75b3 100644 --- a/src/at/bitfire/davdroid/URIUtils.java +++ b/src/at/bitfire/davdroid/URIUtils.java @@ -7,6 +7,8 @@ ******************************************************************************/ package at.bitfire.davdroid; +import java.net.URI; +import java.net.URISyntaxException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -18,7 +20,32 @@ public class URIUtils { private static final String TAG = "davdroid.URIUtils"; - // handles invalid URLs/paths as good as possible + public static String ensureTrailingSlash(String href) { + if (!href.endsWith("/")) { + Log.d(TAG, "Implicitly appending trailing slash to collection " + href); + return href + "/"; + } else + return href; + } + + public static URI ensureTrailingSlash(URI href) { + if (!href.getPath().endsWith("/")) + try { + URI newURI = new URI(href.getScheme(), href.getAuthority(), href.getPath() + "/", href.getQuery(), null); + + // "@" is the only character that is not encoded + newURI = new URI(newURI.toString().replaceAll("@", "%40")); + + Log.d(TAG, "Implicitly appending trailing slash to collection " + href + " -> " + newURI); + return newURI; + } catch (URISyntaxException e) { + Log.e(TAG, "Couldn't append trailing slash to collection URI", e); + } + return href; + } + + + /** handles invalid URLs/paths as good as possible **/ public static String sanitize(String original) { if (original == null) return null; @@ -67,4 +94,5 @@ public class URIUtils { } } + } diff --git a/src/at/bitfire/davdroid/resource/DavResourceFinder.java b/src/at/bitfire/davdroid/resource/DavResourceFinder.java index 13a8e118..389d0724 100644 --- a/src/at/bitfire/davdroid/resource/DavResourceFinder.java +++ b/src/at/bitfire/davdroid/resource/DavResourceFinder.java @@ -88,15 +88,17 @@ public class DavResourceFinder { if (homeSetCalendars.getMembers() != null) for (WebDavResource resource : homeSetCalendars.getMembers()) if (resource.isCalendar()) { - Log.i(TAG, "Found calendar: " + resource.getLocation().getRawPath()); + Log.i(TAG, "Found calendar: " + resource.getLocation().getPath()); if (resource.getSupportedComponents() != null) { // CALDAV:supported-calendar-component-set available boolean supportsEvents = false; for (String supportedComponent : resource.getSupportedComponents()) if (supportedComponent.equalsIgnoreCase("VEVENT")) supportsEvents = true; - if (!supportsEvents) // ignore collections without VEVENT support + if (!supportsEvents) { // ignore collections without VEVENT support + Log.i(TAG, "Ignoring this calendar because of missing VEVENT support"); continue; + } } ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( ServerInfo.ResourceInfo.Type.CALENDAR, diff --git a/src/at/bitfire/davdroid/webdav/DavProp.java b/src/at/bitfire/davdroid/webdav/DavProp.java index 6c4cf002..93f25645 100644 --- a/src/at/bitfire/davdroid/webdav/DavProp.java +++ b/src/at/bitfire/davdroid/webdav/DavProp.java @@ -39,13 +39,16 @@ public class DavProp { @Root(strict=false) public static class ResourceType { @Element(required=false) - @Getter private Addressbook addressbook; - @Element(required=false) - @Getter private Calendar calendar; + @Getter private Collection collection; + public static class Collection { } + @Element(required=false) + @Getter private Addressbook addressbook; @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") public static class Addressbook { } + @Element(required=false) + @Getter private Calendar calendar; @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") public static class Calendar { } } diff --git a/src/at/bitfire/davdroid/webdav/WebDavResource.java b/src/at/bitfire/davdroid/webdav/WebDavResource.java index c8fa8f74..dbc13ece 100644 --- a/src/at/bitfire/davdroid/webdav/WebDavResource.java +++ b/src/at/bitfire/davdroid/webdav/WebDavResource.java @@ -68,7 +68,8 @@ public class WebDavResource { CURRENT_USER_PRINCIPAL, // resource detection ADDRESSBOOK_HOMESET, CALENDAR_HOMESET, CONTENT_TYPE, READ_ONLY, // WebDAV (common) - DISPLAY_NAME, DESCRIPTION, CTAG, ETAG, + DISPLAY_NAME, DESCRIPTION, ETAG, + IS_COLLECTION, CTAG, // collections IS_CALENDAR, COLOR, TIMEZONE, // CalDAV IS_ADDRESSBOOK, VCARD_VERSION // CardDAV } @@ -452,6 +453,7 @@ public class WebDavResource { // member list will be built from response List members = new LinkedList(); + // iterate through all resources (either ourselves or member) for (DavResponse singleResponse : multiStatus.response) { URI href; try { @@ -461,42 +463,19 @@ public class WebDavResource { continue; } Log.d(TAG, "Processing multi-status element: " + href); - - // about which resource is this response? - WebDavResource referenced = null; - - // "this" resource is either at "location" … - if (location.equals(href)) { // -> ourselves - referenced = this; - } else { - // … or at location + "/" (in case of a collection where the server has implicitly appended the trailing slash) - if (!location.getRawPath().endsWith("/")) // this is only possible if location doesn't have a trailing slash - try { - URI locationAsCollection = new URI(location.getScheme(), location.getAuthority(), location.getPath() + "/", location.getQuery(), null); - if (locationAsCollection.equals(href)) { - Log.d(TAG, "Server implicitly appended trailing slash to " + locationAsCollection); - referenced = this; - } - } catch (URISyntaxException e) { - Log.wtf(TAG, "Couldn't understand our own URI", e); - } - - // otherwise, the referenced resource is a member - if (referenced == null) { - referenced = new WebDavResource(this, href); - members.add(referenced); - } - } - + + // process known properties + HashMap properties = new HashMap(); + List supportedComponents = null; + byte[] data = null; + for (DavPropstat singlePropstat : singleResponse.getPropstat()) { StatusLine status = BasicLineParser.parseStatusLine(singlePropstat.status, new BasicLineParser()); // ignore information about missing properties etc. if (status.getStatusCode()/100 != 1 && status.getStatusCode()/100 != 2) continue; - DavProp prop = singlePropstat.prop; - HashMap properties = referenced.properties; if (prop.currentUserPrincipal != null && prop.currentUserPrincipal.getHref() != null) properties.put(Property.CURRENT_USER_PRINCIPAL, prop.currentUserPrincipal.getHref().href); @@ -520,15 +499,20 @@ public class WebDavResource { } if (prop.addressbookHomeSet != null && prop.addressbookHomeSet.getHref() != null) - properties.put(Property.ADDRESSBOOK_HOMESET, prop.addressbookHomeSet.getHref().href); + properties.put(Property.ADDRESSBOOK_HOMESET, URIUtils.ensureTrailingSlash(prop.addressbookHomeSet.getHref().href)); if (prop.calendarHomeSet != null && prop.calendarHomeSet.getHref() != null) - properties.put(Property.CALENDAR_HOMESET, prop.calendarHomeSet.getHref().href); + properties.put(Property.CALENDAR_HOMESET, URIUtils.ensureTrailingSlash(prop.calendarHomeSet.getHref().href)); if (prop.displayname != null) properties.put(Property.DISPLAY_NAME, prop.displayname.getDisplayName()); if (prop.resourcetype != null) { + if (prop.resourcetype.getCollection() != null) { + properties.put(Property.IS_COLLECTION, "1"); + // is a collection, ensure trailing slash + href = URIUtils.ensureTrailingSlash(href); + } if (prop.resourcetype.getAddressbook() != null) { // CardDAV collection properties properties.put(Property.IS_ADDRESSBOOK, "1"); @@ -554,9 +538,9 @@ public class WebDavResource { properties.put(Property.TIMEZONE, Event.TimezoneDefToTzId(prop.calendarTimezone.getTimezone())); if (prop.supportedCalendarComponentSet != null) { - referenced.supportedComponents = new LinkedList(); + supportedComponents = new LinkedList(); for (Comp component : prop.supportedCalendarComponentSet) - referenced.supportedComponents.add(component.getName()); + supportedComponents.add(component.getName()); } } } @@ -568,9 +552,26 @@ public class WebDavResource { properties.put(Property.ETAG, prop.getetag.getETag()); if (prop.calendarData != null && prop.calendarData.ical != null) - referenced.content = prop.calendarData.ical.getBytes(); + data = prop.calendarData.ical.getBytes(); else if (prop.addressData != null && prop.addressData.vcard != null) - referenced.content = prop.addressData.vcard.getBytes(); + data = prop.addressData.vcard.getBytes(); + } + + // about which resource is this response? + // "this" resource is either at "location" … + if (location.equals(href)) { // -> ourselves + this.properties.putAll(properties); + if (supportedComponents != null) + this.supportedComponents = supportedComponents; + this.content = data; + + } else { + WebDavResource member = new WebDavResource(this, href); + member.properties = properties; + member.supportedComponents = supportedComponents; + member.content = data; + + members.add(member); } } diff --git a/test/robohydra/plugins/dav-invalid/index.js b/test/robohydra/plugins/dav-invalid/index.js index dba0c28d..b14ee944 100644 --- a/test/robohydra/plugins/dav-invalid/index.js +++ b/test/robohydra/plugins/dav-invalid/index.js @@ -35,7 +35,7 @@ exports.getBodyParts = function(conf) { \ \ \ - Address Book with absolute URL\ + Address Book with absolute URL and at sign in path\ \ \ HTTP/1.1 200 OK\ diff --git a/test/src/at/bitfire/davdroid/test/URIUtilsTest.java b/test/src/at/bitfire/davdroid/test/URIUtilsTest.java index 25e9cd13..083f16bb 100644 --- a/test/src/at/bitfire/davdroid/test/URIUtilsTest.java +++ b/test/src/at/bitfire/davdroid/test/URIUtilsTest.java @@ -7,11 +7,24 @@ ******************************************************************************/ package at.bitfire.davdroid.test; +import java.net.URI; +import java.net.URISyntaxException; + import junit.framework.TestCase; import at.bitfire.davdroid.URIUtils; public class URIUtilsTest extends TestCase { + public void testEnsureTrailingSlash() throws URISyntaxException { + assertEquals("/test/", URIUtils.ensureTrailingSlash("/test")); + assertEquals("/test/", URIUtils.ensureTrailingSlash("/test/")); + + String withoutSlash = "http://www.test.at/dav/collection", + withSlash = withoutSlash + "/"; + assertEquals(new URI(withSlash), URIUtils.ensureTrailingSlash(new URI(withoutSlash))); + assertEquals(new URI(withSlash), URIUtils.ensureTrailingSlash(new URI(withSlash))); + } + public void testSanitize() { assertNull(URIUtils.sanitize(null)); diff --git a/test/src/at/bitfire/davdroid/webdav/WebDavResourceTest.java b/test/src/at/bitfire/davdroid/webdav/WebDavResourceTest.java index 3c7856eb..0dd948cd 100644 --- a/test/src/at/bitfire/davdroid/webdav/WebDavResourceTest.java +++ b/test/src/at/bitfire/davdroid/webdav/WebDavResourceTest.java @@ -103,7 +103,7 @@ public class WebDavResourceTest extends InstrumentationTestCase { public void testPropfindHomeSets() throws IOException, HttpException, DavException { WebDavResource dav = new WebDavResource(davCollection, "principals/users/test"); dav.propfind(HttpPropfind.Mode.HOME_SETS); - assertEquals("/dav/addressbooks/test", dav.getAddressbookHomeSet()); + assertEquals("/dav/addressbooks/test/", dav.getAddressbookHomeSet()); assertEquals("/dav/calendars/test/", dav.getCalendarHomeSet()); } @@ -221,7 +221,7 @@ public class WebDavResourceTest extends InstrumentationTestCase { List members = dav.getMembers(); assertEquals(2, members.size()); assertEquals(Constants.ROBOHYDRA_BASE + "dav/addressbooks/user%40domain/My%20Contacts%3a1.vcf/", members.get(0).getLocation().toString()); - assertEquals("HTTPS://example.com/user%40domain/absolute-url.vcf", members.get(1).getLocation().toString()); + assertEquals("HTTPS://example.com/user%40domain/absolute-url.vcf/", members.get(1).getLocation().toString()); } }