mirror of https://github.com/tasks/tasks
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
345 lines
13 KiB
Java
345 lines
13 KiB
Java
package org.tasks.caldav;
|
|
|
|
import static at.bitfire.dav4jvm.XmlUtils.NS_CALDAV;
|
|
import static at.bitfire.dav4jvm.XmlUtils.NS_CARDDAV;
|
|
import static at.bitfire.dav4jvm.XmlUtils.NS_WEBDAV;
|
|
import static org.tasks.Strings.isNullOrEmpty;
|
|
|
|
import android.content.Context;
|
|
import at.bitfire.cert4android.CustomCertManager;
|
|
import at.bitfire.cert4android.CustomCertManager.CustomHostnameVerifier;
|
|
import at.bitfire.dav4jvm.BasicDigestAuthHandler;
|
|
import at.bitfire.dav4jvm.DavResource;
|
|
import at.bitfire.dav4jvm.Property.Name;
|
|
import at.bitfire.dav4jvm.Response;
|
|
import at.bitfire.dav4jvm.Response.HrefRelation;
|
|
import at.bitfire.dav4jvm.XmlUtils;
|
|
import at.bitfire.dav4jvm.exception.DavException;
|
|
import at.bitfire.dav4jvm.exception.HttpException;
|
|
import at.bitfire.dav4jvm.property.CalendarColor;
|
|
import at.bitfire.dav4jvm.property.CalendarHomeSet;
|
|
import at.bitfire.dav4jvm.property.CurrentUserPrincipal;
|
|
import at.bitfire.dav4jvm.property.DisplayName;
|
|
import at.bitfire.dav4jvm.property.GetCTag;
|
|
import at.bitfire.dav4jvm.property.ResourceType;
|
|
import at.bitfire.dav4jvm.property.SupportedCalendarComponentSet;
|
|
import at.bitfire.dav4jvm.property.SyncToken;
|
|
import com.todoroo.astrid.helper.UUIDHelper;
|
|
import dagger.hilt.android.qualifiers.ApplicationContext;
|
|
import java.io.IOException;
|
|
import java.io.StringWriter;
|
|
import java.security.KeyManagementException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.concurrent.TimeUnit;
|
|
import javax.inject.Inject;
|
|
import javax.net.ssl.SSLContext;
|
|
import javax.net.ssl.TrustManager;
|
|
import okhttp3.HttpUrl;
|
|
import okhttp3.OkHttpClient;
|
|
import okhttp3.OkHttpClient.Builder;
|
|
import okhttp3.internal.tls.OkHostnameVerifier;
|
|
import org.tasks.DebugNetworkInterceptor;
|
|
import org.tasks.R;
|
|
import org.tasks.data.CaldavAccount;
|
|
import org.tasks.data.CaldavCalendar;
|
|
import org.tasks.preferences.Preferences;
|
|
import org.tasks.security.KeyStoreEncryption;
|
|
import org.tasks.ui.DisplayableException;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
import org.xmlpull.v1.XmlPullParserFactory;
|
|
import org.xmlpull.v1.XmlSerializer;
|
|
import timber.log.Timber;
|
|
|
|
public class CaldavClient {
|
|
|
|
private final KeyStoreEncryption encryption;
|
|
private final Preferences preferences;
|
|
private final DebugNetworkInterceptor interceptor;
|
|
private final OkHttpClient httpClient;
|
|
private final HttpUrl httpUrl;
|
|
private final Context context;
|
|
private final BasicDigestAuthHandler basicDigestAuthHandler;
|
|
private boolean foreground;
|
|
|
|
@Inject
|
|
CaldavClient(
|
|
@ApplicationContext Context context,
|
|
KeyStoreEncryption encryption,
|
|
Preferences preferences,
|
|
DebugNetworkInterceptor interceptor) {
|
|
this.context = context;
|
|
this.encryption = encryption;
|
|
this.preferences = preferences;
|
|
this.interceptor = interceptor;
|
|
httpClient = null;
|
|
httpUrl = null;
|
|
basicDigestAuthHandler = null;
|
|
}
|
|
|
|
private CaldavClient(
|
|
Context context,
|
|
KeyStoreEncryption encryption,
|
|
Preferences preferences,
|
|
DebugNetworkInterceptor interceptor,
|
|
String url,
|
|
String username,
|
|
String password,
|
|
boolean foreground)
|
|
throws NoSuchAlgorithmException, KeyManagementException {
|
|
this.context = context;
|
|
this.encryption = encryption;
|
|
this.preferences = preferences;
|
|
this.interceptor = interceptor;
|
|
|
|
CustomCertManager customCertManager = new CustomCertManager(context);
|
|
customCertManager.setAppInForeground(foreground);
|
|
CustomHostnameVerifier hostnameVerifier =
|
|
customCertManager.hostnameVerifier(OkHostnameVerifier.INSTANCE);
|
|
SSLContext sslContext = SSLContext.getInstance("TLS");
|
|
sslContext.init(null, new TrustManager[] {customCertManager}, null);
|
|
|
|
basicDigestAuthHandler = new BasicDigestAuthHandler(null, username, password);
|
|
Builder builder =
|
|
new OkHttpClient()
|
|
.newBuilder()
|
|
.addNetworkInterceptor(basicDigestAuthHandler)
|
|
.authenticator(basicDigestAuthHandler)
|
|
.cookieJar(new MemoryCookieStore())
|
|
.followRedirects(false)
|
|
.followSslRedirects(true)
|
|
.sslSocketFactory(sslContext.getSocketFactory(), customCertManager)
|
|
.hostnameVerifier(hostnameVerifier)
|
|
.connectTimeout(15, TimeUnit.SECONDS)
|
|
.writeTimeout(30, TimeUnit.SECONDS)
|
|
.readTimeout(120, TimeUnit.SECONDS);
|
|
if (preferences.isFlipperEnabled()) {
|
|
interceptor.add(builder);
|
|
}
|
|
httpClient = builder.build();
|
|
httpUrl = HttpUrl.parse(url);
|
|
}
|
|
|
|
public CaldavClient forAccount(CaldavAccount account)
|
|
throws NoSuchAlgorithmException, KeyManagementException {
|
|
return forUrl(account.getUrl(), account.getUsername(), account.getPassword(encryption));
|
|
}
|
|
|
|
CaldavClient forCalendar(CaldavAccount account, CaldavCalendar calendar)
|
|
throws NoSuchAlgorithmException, KeyManagementException {
|
|
return forUrl(calendar.getUrl(), account.getUsername(), account.getPassword(encryption));
|
|
}
|
|
|
|
CaldavClient forUrl(String url, String username, String password)
|
|
throws KeyManagementException, NoSuchAlgorithmException {
|
|
return new CaldavClient(
|
|
context, encryption, preferences, interceptor, url, username, password, foreground);
|
|
}
|
|
|
|
private String tryFindPrincipal(String link) throws DavException, IOException {
|
|
HttpUrl url = httpUrl.resolve(link);
|
|
Timber.d("Checking for principal: %s", url);
|
|
DavResource davResource = new DavResource(httpClient, url);
|
|
ResponseList responses = new ResponseList();
|
|
davResource.propfind(0, new Name[] {CurrentUserPrincipal.NAME}, responses);
|
|
if (!responses.isEmpty()) {
|
|
Response response = responses.get(0);
|
|
CurrentUserPrincipal currentUserPrincipal = response.get(CurrentUserPrincipal.class);
|
|
if (currentUserPrincipal != null) {
|
|
String href = currentUserPrincipal.getHref();
|
|
if (!isNullOrEmpty(href)) {
|
|
return href;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private String findHomeset() throws DavException, IOException {
|
|
DavResource davResource = new DavResource(httpClient, httpUrl);
|
|
ResponseList responses = new ResponseList();
|
|
davResource.propfind(0, new Name[] {CalendarHomeSet.NAME}, responses);
|
|
Response response = responses.get(0);
|
|
CalendarHomeSet calendarHomeSet = response.get(CalendarHomeSet.class);
|
|
if (calendarHomeSet == null) {
|
|
throw new DisplayableException(R.string.caldav_home_set_not_found);
|
|
}
|
|
List<String> hrefs = calendarHomeSet.getHrefs();
|
|
if (hrefs.size() != 1) {
|
|
throw new DisplayableException(R.string.caldav_home_set_not_found);
|
|
}
|
|
String homeSet = hrefs.get(0);
|
|
if (isNullOrEmpty(homeSet)) {
|
|
throw new DisplayableException(R.string.caldav_home_set_not_found);
|
|
}
|
|
return davResource.getLocation().resolve(homeSet).toString();
|
|
}
|
|
|
|
String getHomeSet()
|
|
throws IOException, DavException, NoSuchAlgorithmException, KeyManagementException {
|
|
String principal = null;
|
|
try {
|
|
principal = tryFindPrincipal("/.well-known/caldav");
|
|
} catch (Exception e) {
|
|
if (e instanceof HttpException && ((HttpException) e).getCode() == 401) {
|
|
throw e;
|
|
}
|
|
|
|
Timber.w(e);
|
|
}
|
|
if (principal == null) {
|
|
principal = tryFindPrincipal("");
|
|
}
|
|
return forUrl(
|
|
(isNullOrEmpty(principal) ? this.httpUrl : httpUrl.resolve(principal)).toString(),
|
|
basicDigestAuthHandler.getUsername(),
|
|
basicDigestAuthHandler.getPassword())
|
|
.findHomeset();
|
|
}
|
|
|
|
public List<Response> getCalendars() throws IOException, DavException {
|
|
DavResource davResource = new DavResource(httpClient, httpUrl);
|
|
ResponseList responses = new ResponseList(HrefRelation.MEMBER);
|
|
davResource.propfind(
|
|
1,
|
|
new Name[] {
|
|
ResourceType.NAME,
|
|
DisplayName.NAME,
|
|
SupportedCalendarComponentSet.NAME,
|
|
GetCTag.NAME,
|
|
CalendarColor.NAME,
|
|
SyncToken.NAME
|
|
},
|
|
responses);
|
|
List<Response> urls = new ArrayList<>();
|
|
for (Response member : responses) {
|
|
ResourceType resourceType = member.get(ResourceType.class);
|
|
if (resourceType == null
|
|
|| !resourceType.getTypes().contains(ResourceType.Companion.getCALENDAR())) {
|
|
Timber.d("%s is not a calendar", member);
|
|
continue;
|
|
}
|
|
SupportedCalendarComponentSet supportedCalendarComponentSet =
|
|
member.get(SupportedCalendarComponentSet.class);
|
|
if (supportedCalendarComponentSet == null
|
|
|| !supportedCalendarComponentSet.getSupportsTasks()) {
|
|
Timber.d("%s does not support tasks", member);
|
|
continue;
|
|
}
|
|
urls.add(member);
|
|
}
|
|
return urls;
|
|
}
|
|
|
|
void deleteCollection() throws IOException, HttpException {
|
|
new DavResource(httpClient, httpUrl).delete(null, response -> null);
|
|
}
|
|
|
|
String makeCollection(String displayName, int color)
|
|
throws IOException, XmlPullParserException, HttpException {
|
|
DavResource davResource =
|
|
new DavResource(httpClient, httpUrl.resolve(UUIDHelper.newUUID() + "/"));
|
|
String mkcolString = getMkcolString(displayName, color);
|
|
|
|
davResource.mkCol(mkcolString, response -> null);
|
|
return davResource.getLocation().toString();
|
|
}
|
|
|
|
String updateCollection(String displayName, int color)
|
|
throws IOException, XmlPullParserException, HttpException {
|
|
PatchableDavResource davResource = new PatchableDavResource(httpClient, httpUrl);
|
|
davResource.propPatch(getPropPatchString(displayName, color), response -> null);
|
|
return davResource.getLocation().toString();
|
|
}
|
|
|
|
private String getPropPatchString(String displayName, int color)
|
|
throws IOException, XmlPullParserException {
|
|
XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
|
|
XmlSerializer xml = xmlPullParserFactory.newSerializer();
|
|
StringWriter stringWriter = new StringWriter();
|
|
xml.setOutput(stringWriter);
|
|
xml.startDocument("UTF-8", null);
|
|
xml.setPrefix("", NS_WEBDAV);
|
|
xml.setPrefix("CAL", NS_CALDAV);
|
|
xml.setPrefix("CARD", NS_CARDDAV);
|
|
xml.startTag(NS_WEBDAV, "propertyupdate");
|
|
xml.startTag(XmlUtils.NS_WEBDAV, "set");
|
|
xml.startTag(XmlUtils.NS_WEBDAV, "prop");
|
|
setDisplayName(xml, displayName);
|
|
if (color != 0) {
|
|
setColor(xml, color);
|
|
}
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "prop");
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "set");
|
|
if (color == 0) {
|
|
xml.startTag(XmlUtils.NS_WEBDAV, "remove");
|
|
xml.startTag(XmlUtils.NS_WEBDAV, "prop");
|
|
xml.startTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
|
|
xml.endTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "prop");
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "remove");
|
|
}
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "propertyupdate");
|
|
xml.endDocument();
|
|
xml.flush();
|
|
return stringWriter.toString();
|
|
}
|
|
|
|
private String getMkcolString(String displayName, int color) throws IOException, XmlPullParserException {
|
|
XmlPullParserFactory xmlPullParserFactory = XmlPullParserFactory.newInstance();
|
|
XmlSerializer xml = xmlPullParserFactory.newSerializer();
|
|
StringWriter stringWriter = new StringWriter();
|
|
xml.setOutput(stringWriter);
|
|
xml.startDocument("UTF-8", null);
|
|
xml.setPrefix("", NS_WEBDAV);
|
|
xml.setPrefix("CAL", NS_CALDAV);
|
|
xml.setPrefix("CARD", NS_CARDDAV);
|
|
xml.startTag(NS_WEBDAV, "mkcol");
|
|
xml.startTag(XmlUtils.NS_WEBDAV, "set");
|
|
xml.startTag(XmlUtils.NS_WEBDAV, "prop");
|
|
xml.startTag(XmlUtils.NS_WEBDAV, "resourcetype");
|
|
xml.startTag(XmlUtils.NS_WEBDAV, "collection");
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "collection");
|
|
xml.startTag(XmlUtils.NS_CALDAV, "calendar");
|
|
xml.endTag(XmlUtils.NS_CALDAV, "calendar");
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "resourcetype");
|
|
setDisplayName(xml, displayName);
|
|
if (color != 0) {
|
|
setColor(xml, color);
|
|
}
|
|
xml.startTag(XmlUtils.NS_CALDAV, "supported-calendar-component-set");
|
|
xml.startTag(XmlUtils.NS_CALDAV, "comp");
|
|
xml.attribute(null, "name", "VTODO");
|
|
xml.endTag(XmlUtils.NS_CALDAV, "comp");
|
|
xml.endTag(XmlUtils.NS_CALDAV, "supported-calendar-component-set");
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "prop");
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "set");
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "mkcol");
|
|
xml.endDocument();
|
|
xml.flush();
|
|
return stringWriter.toString();
|
|
}
|
|
|
|
private void setDisplayName(XmlSerializer xml, String name) throws IOException {
|
|
xml.startTag(XmlUtils.NS_WEBDAV, "displayname");
|
|
xml.text(name);
|
|
xml.endTag(XmlUtils.NS_WEBDAV, "displayname");
|
|
}
|
|
|
|
private void setColor(XmlSerializer xml, int color) throws IOException {
|
|
xml.startTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
|
|
xml.text(String.format("#%06X%02X", color & 0xFFFFFF, color >>> 24));
|
|
xml.endTag(XmlUtils.NS_APPLE_ICAL, "calendar-color");
|
|
}
|
|
|
|
OkHttpClient getHttpClient() {
|
|
return httpClient;
|
|
}
|
|
|
|
CaldavClient setForeground() {
|
|
foreground = true;
|
|
return this;
|
|
}
|
|
}
|