From 2c79ae20e52b16144e2e9ecf611e0145d44c6a5a Mon Sep 17 00:00:00 2001 From: rfc2822 Date: Sun, 27 Jul 2014 12:54:53 +0200 Subject: [PATCH] Move resource detection to separate class + tests --- .gitignore | 3 + res/values-ca/strings.xml | 3 - res/values-de/strings.xml | 5 +- res/values-es/strings.xml | 3 - res/values-fr/strings.xml | 3 - res/values/strings.xml | 5 +- .../syncadapter/DavResourceFinder.java | 160 ++++++++++++++++++ .../QueryServerDialogFragment.java | 140 +-------------- test/robohydra/plugins/dav/index.js | 17 +- test/robohydra/plugins/headdav.js | 7 +- test/robohydra/plugins/redirect/index.js | 2 +- .../syncadapter/DavResourceFinderTest.java | 37 ++++ .../webdav/DavRedirectStrategyTest.java | 4 +- 13 files changed, 219 insertions(+), 170 deletions(-) create mode 100644 src/at/bitfire/davdroid/syncadapter/DavResourceFinder.java create mode 100644 test/src/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java diff --git a/.gitignore b/.gitignore index 02804d0f..526adfcc 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ proguard/ *.iws .idea/ +# private files +.private + ### ECLIPSE diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index a1e821b2..081e7c1e 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -21,9 +21,6 @@ URI invàlida: %s Missing capabilities: %s Ni CalDAV ni CardDAV estan disponibles - "No s\'ha pogut determinar la ruta principal (RFC 5397)" - "No s\'ha pogut determinar l\'arrel de les llibretes de contactes" - "No s\'ha pogut determinar l\'arrel dels calendaris" Afegir compte Querying server. Please wait… Error HTTP: %s diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 40eec9b3..0fc21673 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -12,10 +12,7 @@ E/A-Fehler: %s Ungültiger URI: %s Fehlende Server-Unterstützung: %s - Weder CalDAV noch CardDAV ist verfügbar. - Benutzerpfad konnte nicht festgestellt werden (RFC 5397) - Adressbuch-Ordner konnte nicht festgestellt werden - Kalender-Ordner konnte nicht festgestellt werden + An dieser Adresse konnte kein CalDAV- oder CardDAV-Dienst gefunden werden. Konto hinzufügen Daten werden vom Server abgefragt. Bitte warten… HTTP-Fehler: %s diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 36a23fb5..8bd05f9e 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -21,9 +21,6 @@ URI no válida: %s Se han perdido capacidades: %s Ni CalDAV ni CardDAV están disponibles - "No se ha podido determinar la ruta principal (RFC 5397)" - "No se ha podido determinar la configuración de inicio de la agenda" - "No se ha podido determinar la configuración de inicio del calendario" Añadir cuenta Consultando el servidor. Espera, por favor… error HTTP: %s diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 1169b9c7..0f04db60 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -21,9 +21,6 @@ URI incorrecte: %s Capacités manquantes: %s Aucun CalDAV ou CardDAV disponible - Impossible de déterminer le chemin principal(RFC 5397) - Impossible de déterminer le type de carnets d\'adresses - Impossible de déterminer le type d\'agendas" Ajouter un compte Interroge le serveur. Patientez svp. Erreur HTTP: %s diff --git a/res/values/strings.xml b/res/values/strings.xml index 28d644e9..80b3f77e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -20,10 +20,7 @@ I/O error: %s Invalid URI: %s Missing capabilities: %s - Neither CalDAV nor CardDAV available - "Couldn't determine principal path (RFC 5397)" - "Couldn't determine address book home set" - "Couldn't determine calendar home set" + No CalDAV-/CardDAV service is available at this location. Add account Querying server. Please wait… HTTP error: %s diff --git a/src/at/bitfire/davdroid/syncadapter/DavResourceFinder.java b/src/at/bitfire/davdroid/syncadapter/DavResourceFinder.java new file mode 100644 index 00000000..8a1484c9 --- /dev/null +++ b/src/at/bitfire/davdroid/syncadapter/DavResourceFinder.java @@ -0,0 +1,160 @@ +package at.bitfire.davdroid.syncadapter; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.LinkedList; +import java.util.List; + +import ch.boye.httpclientandroidlib.HttpException; +import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient; +import android.content.Context; +import android.util.Log; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.DavHttpClient; +import at.bitfire.davdroid.webdav.DavIncapableException; +import at.bitfire.davdroid.webdav.WebDavResource; +import at.bitfire.davdroid.webdav.HttpPropfind.Mode; + +public class DavResourceFinder { + private final static String TAG = "davdroid.DavResourceFinder"; + + + public static void findResources(Context context, ServerInfo serverInfo) throws URISyntaxException, DavException, HttpException, IOException { + // disable compression and enable network logging for debugging purposes + CloseableHttpClient httpClient = DavHttpClient.create(true, true); + + WebDavResource base = new WebDavResource(httpClient, new URI(serverInfo.getProvidedURL()), serverInfo.getUserName(), + serverInfo.getPassword(), serverInfo.isAuthPreemptive()); + + // CardDAV + WebDavResource principal = getCurrentUserPrincipal(base, "carddav"); + if (principal != null) { + serverInfo.setCardDAV(true); + + principal.propfind(Mode.HOME_SETS); + String pathAddressBooks = principal.getAddressbookHomeSet(); + if (pathAddressBooks != null) { + Log.i(TAG, "Found address book home set: " + pathAddressBooks); + + WebDavResource homeSetAddressBooks = new WebDavResource(principal, pathAddressBooks); + if (checkCapabilities(homeSetAddressBooks, "addressbook")) { + homeSetAddressBooks.propfind(Mode.MEMBERS_COLLECTIONS); + + List addressBooks = new LinkedList(); + if (homeSetAddressBooks.getMembers() != null) + for (WebDavResource resource : homeSetAddressBooks.getMembers()) + if (resource.isAddressBook()) { + Log.i(TAG, "Found address book: " + resource.getLocation().getRawPath()); + ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( + ServerInfo.ResourceInfo.Type.ADDRESS_BOOK, + resource.isReadOnly(), + resource.getLocation().toASCIIString(), + resource.getDisplayName(), + resource.getDescription(), resource.getColor() + ); + addressBooks.add(info); + } + serverInfo.setAddressBooks(addressBooks); + } else + Log.w(TAG, "Found address-book home set, but it doesn't advertise CardDAV support"); + } + } + + // CalDAV + principal = getCurrentUserPrincipal(base, "caldav"); + if (principal != null) { + serverInfo.setCalDAV(true); + + principal.propfind(Mode.HOME_SETS); + String pathCalendars = principal.getCalendarHomeSet(); + if (pathCalendars != null) { + Log.i(TAG, "Found calendar home set: " + pathCalendars); + + WebDavResource homeSetCalendars = new WebDavResource(principal, pathCalendars); + if (checkCapabilities(homeSetCalendars, "calendar-access")) { + homeSetCalendars.propfind(Mode.MEMBERS_COLLECTIONS); + + List calendars = new LinkedList(); + if (homeSetCalendars.getMembers() != null) + for (WebDavResource resource : homeSetCalendars.getMembers()) + if (resource.isCalendar()) { + Log.i(TAG, "Found calendar: " + resource.getLocation().getRawPath()); + 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 + continue; + } + ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( + ServerInfo.ResourceInfo.Type.CALENDAR, + resource.isReadOnly(), + resource.getLocation().toASCIIString(), + resource.getDisplayName(), + resource.getDescription(), resource.getColor() + ); + info.setTimezone(resource.getTimezone()); + calendars.add(info); + } + serverInfo.setCalendars(calendars); + } else + Log.w(TAG, "Found calendar home set, but it doesn't advertise CalDAV support"); + } + } + + if (!serverInfo.isCalDAV() && !serverInfo.isCardDAV()) + throw new DavIncapableException(context.getString(R.string.neither_caldav_nor_carddav)); + + } + + + /** + * Detects the current-user-principal for a given WebDavResource. At first, /.well-known/ is tried. Only + * if no current-user-principal can be detected for the .well-known location, the given location of the resource + * is tried. + * @param resource Location that will be queried + * @param serviceName Well-known service name ("carddav", "caldav") + * @return WebDavResource of current-user-principal for the given service, or null if it can't be found + */ + private static WebDavResource getCurrentUserPrincipal(WebDavResource resource, String serviceName) throws IOException, HttpException, DavException { + // look for well-known service (RFC 5785) + try { + WebDavResource wellKnown = new WebDavResource(resource, "/.well-known/" + serviceName); + wellKnown.propfind(Mode.CURRENT_USER_PRINCIPAL); + if (wellKnown.getCurrentUserPrincipal() != null) + return new WebDavResource(wellKnown, wellKnown.getCurrentUserPrincipal()); + } catch (HttpException e) { + Log.d(TAG, "Well-known service detection failed with HTTP error", e); + } catch (DavException e) { + Log.d(TAG, "Well-known service detection failed at DAV level", e); + } + + // fall back to user-given initial context path + resource.propfind(Mode.CURRENT_USER_PRINCIPAL); + if (resource.getCurrentUserPrincipal() != null) + return new WebDavResource(resource, resource.getCurrentUserPrincipal()); + return null; + } + + private static boolean checkCapabilities(WebDavResource resource, String davCapability) throws IOException { + // check for necessary capabilities + try { + resource.options(); + if (resource.supportsDAV(davCapability) && + resource.supportsMethod("PROPFIND") && + resource.supportsMethod("GET") && + resource.supportsMethod("REPORT") && + resource.supportsMethod("PUT") && + resource.supportsMethod("DELETE")) + return true; + } catch(HttpException e) { + // for instance, 405 Method not allowed + } + return false; + } + +} diff --git a/src/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java b/src/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java index 12f350c9..12c224b3 100644 --- a/src/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java +++ b/src/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java @@ -8,10 +8,7 @@ package at.bitfire.davdroid.syncadapter; import java.io.IOException; -import java.net.URI; import java.net.URISyntaxException; -import java.util.LinkedList; -import java.util.List; import android.app.DialogFragment; import android.app.LoaderManager.LoaderCallbacks; @@ -27,12 +24,7 @@ import android.widget.ProgressBar; import android.widget.Toast; import at.bitfire.davdroid.R; import at.bitfire.davdroid.webdav.DavException; -import at.bitfire.davdroid.webdav.DavHttpClient; -import at.bitfire.davdroid.webdav.DavIncapableException; -import at.bitfire.davdroid.webdav.HttpPropfind.Mode; -import at.bitfire.davdroid.webdav.WebDavResource; import ch.boye.httpclientandroidlib.HttpException; -import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient; public class QueryServerDialogFragment extends DialogFragment implements LoaderCallbacks { private static final String TAG = "davdroid.QueryServerDialogFragment"; @@ -111,94 +103,8 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC args.getBoolean(EXTRA_AUTH_PREEMPTIVE) ); - // disable compression and enable network logging for debugging purposes - CloseableHttpClient httpClient = DavHttpClient.create(true, true); - try { - WebDavResource base = new WebDavResource(httpClient, new URI(serverInfo.getProvidedURL()), serverInfo.getUserName(), - serverInfo.getPassword(), serverInfo.isAuthPreemptive()); - - // CardDAV - WebDavResource principal = getCurrentUserPrincipal(base, "carddav"); - if (principal != null) { - serverInfo.setCardDAV(true); - - principal.propfind(Mode.HOME_SETS); - String pathAddressBooks = principal.getAddressbookHomeSet(); - if (pathAddressBooks != null) { - Log.i(TAG, "Found address book home set: " + pathAddressBooks); - - WebDavResource homeSetAddressBooks = new WebDavResource(principal, pathAddressBooks); - if (checkCapabilities(homeSetAddressBooks, "addressbook")) { - homeSetAddressBooks.propfind(Mode.MEMBERS_COLLECTIONS); - - List addressBooks = new LinkedList(); - if (homeSetAddressBooks.getMembers() != null) - for (WebDavResource resource : homeSetAddressBooks.getMembers()) - if (resource.isAddressBook()) { - Log.i(TAG, "Found address book: " + resource.getLocation().getRawPath()); - ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( - ServerInfo.ResourceInfo.Type.ADDRESS_BOOK, - resource.isReadOnly(), - resource.getLocation().toASCIIString(), - resource.getDisplayName(), - resource.getDescription(), resource.getColor() - ); - addressBooks.add(info); - } - serverInfo.setAddressBooks(addressBooks); - } else - Log.w(TAG, "Found address-book home set, but it doesn't advertise CardDAV support"); - } - } - - // CalDAV - principal = getCurrentUserPrincipal(base, "caldav"); - if (principal != null) { - serverInfo.setCalDAV(true); - - principal.propfind(Mode.HOME_SETS); - String pathCalendars = principal.getCalendarHomeSet(); - if (pathCalendars != null) { - Log.i(TAG, "Found calendar home set: " + pathCalendars); - - WebDavResource homeSetCalendars = new WebDavResource(principal, pathCalendars); - if (checkCapabilities(homeSetCalendars, "calendar-access")) { - homeSetCalendars.propfind(Mode.MEMBERS_COLLECTIONS); - - List calendars = new LinkedList(); - if (homeSetCalendars.getMembers() != null) - for (WebDavResource resource : homeSetCalendars.getMembers()) - if (resource.isCalendar()) { - Log.i(TAG, "Found calendar: " + resource.getLocation().getRawPath()); - 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 - continue; - } - ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( - ServerInfo.ResourceInfo.Type.CALENDAR, - resource.isReadOnly(), - resource.getLocation().toASCIIString(), - resource.getDisplayName(), - resource.getDescription(), resource.getColor() - ); - info.setTimezone(resource.getTimezone()); - calendars.add(info); - } - serverInfo.setCalendars(calendars); - } else - Log.w(TAG, "Found calendar home set, but it doesn't advertise CalDAV support"); - } - } - - if (!serverInfo.isCalDAV() && !serverInfo.isCardDAV()) - throw new DavIncapableException(getContext().getString(R.string.neither_caldav_nor_carddav)); - + DavResourceFinder.findResources(context, serverInfo); } catch (URISyntaxException e) { serverInfo.setErrorMessage(getContext().getString(R.string.exception_uri_syntax, e.getMessage())); } catch (IOException e) { @@ -214,49 +120,5 @@ public class QueryServerDialogFragment extends DialogFragment implements LoaderC return serverInfo; } - /** - * Detects the current-user-principal for a given WebDavResource. At first, /.well-known/ is tried. Only - * if no current-user-principal can be detected for the .well-known location, the given location of the resource - * is tried. - * @param resource Location that will be queried - * @param serviceName Well-known service name ("carddav", "caldav") - * @return WebDavResource of current-user-principal for the given service, or null if it can't be found - */ - private WebDavResource getCurrentUserPrincipal(WebDavResource resource, String serviceName) throws IOException, HttpException, DavException { - // look for well-known service (RFC 5785) - try { - WebDavResource wellKnown = new WebDavResource(resource, "/.well-known/" + serviceName); - wellKnown.propfind(Mode.CURRENT_USER_PRINCIPAL); - if (wellKnown.getCurrentUserPrincipal() != null) - return new WebDavResource(wellKnown, wellKnown.getCurrentUserPrincipal()); - } catch (HttpException e) { - Log.d(TAG, "Well-known service detection failed with HTTP error", e); - } catch (DavException e) { - Log.d(TAG, "Well-known service detection failed at DAV level", e); - } - - // fall back to user-given initial context path - resource.propfind(Mode.CURRENT_USER_PRINCIPAL); - if (resource.getCurrentUserPrincipal() != null) - return new WebDavResource(resource, resource.getCurrentUserPrincipal()); - return null; - } - - private boolean checkCapabilities(WebDavResource resource, String davCapability) throws IOException { - // check for necessary capabilities - try { - resource.options(); - if (resource.supportsDAV(davCapability) && - resource.supportsMethod("PROPFIND") && - resource.supportsMethod("GET") && - resource.supportsMethod("REPORT") && - resource.supportsMethod("PUT") && - resource.supportsMethod("DELETE")) - return true; - } catch(HttpException e) { - // for instance, 405 Method not allowed - } - return false; - } } } diff --git a/test/robohydra/plugins/dav/index.js b/test/robohydra/plugins/dav/index.js index 8ca613c3..f3bd64d1 100644 --- a/test/robohydra/plugins/dav/index.js +++ b/test/robohydra/plugins/dav/index.js @@ -3,11 +3,8 @@ var roboHydraHeadDAV = require("../headdav"); exports.getBodyParts = function(conf) { return { heads: [ - /* base URL */ - new RoboHydraHeadDAV({ - path: "/dav/", - handler: function(req,res,next) { } - }), + /* base URL, provide default DAV here */ + new RoboHydraHeadDAV({ path: "/dav/" }), /* principal URL */ new RoboHydraHeadDAV({ @@ -66,9 +63,7 @@ exports.getBodyParts = function(conf) { \ \ \ - \ - Default Address Book\ - \ + Default Address Book\ \ HTTP/1.1 200 OK\ \ @@ -104,6 +99,8 @@ exports.getBodyParts = function(conf) { \ \ \ + Private Calendar\ + This is my private calendar.\ \ HTTP/1.1 200 OK\ \ @@ -116,6 +113,10 @@ exports.getBodyParts = function(conf) { \ \ \ + \ + \ + \ + Work Calendar\ 0xFF00FF\ \ HTTP/1.1 200 OK\ diff --git a/test/robohydra/plugins/headdav.js b/test/robohydra/plugins/headdav.js index 669868f9..c9c25524 100644 --- a/test/robohydra/plugins/headdav.js +++ b/test/robohydra/plugins/headdav.js @@ -4,7 +4,8 @@ var roboHydra = require("robohydra"), RoboHydraHeadDAV = roboHydraHeads.roboHydraHeadType({ name: 'WebDAV Server', - mandatoryProperties: [ 'path', 'handler' ], + mandatoryProperties: [ 'path' ], + optionalProperties: [ 'handler' ], parentPropBuilder: function() { var myHandler = this.handler; @@ -18,7 +19,7 @@ RoboHydraHeadDAV = roboHydraHeads.roboHydraHeadType({ // DAV operations that work on all URLs if (req.method == "OPTIONS") { res.statusCode = 204; - res.headers['Allow'] = 'OPTIONS, PROPFIND, GET, PUT, DELETE'; + res.headers['Allow'] = 'OPTIONS, PROPFIND, GET, PUT, DELETE, REPORT'; } else if (req.method == "PROPFIND" && req.rawBody.toString().match(/current-user-principal/)) { res.statusCode = 207; @@ -38,7 +39,7 @@ RoboHydraHeadDAV = roboHydraHeads.roboHydraHeadType({ \ '); - } else + } else if (typeof myHandler != 'undefined') myHandler(req,res,next); res.end(); diff --git a/test/robohydra/plugins/redirect/index.js b/test/robohydra/plugins/redirect/index.js index b3b994f5..4c2cbab0 100644 --- a/test/robohydra/plugins/redirect/index.js +++ b/test/robohydra/plugins/redirect/index.js @@ -12,7 +12,7 @@ exports.getBodyParts = function(conf) { headers: { Location: '/dav/' } }), new SimpleResponseHead({ - path: '/.well-known/caldav', + path: '/.well-known/carddav', status: 302, headers: { Location: '/dav/' } }), diff --git a/test/src/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java b/test/src/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java new file mode 100644 index 00000000..a19be04c --- /dev/null +++ b/test/src/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java @@ -0,0 +1,37 @@ +package at.bitfire.davdroid.syncadapter; + +import java.util.List; + +import android.test.InstrumentationTestCase; +import at.bitfire.davdroid.syncadapter.ServerInfo.ResourceInfo; +import at.bitfire.davdroid.test.Constants; + +public class DavResourceFinderTest extends InstrumentationTestCase { + + public void testFindResources() throws Exception { + ServerInfo info = new ServerInfo(Constants.ROBOHYDRA_BASE, "test", "test", true); + DavResourceFinder.findResources(getInstrumentation().getContext(), info); + + // CardDAV + assertTrue(info.isCardDAV()); + List collections = info.getAddressBooks(); + assertEquals(1, collections.size()); + + assertEquals("Default Address Book", collections.get(0).getDescription()); + + // CalDAV + assertTrue(info.isCalDAV()); + collections = info.getCalendars(); + assertEquals(2, collections.size()); + + ResourceInfo resource = collections.get(0); + assertEquals("Private Calendar", resource.getTitle()); + assertEquals("This is my private calendar.", resource.getDescription()); + assertFalse(resource.isReadOnly()); + + resource = collections.get(1); + assertEquals("Work Calendar", resource.getTitle()); + assertTrue(resource.isReadOnly()); + } + +} diff --git a/test/src/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java b/test/src/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java index 25f267ce..8b92629d 100644 --- a/test/src/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java +++ b/test/src/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java @@ -2,7 +2,7 @@ package at.bitfire.davdroid.webdav; import java.io.IOException; -import android.test.InstrumentationTestCase; +import junit.framework.TestCase; import at.bitfire.davdroid.test.Constants; import ch.boye.httpclientandroidlib.HttpResponse; import ch.boye.httpclientandroidlib.client.methods.HttpOptions; @@ -12,7 +12,7 @@ import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient; import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder; import ch.boye.httpclientandroidlib.protocol.HttpContext; -public class DavRedirectStrategyTest extends InstrumentationTestCase { +public class DavRedirectStrategyTest extends TestCase { CloseableHttpClient httpClient; DavRedirectStrategy strategy = DavRedirectStrategy.INSTANCE;