├── .github ├── FUNDING.yml └── workflows │ ├── test.yml │ ├── build-kdoc.yml │ └── codeql.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── CHANGELOG.md ├── run-tests.sh ├── src ├── commonMain │ └── kotlin │ │ └── at │ │ └── bitfire │ │ └── dav4jvm │ │ ├── exception │ │ ├── InvalidPropertyException.kt │ │ ├── GoneException.kt │ │ ├── ForbiddenException.kt │ │ ├── ConflictException.kt │ │ ├── NotFoundException.kt │ │ ├── UnauthorizedException.kt │ │ ├── PreconditionFailedException.kt │ │ ├── HttpException.kt │ │ ├── ServiceUnavailableException.kt │ │ └── DavException.kt │ │ ├── Dav4jvm.kt │ │ ├── PropertyFactory.kt │ │ ├── property │ │ ├── Owner.kt │ │ ├── Source.kt │ │ ├── GroupMembership.kt │ │ ├── CalendarHomeSet.kt │ │ ├── AddressbookHomeSet.kt │ │ ├── CalendarProxyReadFor.kt │ │ ├── CalendarUserAddressSet.kt │ │ ├── CalendarProxyWriteFor.kt │ │ ├── GetCTag.kt │ │ ├── SyncToken.kt │ │ ├── DisplayName.kt │ │ ├── HrefListProperty.kt │ │ ├── CalendarTimezone.kt │ │ ├── CreationDate.kt │ │ ├── CalendarDescription.kt │ │ ├── AddMember.kt │ │ ├── AddressbookDescription.kt │ │ ├── GetContentType.kt │ │ ├── AddressData.kt │ │ ├── CalendarData.kt │ │ ├── GetLastModified.kt │ │ ├── MaxVCardSize.kt │ │ ├── QuotaUsedBytes.kt │ │ ├── MaxICalendarSize.kt │ │ ├── GetContentLength.kt │ │ ├── QuotaAvailableBytes.kt │ │ ├── CurrentUserPrincipal.kt │ │ ├── KtorAuthExtensions.kt │ │ ├── ScheduleTag.kt │ │ ├── ResourceType.kt │ │ ├── SupportedReportSet.kt │ │ ├── CalendarColor.kt │ │ ├── SupportedCalendarData.kt │ │ ├── SupportedAddressData.kt │ │ ├── GetETag.kt │ │ ├── SupportedCalendarComponentSet.kt │ │ └── CurrentUserPrivilegeSet.kt │ │ ├── QuotedStringUtils.kt │ │ ├── Property.kt │ │ ├── CallbackInterfaces.kt │ │ ├── Error.kt │ │ ├── PropStat.kt │ │ ├── StatusLine.kt │ │ ├── PropertyRegistry.kt │ │ ├── HttpUtils.kt │ │ ├── UrlUtils.kt │ │ ├── DavCollection.kt │ │ ├── XmlUtils.kt │ │ ├── DavAddressBook.kt │ │ ├── DavCalendar.kt │ │ └── Response.kt ├── jvmTest │ └── resources │ │ └── logback.xml └── commonTest │ └── kotlin │ └── at │ └── bitfire │ └── dav4jvm │ ├── property │ ├── PropertyTest.kt │ ├── CalendarDescriptionTest.kt │ ├── OwnerTest.kt │ └── GetETagTest.kt │ ├── ErrorTest.kt │ ├── HttpUtilsTest.kt │ ├── QuotedStringUtilsTest.kt │ ├── exception │ ├── HttpExceptionTest.kt │ ├── ServiceUnavailableExceptionTest.kt │ └── DavExceptionTest.kt │ ├── PropertyTest.kt │ ├── TestUtils.kt │ ├── XmlUtilsTest.kt │ ├── UrlUtilsTest.kt │ ├── BasicDigestAuthHandlerTest.kt │ └── DavCollectionTest.kt ├── .gitignore ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | github: bitfireAT 3 | custom: https://www.davx5.com/donate 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/McDjuady/dav4jvm/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | See https://github.com/bitfireAT/dav4jvm/compare/, for instance 3 | https://github.com/bitfireAT/dav4jvm/compare/2.1.2...2.1.3 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./gradlew -i check 4 | 5 | echo 6 | echo View lint report: 7 | echo -n file:// 8 | realpath build/outputs/lint-results-debug.html 9 | 10 | echo 11 | echo View local unit test reports: 12 | echo -n file:// 13 | realpath build/reports/tests/debug/index.html 14 | echo -n file:// 15 | realpath build/reports/tests/release/index.html 16 | 17 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/exception/InvalidPropertyException.kt: -------------------------------------------------------------------------------- 1 | package at.bitfire.dav4jvm.exception 2 | 3 | /** 4 | * Represents an invalid XML (WebDAV) property. This is for instance thrown 5 | * when parsing something like `...` 6 | * because a text value would be expected. 7 | */ 8 | class InvalidPropertyException(message: String): Exception(message) 9 | -------------------------------------------------------------------------------- /src/jvmTest/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/Dav4jvm.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import io.ktor.util.logging.* 10 | 11 | object Dav4jvm { 12 | 13 | var log: Logger = KtorSimpleLogger("Dav4Jvm") 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/property/PropertyTest.kt: -------------------------------------------------------------------------------- 1 | package at.bitfire.dav4jvm.property 2 | 3 | import at.bitfire.dav4jvm.Property 4 | import at.bitfire.dav4jvm.XmlUtils 5 | import io.kotest.core.spec.style.FunSpec 6 | 7 | open class PropertyTest : FunSpec() { 8 | 9 | companion object { 10 | 11 | fun parseProperty(s: String): List { 12 | val parser = XmlUtils.createReader("$s") 13 | parser.nextTag() // move into 14 | return Property.parse(parser) 15 | } 16 | 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: push 3 | jobs: 4 | test: 5 | name: Run tests 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: actions/setup-java@v3 10 | with: 11 | distribution: 'temurin' 12 | java-version: 17 13 | - uses: gradle/gradle-build-action@v2 14 | 15 | - name: Check 16 | run: ./gradlew --no-daemon check 17 | 18 | - name: Archive results 19 | uses: actions/upload-artifact@v2 20 | with: 21 | name: test-results 22 | path: build/reports 23 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/property/CalendarDescriptionTest.kt: -------------------------------------------------------------------------------- 1 | package at.bitfire.dav4jvm.property 2 | 3 | import kotlin.test.assertEquals 4 | 5 | object CalendarDescriptionTest : PropertyTest() { 6 | 7 | init { 8 | test("testCalendarDescription") { 9 | val results = 10 | parseProperty("My Calendar") 11 | val result = results.first() as CalendarDescription 12 | assertEquals("My Calendar", result.description) 13 | } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/ErrorTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import io.kotest.core.spec.style.FunSpec 10 | import nl.adaptivity.xmlutil.QName 11 | import kotlin.test.assertTrue 12 | 13 | object ErrorTest : FunSpec({ 14 | 15 | test("testEquals") { 16 | val errors = listOf(Error(QName("DAV:", "valid-sync-token"))) 17 | assertTrue(errors.contains(Error.VALID_SYNC_TOKEN)) 18 | } 19 | 20 | }) -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/exception/GoneException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | 12 | class GoneException internal constructor(statusCode: HttpStatusCode, exceptionData: ExceptionData) : 13 | HttpException(statusCode, exceptionData) { 14 | 15 | companion object { 16 | suspend operator fun invoke(httpResponse: HttpResponse) = 17 | GoneException(httpResponse.status, createExceptionData(httpResponse)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/exception/ForbiddenException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | 12 | class ForbiddenException internal constructor(statusCode: HttpStatusCode, exceptionData: ExceptionData) : 13 | HttpException(statusCode, exceptionData) { 14 | 15 | companion object { 16 | suspend operator fun invoke(httpResponse: HttpResponse) = 17 | ForbiddenException(httpResponse.status, createExceptionData(httpResponse)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/exception/ConflictException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | 12 | class ConflictException internal constructor(statusCode: HttpStatusCode, exceptionData: ExceptionData) : 13 | HttpException(statusCode, exceptionData) { 14 | 15 | companion object { 16 | suspend operator fun invoke(httpResponse: HttpResponse) = 17 | ConflictException(httpResponse.status, createExceptionData(httpResponse)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/exception/NotFoundException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | 12 | class NotFoundException internal constructor(statusCode: HttpStatusCode, exceptionData: ExceptionData) : 13 | HttpException(statusCode, exceptionData) { 14 | 15 | companion object { 16 | suspend operator fun invoke(httpResponse: HttpResponse) = 17 | NotFoundException(httpResponse.status, createExceptionData(httpResponse)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/exception/UnauthorizedException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | 12 | class UnauthorizedException internal constructor(statusCode: HttpStatusCode, exceptionData: ExceptionData) : 13 | HttpException(statusCode, exceptionData) { 14 | 15 | companion object { 16 | suspend operator fun invoke(httpResponse: HttpResponse) = 17 | UnauthorizedException(httpResponse.status, createExceptionData(httpResponse)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/exception/PreconditionFailedException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | 12 | class PreconditionFailedException internal constructor(statusCode: HttpStatusCode, exceptionData: ExceptionData) : 13 | HttpException(statusCode, exceptionData) { 14 | 15 | companion object { 16 | suspend operator fun invoke(httpResponse: HttpResponse) = 17 | PreconditionFailedException(httpResponse.status, createExceptionData(httpResponse)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/PropertyFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import nl.adaptivity.xmlutil.QName 10 | import nl.adaptivity.xmlutil.XmlReader 11 | 12 | interface PropertyFactory { 13 | 14 | /** 15 | * Name of the Property the factory creates, 16 | * e.g. QName("DAV:", "displayname") if the factory creates DisplayName objects) 17 | */ 18 | fun getName(): QName 19 | 20 | /** 21 | * Parses XML of a property and returns its data class. 22 | * @throws XmlPullParserException in case of parsing errors 23 | */ 24 | fun create(parser: XmlReader): Property? 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/Owner.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.XmlUtils 10 | import nl.adaptivity.xmlutil.QName 11 | import nl.adaptivity.xmlutil.XmlReader 12 | import kotlin.jvm.JvmField 13 | 14 | class Owner : HrefListProperty() { 15 | 16 | companion object { 17 | @JvmField 18 | val NAME = QName(XmlUtils.NS_WEBDAV, "owner") 19 | } 20 | 21 | 22 | object Factory : HrefListProperty.Factory() { 23 | 24 | override fun getName() = NAME 25 | 26 | override fun create(parser: XmlReader) = 27 | create(parser, Owner()) 28 | 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/Source.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.XmlUtils 10 | import nl.adaptivity.xmlutil.QName 11 | import nl.adaptivity.xmlutil.XmlReader 12 | import kotlin.jvm.JvmField 13 | 14 | class Source : HrefListProperty() { 15 | 16 | companion object { 17 | @JvmField 18 | val NAME = QName(XmlUtils.NS_CALENDARSERVER, "source") 19 | } 20 | 21 | 22 | object Factory : HrefListProperty.Factory() { 23 | 24 | override fun getName() = NAME 25 | 26 | override fun create(parser: XmlReader) = 27 | create(parser, Source()) 28 | 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/GroupMembership.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.XmlUtils 10 | import nl.adaptivity.xmlutil.QName 11 | import nl.adaptivity.xmlutil.XmlReader 12 | import kotlin.jvm.JvmField 13 | 14 | class GroupMembership : HrefListProperty() { 15 | 16 | companion object { 17 | @JvmField 18 | val NAME = QName(XmlUtils.NS_WEBDAV, "group-membership") 19 | } 20 | 21 | 22 | object Factory : HrefListProperty.Factory() { 23 | 24 | override fun getName() = NAME 25 | 26 | override fun create(parser: XmlReader) = 27 | create(parser, GroupMembership()) 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CalendarHomeSet.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.XmlUtils 10 | import nl.adaptivity.xmlutil.QName 11 | import nl.adaptivity.xmlutil.XmlReader 12 | import kotlin.jvm.JvmField 13 | 14 | class CalendarHomeSet : HrefListProperty() { 15 | 16 | companion object { 17 | @JvmField 18 | val NAME = QName(XmlUtils.NS_CALDAV, "calendar-home-set") 19 | } 20 | 21 | 22 | object Factory : HrefListProperty.Factory() { 23 | 24 | override fun getName() = NAME 25 | 26 | override fun create(parser: XmlReader) = 27 | create(parser, CalendarHomeSet()) 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/AddressbookHomeSet.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.XmlUtils 10 | import nl.adaptivity.xmlutil.QName 11 | import nl.adaptivity.xmlutil.XmlReader 12 | import kotlin.jvm.JvmField 13 | 14 | class AddressbookHomeSet : HrefListProperty() { 15 | 16 | companion object { 17 | @JvmField 18 | val NAME = QName(XmlUtils.NS_CARDDAV, "addressbook-home-set") 19 | } 20 | 21 | 22 | object Factory : HrefListProperty.Factory() { 23 | 24 | override fun getName() = NAME 25 | 26 | override fun create(parser: XmlReader) = 27 | create(parser, AddressbookHomeSet()) 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CalendarProxyReadFor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.XmlUtils 10 | import nl.adaptivity.xmlutil.QName 11 | import nl.adaptivity.xmlutil.XmlReader 12 | import kotlin.jvm.JvmField 13 | 14 | class CalendarProxyReadFor : HrefListProperty() { 15 | 16 | companion object { 17 | @JvmField 18 | val NAME = QName(XmlUtils.NS_CALENDARSERVER, "calendar-proxy-read-for") 19 | } 20 | 21 | 22 | object Factory : HrefListProperty.Factory() { 23 | 24 | override fun getName() = NAME 25 | 26 | override fun create(parser: XmlReader) = 27 | create(parser, CalendarProxyReadFor()) 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CalendarUserAddressSet.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.XmlUtils 10 | import nl.adaptivity.xmlutil.QName 11 | import nl.adaptivity.xmlutil.XmlReader 12 | import kotlin.jvm.JvmField 13 | 14 | class CalendarUserAddressSet : HrefListProperty() { 15 | 16 | companion object { 17 | @JvmField 18 | val NAME = QName(XmlUtils.NS_CALDAV, "calendar-user-address-set") 19 | } 20 | 21 | 22 | object Factory : HrefListProperty.Factory() { 23 | 24 | override fun getName() = NAME 25 | 26 | override fun create(parser: XmlReader) = 27 | create(parser, CalendarUserAddressSet()) 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CalendarProxyWriteFor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.XmlUtils 10 | import nl.adaptivity.xmlutil.QName 11 | import nl.adaptivity.xmlutil.XmlReader 12 | import kotlin.jvm.JvmField 13 | 14 | class CalendarProxyWriteFor : HrefListProperty() { 15 | 16 | companion object { 17 | @JvmField 18 | val NAME = QName(XmlUtils.NS_CALENDARSERVER, "calendar-proxy-write-for") 19 | } 20 | 21 | 22 | object Factory : HrefListProperty.Factory() { 23 | 24 | override fun getName() = NAME 25 | 26 | override fun create(parser: XmlReader) = 27 | create(parser, CalendarProxyWriteFor()) 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/exception/HttpException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | 12 | /** 13 | * Signals that a HTTP error was sent by the server. 14 | */ 15 | open class HttpException internal constructor(statusCode: HttpStatusCode, exceptionData: ExceptionData) : DavException( 16 | "HTTP ${statusCode}", 17 | exceptionData = exceptionData 18 | ) { 19 | 20 | companion object { 21 | suspend operator fun invoke(response: HttpResponse) = 22 | HttpException(response.status, createExceptionData(response)) 23 | } 24 | 25 | var code: Int 26 | 27 | init { 28 | code = statusCode.value 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/GetCTag.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.QName 13 | import nl.adaptivity.xmlutil.XmlReader 14 | import kotlin.jvm.JvmField 15 | 16 | data class GetCTag( 17 | val cTag: String? 18 | ) : Property { 19 | 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_CALENDARSERVER, "getctag") 23 | } 24 | 25 | 26 | object Factory : PropertyFactory { 27 | 28 | override fun getName() = NAME 29 | 30 | override fun create(parser: XmlReader) = 31 | GetCTag(XmlUtils.readText(parser)) 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/SyncToken.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.QName 13 | import nl.adaptivity.xmlutil.XmlReader 14 | import kotlin.jvm.JvmField 15 | 16 | data class SyncToken( 17 | val token: String? 18 | ): Property { 19 | 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_WEBDAV, "sync-token") 23 | } 24 | 25 | 26 | object Factory: PropertyFactory { 27 | 28 | override fun getName() = NAME 29 | 30 | override fun create(parser: XmlReader) = 31 | // 32 | SyncToken(XmlUtils.readText(parser)) 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/DisplayName.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.QName 13 | import nl.adaptivity.xmlutil.XmlReader 14 | import kotlin.jvm.JvmField 15 | 16 | data class DisplayName( 17 | val displayName: String? 18 | ) : Property { 19 | 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_WEBDAV, "displayname") 23 | } 24 | 25 | 26 | object Factory : PropertyFactory { 27 | 28 | override fun getName() = NAME 29 | 30 | override fun create(parser: XmlReader) = 31 | // 32 | DisplayName(XmlUtils.readText(parser)) 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/build-kdoc.yml: -------------------------------------------------------------------------------- 1 | name: Build KDoc 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 7 | permissions: 8 | contents: read 9 | pages: write 10 | id-token: write 11 | 12 | jobs: 13 | build: 14 | name: Build and publish KDoc 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-java@v3 19 | with: 20 | distribution: 'temurin' 21 | java-version: 17 22 | - uses: gradle/gradle-build-action@v2 23 | 24 | - name: Build KDoc 25 | run: ./gradlew --no-daemon dokkaHtml 26 | 27 | - uses: actions/upload-pages-artifact@v1 28 | with: 29 | path: build/dokka/html 30 | 31 | deploy: 32 | environment: 33 | name: github-pages 34 | url: ${{ steps.deployment.outputs.page_url }} 35 | runs-on: ubuntu-latest 36 | needs: build 37 | steps: 38 | - name: Deploy to GitHub Pages 39 | id: deployment 40 | uses: actions/deploy-pages@v2 41 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/HrefListProperty.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.DavResource 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import nl.adaptivity.xmlutil.XmlReader 14 | 15 | abstract class HrefListProperty : Property { 16 | 17 | val hrefs = mutableListOf() 18 | 19 | val href 20 | get() = hrefs.firstOrNull() 21 | 22 | override fun toString() = "href=[" + hrefs.joinToString(", ") + "]" 23 | 24 | 25 | abstract class Factory : PropertyFactory { 26 | 27 | fun create(parser: XmlReader, list: HrefListProperty): HrefListProperty { 28 | XmlUtils.readTextPropertyList(parser, DavResource.HREF, list.hrefs) 29 | return list 30 | } 31 | 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CalendarTimezone.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.QName 13 | import nl.adaptivity.xmlutil.XmlReader 14 | import kotlin.jvm.JvmField 15 | 16 | data class CalendarTimezone( 17 | val vTimeZone: String? 18 | ) : Property { 19 | 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_CALDAV, "calendar-timezone") 23 | } 24 | 25 | 26 | object Factory : PropertyFactory { 27 | 28 | override fun getName() = NAME 29 | 30 | override fun create(parser: XmlReader) = 31 | // 32 | CalendarTimezone(XmlUtils.readText(parser)) 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CreationDate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.QName 13 | import nl.adaptivity.xmlutil.XmlReader 14 | import kotlin.jvm.JvmField 15 | 16 | data class CreationDate( 17 | var creationDate: String 18 | ) : Property { 19 | companion object { 20 | @JvmField 21 | val NAME = QName(XmlUtils.NS_WEBDAV, "creationdate") 22 | } 23 | 24 | object Factory : PropertyFactory { 25 | override fun getName() = NAME 26 | 27 | override fun create(parser: XmlReader): CreationDate? { 28 | XmlUtils.readText(parser)?.let { it -> 29 | return CreationDate(it) 30 | } 31 | return null 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CalendarDescription.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.QName 13 | import nl.adaptivity.xmlutil.XmlReader 14 | import kotlin.jvm.JvmField 15 | 16 | data class CalendarDescription( 17 | val description: String? 18 | ) : Property { 19 | 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_CALDAV, "calendar-description") 23 | } 24 | 25 | 26 | object Factory : PropertyFactory { 27 | 28 | override fun getName() = NAME 29 | 30 | override fun create(parser: XmlReader) = 31 | // 32 | CalendarDescription(XmlUtils.readText(parser)) 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/property/OwnerTest.kt: -------------------------------------------------------------------------------- 1 | package at.bitfire.dav4jvm.property 2 | 3 | import kotlin.test.assertEquals 4 | import kotlin.test.assertNull 5 | 6 | object OwnerTest : PropertyTest() { 7 | 8 | init { 9 | test("testOwner_PlainText") { 10 | val results = parseProperty("https://example.com") 11 | val owner = results.first() as Owner 12 | assertNull(owner.href) 13 | } 14 | 15 | test("testOwner_PlainTextAndHref") { 16 | val results = 17 | parseProperty("Principal Name. mailto:owner@example.com (test)") 18 | val owner = results.first() as Owner 19 | assertEquals("mailto:owner@example.com", owner.href) 20 | } 21 | 22 | test("testOwner_Href") { 23 | val results = parseProperty("https://example.com") 24 | val owner = results.first() as Owner 25 | assertEquals("https://example.com", owner.href) 26 | } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/AddMember.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.DavResource 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | /** 18 | * Defined in RFC 5995 3.2.1 DAV:add-member Property (Protected). 19 | */ 20 | data class AddMember( 21 | val href: String? 22 | ): Property { 23 | companion object { 24 | @JvmField 25 | val NAME = QName(XmlUtils.NS_WEBDAV, "add-member") 26 | } 27 | 28 | object Factory: PropertyFactory { 29 | override fun getName() = NAME 30 | 31 | override fun create(parser: XmlReader) = 32 | AddMember(XmlUtils.readTextProperty(parser, DavResource.HREF)) 33 | } 34 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/AddressbookDescription.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.QName 13 | import nl.adaptivity.xmlutil.XmlReader 14 | import kotlin.jvm.JvmField 15 | 16 | data class AddressbookDescription( 17 | var description: String? = null 18 | ): Property { 19 | 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_CARDDAV, "addressbook-description") 23 | } 24 | 25 | object Factory: PropertyFactory { 26 | 27 | override fun getName() = NAME 28 | 29 | override fun create(parser: XmlReader) = 30 | // 31 | AddressbookDescription(XmlUtils.readText(parser)) 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/GetContentType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import io.ktor.http.* 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | data class GetContentType( 18 | val type: ContentType? 19 | ): Property { 20 | 21 | companion object { 22 | @JvmField 23 | val NAME = QName(XmlUtils.NS_WEBDAV, "getcontenttype") 24 | } 25 | 26 | 27 | object Factory: PropertyFactory { 28 | 29 | override fun getName() = NAME 30 | 31 | override fun create(parser: XmlReader) = 32 | // 33 | GetContentType(XmlUtils.readText(parser)?.run(ContentType::parse)) 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/AddressData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.QName 13 | import nl.adaptivity.xmlutil.XmlReader 14 | import kotlin.jvm.JvmField 15 | 16 | data class AddressData( 17 | val card: String? 18 | ) : Property { 19 | 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_CARDDAV, "address-data") 23 | 24 | // attributes 25 | const val CONTENT_TYPE = "content-type" 26 | const val VERSION = "version" 27 | } 28 | 29 | 30 | object Factory : PropertyFactory { 31 | 32 | override fun getName() = NAME 33 | 34 | override fun create(parser: XmlReader) = 35 | // 36 | AddressData(XmlUtils.readText(parser)) 37 | 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CalendarData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.QName 13 | import nl.adaptivity.xmlutil.XmlReader 14 | import kotlin.jvm.JvmField 15 | 16 | data class CalendarData( 17 | val iCalendar: String? 18 | ) : Property { 19 | 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_CALDAV, "calendar-data") 23 | 24 | // attributes 25 | const val CONTENT_TYPE = "content-type" 26 | const val VERSION = "version" 27 | } 28 | 29 | 30 | object Factory : PropertyFactory { 31 | 32 | override fun getName() = NAME 33 | 34 | override fun create(parser: XmlReader) = 35 | // 36 | CalendarData(XmlUtils.readText(parser)) 37 | 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/QuotedStringUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | object QuotedStringUtils { 10 | 11 | fun asQuotedString(raw: String) = 12 | "\"" + raw.replace("\\" ,"\\\\").replace("\"", "\\\"") + "\"" 13 | 14 | fun decodeQuotedString(quoted: String): String { 15 | /* quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) 16 | qdtext = > 17 | quoted-pair = "\" CHAR 18 | */ 19 | 20 | val len = quoted.length 21 | if (len >= 2 && quoted[0] == '"' && quoted[len-1] == '"') { 22 | val result = StringBuilder(len) 23 | var pos = 1 24 | while (pos < len-1) { 25 | var c = quoted[pos] 26 | if (c == '\\' && pos != len-2) 27 | c = quoted[++pos] 28 | result.append(c) 29 | pos++ 30 | } 31 | return result.toString() 32 | } else 33 | return quoted 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/HttpUtilsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import io.kotest.core.spec.style.FunSpec 10 | import io.ktor.http.* 11 | import kotlin.test.assertEquals 12 | 13 | 14 | object HttpUtilsTest: FunSpec({ 15 | 16 | fun String.toHttpUrl() = Url(this) 17 | 18 | test("fileName") { 19 | assertEquals("", HttpUtils.fileName("https://example.com".toHttpUrl())) 20 | assertEquals("", HttpUtils.fileName("https://example.com/".toHttpUrl())) 21 | assertEquals("file1", HttpUtils.fileName("https://example.com/file1".toHttpUrl())) 22 | assertEquals("dir1", HttpUtils.fileName("https://example.com/dir1/".toHttpUrl())) 23 | assertEquals("file2", HttpUtils.fileName("https://example.com/dir1/file2".toHttpUrl())) 24 | assertEquals("dir2", HttpUtils.fileName("https://example.com/dir1/dir2/".toHttpUrl())) 25 | } 26 | 27 | test("parseDate") { 28 | assertEquals(1683825995000, HttpUtils.parseDate("Thu, 11 May 2023 17:26:35 GMT")?.local?.unixMillisLong) 29 | } 30 | 31 | }) 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/GetLastModified.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.* 10 | import nl.adaptivity.xmlutil.QName 11 | import nl.adaptivity.xmlutil.XmlReader 12 | import kotlin.jvm.JvmField 13 | 14 | data class GetLastModified( 15 | var lastModified: Long 16 | ): Property { 17 | 18 | companion object { 19 | @JvmField 20 | val NAME = QName(XmlUtils.NS_WEBDAV, "getlastmodified") 21 | } 22 | 23 | 24 | object Factory: PropertyFactory { 25 | 26 | override fun getName() = NAME 27 | 28 | override fun create(parser: XmlReader): GetLastModified? { 29 | // 30 | XmlUtils.readText(parser)?.let { rawDate -> 31 | val date = HttpUtils.parseDate(rawDate) 32 | if (date != null) 33 | return GetLastModified(date.local.unixMillisLong) 34 | else 35 | Dav4jvm.log.warn("Couldn't parse Last-Modified date") 36 | } 37 | return null 38 | } 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/MaxVCardSize.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | data class MaxVCardSize( 18 | val maxSize: Long 19 | ) : Property { 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_CARDDAV, "max-resource-size") 23 | } 24 | 25 | object Factory : PropertyFactory { 26 | override fun getName() = NAME 27 | 28 | override fun create(parser: XmlReader): MaxVCardSize? { 29 | XmlUtils.readText(parser)?.let { valueStr -> 30 | try { 31 | return MaxVCardSize(valueStr.toLong()) 32 | } catch (e: NumberFormatException) { 33 | Dav4jvm.log.warn("Couldn't parse $NAME: $valueStr", e) 34 | } 35 | } 36 | return null 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/QuotaUsedBytes.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | data class QuotaUsedBytes( 18 | val quotaUsedBytes: Long 19 | ) : Property { 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_WEBDAV, "quota-used-bytes") 23 | } 24 | 25 | object Factory : PropertyFactory { 26 | override fun getName() = NAME 27 | 28 | override fun create(parser: XmlReader): QuotaUsedBytes? { 29 | XmlUtils.readText(parser)?.let { valueStr -> 30 | try { 31 | return QuotaUsedBytes(valueStr.toLong()) 32 | } catch (e: NumberFormatException) { 33 | Dav4jvm.log.warn("Couldn't parse $NAME: $valueStr", e) 34 | } 35 | } 36 | return null 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/MaxICalendarSize.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | data class MaxICalendarSize( 18 | val maxSize: Long 19 | ) : Property { 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_CALDAV, "max-resource-size") 23 | } 24 | 25 | object Factory : PropertyFactory { 26 | override fun getName() = NAME 27 | 28 | override fun create(parser: XmlReader): MaxICalendarSize? { 29 | XmlUtils.readText(parser)?.let { valueStr -> 30 | try { 31 | return MaxICalendarSize(valueStr.toLong()) 32 | } catch (e: NumberFormatException) { 33 | Dav4jvm.log.warn("Couldn't parse $NAME: $valueStr", e) 34 | } 35 | } 36 | return null 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/GetContentLength.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | data class GetContentLength( 18 | val contentLength: Long 19 | ) : Property { 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_WEBDAV, "getcontentlength") 23 | } 24 | 25 | object Factory : PropertyFactory { 26 | override fun getName() = NAME 27 | 28 | override fun create(parser: XmlReader): GetContentLength? { 29 | XmlUtils.readText(parser)?.let { valueStr -> 30 | try { 31 | return GetContentLength(valueStr.toLong()) 32 | } catch (e: NumberFormatException) { 33 | Dav4jvm.log.warn("Couldn't parse $NAME: $valueStr", e) 34 | } 35 | } 36 | return null 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/QuotaAvailableBytes.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | data class QuotaAvailableBytes( 18 | val quotaAvailableBytes: Long 19 | ) : Property { 20 | companion object { 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_WEBDAV, "quota-available-bytes") 23 | } 24 | 25 | object Factory : PropertyFactory { 26 | override fun getName() = NAME 27 | 28 | override fun create(parser: XmlReader): QuotaAvailableBytes? { 29 | XmlUtils.readText(parser)?.let { valueStr -> 30 | try { 31 | return QuotaAvailableBytes(valueStr.toLong()) 32 | } catch (e: NumberFormatException) { 33 | Dav4jvm.log.warn("Couldn't parse $NAME: $valueStr", e) 34 | } 35 | } 36 | return null 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CurrentUserPrincipal.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.DavResource 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | // see RFC 5397: WebDAV Current Principal Extension 18 | 19 | data class CurrentUserPrincipal( 20 | val href: String? 21 | ) : Property { 22 | 23 | companion object { 24 | @JvmField 25 | val NAME = QName(XmlUtils.NS_WEBDAV, "current-user-principal") 26 | } 27 | 28 | 29 | object Factory : PropertyFactory { 30 | 31 | override fun getName() = NAME 32 | 33 | override fun create(parser: XmlReader): CurrentUserPrincipal { 34 | // 35 | var href: String? = null 36 | XmlUtils.processTag(parser, DavResource.HREF) { 37 | href = XmlUtils.readText(parser) 38 | } 39 | return CurrentUserPrincipal(href) 40 | } 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/KtorAuthExtensions.kt: -------------------------------------------------------------------------------- 1 | package at.bitfire.dav4jvm.property 2 | 3 | import io.ktor.client.plugins.auth.* 4 | import io.ktor.client.request.* 5 | import io.ktor.http.auth.* 6 | 7 | fun Auth.forDomain(pattern: String, block: Auth.() -> Unit) = forDomain(pattern.toRegex(), block) 8 | 9 | fun Auth.forDomain(pattern: Regex, block: Auth.() -> Unit) { 10 | val old = this.providers.toSet() 11 | block() 12 | val newProviders = providers - old 13 | providers.removeAll(newProviders) 14 | newProviders.forEach { providers += AuthDomainLimiter(pattern, it) } 15 | } 16 | 17 | class AuthDomainLimiter(private val domain: Regex, private val downstreamProvider: AuthProvider) : AuthProvider { 18 | @Deprecated("Please use sendWithoutRequest function instead") 19 | override val sendWithoutRequest: Boolean 20 | get() = error("Deprecated") 21 | 22 | override fun sendWithoutRequest(request: HttpRequestBuilder): Boolean = 23 | domain.matches(request.url.buildString()) && downstreamProvider.sendWithoutRequest(request) 24 | 25 | override suspend fun addRequestHeaders(request: HttpRequestBuilder, authHeader: HttpAuthHeader?) { 26 | if (domain.matches(request.url.buildString())) downstreamProvider.addRequestHeaders(request, authHeader) 27 | } 28 | 29 | override fun isApplicable(auth: HttpAuthHeader): Boolean = downstreamProvider.isApplicable(auth) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/ScheduleTag.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.QuotedStringUtils 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import io.ktor.client.statement.* 14 | import nl.adaptivity.xmlutil.QName 15 | import nl.adaptivity.xmlutil.XmlReader 16 | import kotlin.jvm.JvmField 17 | 18 | class ScheduleTag( 19 | rawScheduleTag: String? 20 | ) : Property { 21 | 22 | companion object { 23 | @JvmField 24 | val NAME = QName(XmlUtils.NS_CALDAV, "schedule-tag") 25 | 26 | fun fromResponse(response: HttpResponse) = 27 | response.headers["Schedule-Tag"]?.let { ScheduleTag(it) } 28 | } 29 | 30 | /* Value: opaque-tag 31 | opaque-tag = quoted-string 32 | */ 33 | val scheduleTag: String? = rawScheduleTag?.let { QuotedStringUtils.decodeQuotedString(it) } 34 | 35 | override fun toString() = scheduleTag ?: "(null)" 36 | 37 | 38 | object Factory : PropertyFactory { 39 | 40 | override fun getName() = NAME 41 | 42 | override fun create(parser: XmlReader) = 43 | ScheduleTag(XmlUtils.readText(parser)) 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/Property.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm.log 10 | import at.bitfire.dav4jvm.exception.InvalidPropertyException 11 | import nl.adaptivity.xmlutil.XmlReader 12 | 13 | /** 14 | * Represents a WebDAV property. 15 | * 16 | * Every [Property] must define a static field (use `@JvmStatic`) called `NAME` of type [QName], 17 | * which will be accessed by reflection. 18 | */ 19 | interface Property { 20 | 21 | companion object { 22 | 23 | fun parse(parser: XmlReader): List { 24 | // 25 | val properties = mutableListOf() 26 | XmlUtils.processTag(parser) { 27 | val name = parser.name 28 | 29 | try { 30 | val property = PropertyRegistry.create(name, parser) 31 | 32 | if (property != null) { 33 | properties.add(property) 34 | } else 35 | log.trace("Ignoring unknown property $name") 36 | } catch (e: InvalidPropertyException) { 37 | log.warn("Ignoring invalid property", e) 38 | } 39 | } 40 | 41 | return properties 42 | } 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/CallbackInterfaces.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import io.ktor.client.statement.* 10 | 11 | /** 12 | * Callback for the OPTIONS request. 13 | */ 14 | fun interface CapabilitiesCallback { 15 | fun onCapabilities(davCapabilities: Set, response: HttpResponse) 16 | } 17 | 18 | /** 19 | * Callback for 207 Multi-Status responses. 20 | */ 21 | fun interface MultiResponseCallback { 22 | /** 23 | * Called for every `` element in the `` body. For instance, 24 | * in response to a `PROPFIND` request, this callback will be called once for every found 25 | * member resource. 26 | * 27 | * @param response the parsed response (including URL) 28 | * @param relation relation of the response to the called resource 29 | */ 30 | fun onResponse(response: Response, relation: Response.HrefRelation) 31 | } 32 | 33 | /** 34 | * Callback for HTTP responses. 35 | */ 36 | fun interface ResponseCallback { 37 | /** 38 | * Called for a HTTP response. Typically this is only called for successful/redirect 39 | * responses because HTTP errors throw an exception before this callback is called. 40 | */ 41 | @Suppress("FUN_INTERFACE_WITH_SUSPEND_FUNCTION") 42 | suspend fun onResponse(response: HttpResponse) 43 | } 44 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/Error.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Ricki Hirner (bitfire web engineering). 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the GNU Public License v3.0 5 | * which accompanies this distribution, and is available at 6 | * http://www.gnu.org/licenses/gpl.html 7 | */ 8 | 9 | package at.bitfire.dav4jvm 10 | 11 | import nl.adaptivity.xmlutil.QName 12 | import nl.adaptivity.xmlutil.XmlReader 13 | 14 | 15 | /** 16 | * Represents an XML precondition/postcondition error. Every error has a name, which is the XML element 17 | * name. Subclassed errors may have more specific information available. 18 | * 19 | * At the moment, there is no logic for subclassing errors. 20 | */ 21 | class Error( 22 | val name: QName 23 | ) { 24 | 25 | companion object { 26 | 27 | val NAME = QName(XmlUtils.NS_WEBDAV, "error") 28 | 29 | fun parseError(parser: XmlReader): List { 30 | val names = mutableSetOf() 31 | 32 | XmlUtils.processTag(parser) { names += parser.name } 33 | 34 | return names.map { Error(it) } 35 | } 36 | 37 | 38 | // some pre-defined errors 39 | 40 | val NEED_PRIVILEGES = Error(QName(XmlUtils.NS_WEBDAV, "need-privileges")) 41 | val VALID_SYNC_TOKEN = Error(QName(XmlUtils.NS_WEBDAV, "valid-sync-token")) 42 | 43 | } 44 | 45 | override fun equals(other: Any?) = 46 | (other is Error) && other.name == name 47 | 48 | override fun hashCode() = name.hashCode() 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/QuotedStringUtilsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import io.kotest.core.spec.style.FunSpec 10 | import kotlin.test.assertEquals 11 | 12 | object QuotedStringUtilsTest : FunSpec({ 13 | 14 | test("testAsQuotedString") { 15 | assertEquals("\"\"", QuotedStringUtils.asQuotedString("")) 16 | assertEquals("\"\\\"\"", QuotedStringUtils.asQuotedString("\"")) 17 | assertEquals("\"\\\\\"", QuotedStringUtils.asQuotedString("\\")) 18 | } 19 | 20 | fun testDecodeQuotedString() { 21 | assertEquals("\"", QuotedStringUtils.decodeQuotedString("\"")) 22 | assertEquals("\\", QuotedStringUtils.decodeQuotedString("\"\\\"")) 23 | assertEquals("\"test", QuotedStringUtils.decodeQuotedString("\"test")) 24 | assertEquals("test", QuotedStringUtils.decodeQuotedString("test")) 25 | assertEquals("", QuotedStringUtils.decodeQuotedString("\"\"")) 26 | assertEquals("test", QuotedStringUtils.decodeQuotedString("\"test\"")) 27 | assertEquals("test\\", QuotedStringUtils.decodeQuotedString("\"test\\\"")) 28 | assertEquals("test", QuotedStringUtils.decodeQuotedString("\"t\\e\\st\"")) 29 | assertEquals("12\"34", QuotedStringUtils.decodeQuotedString("\"12\\\"34\"")) 30 | assertEquals("1234\"", QuotedStringUtils.decodeQuotedString("\"1234\\\"\"")) 31 | } 32 | 33 | }) 34 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/exception/HttpExceptionTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import at.bitfire.dav4jvm.buildRequest 10 | import at.bitfire.dav4jvm.createMockClient 11 | import at.bitfire.dav4jvm.createResponse 12 | import io.kotest.core.spec.style.FunSpec 13 | import io.kotest.matchers.string.shouldContain 14 | import io.ktor.client.request.* 15 | import io.ktor.http.* 16 | import io.ktor.http.content.* 17 | import io.ktor.utils.io.core.* 18 | 19 | object HttpExceptionTest : FunSpec({ 20 | 21 | val responseMessage = "Unknown error" 22 | 23 | test("testHttpFormatting") { 24 | val request = buildRequest { 25 | method = HttpMethod.Post 26 | url("http://example.com") 27 | header(HttpHeaders.ContentType, "text/something") 28 | setBody(ByteArrayContent("REQUEST\nBODY".toByteArray())) 29 | } 30 | 31 | val response = createMockClient().createResponse( 32 | request, 33 | HttpStatusCode.InternalServerError.description(responseMessage), 34 | headersOf(HttpHeaders.ContentType, "text/something-other"), 35 | "SERVER\r\nRESPONSE" 36 | ) 37 | val e = HttpException(response) 38 | e.message.shouldContain("500") 39 | e.message.shouldContain(responseMessage) 40 | e.requestBody.shouldContain("REQUEST\nBODY") 41 | e.responseBody.shouldContain("SERVER\r\nRESPONSE") 42 | } 43 | 44 | }) 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Android ### 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | 32 | ### Intellij ### 33 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 34 | 35 | *.iml 36 | 37 | ## Directory-based project format: 38 | .idea/ 39 | # if you remove the above rule, at least ignore the following: 40 | 41 | # User-specific stuff: 42 | # .idea/workspace.xml 43 | # .idea/tasks.xml 44 | # .idea/dictionaries 45 | 46 | # Sensitive or high-churn files: 47 | # .idea/dataSources.ids 48 | # .idea/dataSources.xml 49 | # .idea/sqlDataSources.xml 50 | # .idea/dynamic.xml 51 | # .idea/uiDesigner.xml 52 | 53 | # Gradle: 54 | # .idea/gradle.xml 55 | # .idea/libraries 56 | 57 | # Mongo Explorer plugin: 58 | # .idea/mongoSettings.xml 59 | 60 | ## File-based project format: 61 | *.ipr 62 | *.iws 63 | 64 | ## Plugin-specific files: 65 | 66 | # IntelliJ 67 | out/ 68 | 69 | # mpeltonen/sbt-idea plugin 70 | .idea_modules/ 71 | 72 | # JIRA plugin 73 | atlassian-ide-plugin.xml 74 | 75 | # Crashlytics plugin (for Android Studio and IntelliJ) 76 | com_crashlytics_export_strings.xml 77 | crashlytics.properties 78 | crashlytics-build.properties 79 | 80 | 81 | ### Gradle ### 82 | .gradle 83 | build/ 84 | 85 | # Ignore Gradle GUI config 86 | gradle-app.setting 87 | 88 | 89 | ### external libs ### 90 | .svn 91 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '23 22 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'java' ] 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v2 44 | with: 45 | languages: ${{ matrix.language }} 46 | 47 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 48 | # If this step fails, then you should remove it and run the build manually (see below) 49 | - name: Autobuild 50 | uses: github/codeql-action/autobuild@v2 51 | 52 | - name: Perform CodeQL Analysis 53 | uses: github/codeql-action/analyze@v2 54 | with: 55 | category: "/language:${{matrix.language}}" 56 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/exception/ServiceUnavailableException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm 10 | import at.bitfire.dav4jvm.HttpUtils 11 | import io.ktor.client.statement.* 12 | import korlibs.time.DateTime 13 | import korlibs.time.DateTimeTz 14 | import korlibs.time.seconds 15 | 16 | class ServiceUnavailableException// not a HTTP-date, must be delta-seconds// Retry-After = "Retry-After" ":" ( HTTP-date | delta-seconds ) 17 | // HTTP-date = rfc1123-date | rfc850-date | asctime-date 18 | internal constructor(response: HttpResponse, exceptionData: ExceptionData) : 19 | HttpException(response.status, exceptionData) { 20 | 21 | companion object { 22 | suspend operator fun invoke(httpResponse: HttpResponse) = 23 | ServiceUnavailableException(httpResponse, createExceptionData(httpResponse)) 24 | } 25 | 26 | var retryAfter: DateTimeTz? = null 27 | 28 | 29 | init { 30 | response.headers["Retry-After"]?.let { after -> 31 | retryAfter = HttpUtils.parseDate(after) ?: 32 | // not a HTTP-date, must be delta-seconds 33 | try { 34 | val seconds = after.toInt() 35 | 36 | DateTime.now().local + seconds.seconds 37 | 38 | } catch (ignored: NumberFormatException) { 39 | Dav4jvm.log.warn("Received Retry-After which was not a HTTP-date nor delta-seconds: $after") 40 | null 41 | } 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/property/GetETagTest.kt: -------------------------------------------------------------------------------- 1 | package at.bitfire.dav4jvm.property 2 | 3 | import kotlin.test.assertEquals 4 | import kotlin.test.assertFalse 5 | import kotlin.test.assertTrue 6 | 7 | object GetETagTest : PropertyTest() { 8 | 9 | init { 10 | test("testGetETag_Strong") { 11 | val results = parseProperty("\"Correct strong ETag\"") 12 | val getETag = results.first() as GetETag 13 | assertEquals("Correct strong ETag", getETag.eTag) 14 | assertFalse(getETag.weak) 15 | } 16 | 17 | test("testGetETag_Strong_NoQuotes") { 18 | val results = parseProperty("Strong ETag without quotes") 19 | val getETag = results.first() as GetETag 20 | assertEquals("Strong ETag without quotes", getETag.eTag) 21 | assertFalse(getETag.weak) 22 | } 23 | 24 | test("testGetETag_Weak") { 25 | val results = parseProperty("W/\"Correct weak ETag\"") 26 | val getETag = results.first() as GetETag 27 | assertEquals("Correct weak ETag", getETag.eTag) 28 | assertTrue(getETag.weak) 29 | } 30 | 31 | test("testGetETag_Weak_Empty") { 32 | val results = parseProperty("W/") 33 | val getETag = results.first() as GetETag 34 | assertEquals("", getETag.eTag) 35 | assertTrue(getETag.weak) 36 | } 37 | 38 | test("testGetETag_Weak_NoQuotes") { 39 | val results = parseProperty("W/Weak ETag without quotes") 40 | val getETag = results.first() as GetETag 41 | assertEquals("Weak ETag without quotes", getETag.eTag) 42 | assertTrue(getETag.weak) 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/PropertyTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import at.bitfire.dav4jvm.property.GetETag 10 | import io.kotest.core.spec.style.FunSpec 11 | import nl.adaptivity.xmlutil.EventType 12 | import nl.adaptivity.xmlutil.localPart 13 | import kotlin.test.assertEquals 14 | import kotlin.test.assertTrue 15 | 16 | object PropertyTest : FunSpec({ 17 | 18 | test("testParse_InvalidProperty") { 19 | val parser = XmlUtils.createReader("") 20 | parser.next() 21 | 22 | // we're now at the start of 23 | assertEquals(EventType.START_ELEMENT, parser.eventType) 24 | assertEquals("multistatus", parser.name.localPart) 25 | 26 | // parse invalid DAV:getetag 27 | assertTrue(Property.parse(parser).isEmpty()) 28 | 29 | // we're now at the end of 30 | assertEquals(EventType.END_ELEMENT, parser.eventType) 31 | assertEquals("multistatus", parser.name.localPart) 32 | } 33 | 34 | test("testParse_ValidProperty") { 35 | val parser = XmlUtils.createReader("12345") 36 | parser.next() 37 | 38 | // we're now at the start of 39 | assertEquals(EventType.START_ELEMENT, parser.eventType) 40 | assertEquals("multistatus", parser.name.localPart) 41 | 42 | val etag = Property.parse(parser).first() 43 | assertEquals(GetETag("12345"), etag) 44 | 45 | // we're now at the end of 46 | assertEquals(EventType.END_ELEMENT, parser.eventType) 47 | assertEquals("multistatus", parser.name.localPart) 48 | } 49 | 50 | }) -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/ResourceType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.QName 13 | import nl.adaptivity.xmlutil.XmlReader 14 | import kotlin.jvm.JvmField 15 | 16 | class ResourceType : Property { 17 | 18 | companion object { 19 | @JvmField 20 | val NAME = QName(XmlUtils.NS_WEBDAV, "resourcetype") 21 | 22 | val COLLECTION = QName(XmlUtils.NS_WEBDAV, "collection") // WebDAV 23 | val PRINCIPAL = QName(XmlUtils.NS_WEBDAV, "principal") // WebDAV ACL 24 | val ADDRESSBOOK = QName(XmlUtils.NS_CARDDAV, "addressbook") // CardDAV 25 | val CALENDAR = QName(XmlUtils.NS_CALDAV, "calendar") // CalDAV 26 | val SUBSCRIBED = QName(XmlUtils.NS_CALENDARSERVER, "subscribed") 27 | } 28 | 29 | val types = mutableSetOf() 30 | 31 | override fun toString() = "[${types.joinToString(", ")}]" 32 | 33 | 34 | object Factory : PropertyFactory { 35 | 36 | override fun getName() = NAME 37 | 38 | override fun create(parser: XmlReader): ResourceType { 39 | val type = ResourceType() 40 | 41 | XmlUtils.processTag(parser) { 42 | // use static objects to allow types.contains() 43 | var typeName = parser.name 44 | when (typeName) { 45 | COLLECTION -> typeName = COLLECTION 46 | PRINCIPAL -> typeName = PRINCIPAL 47 | ADDRESSBOOK -> typeName = ADDRESSBOOK 48 | CALENDAR -> typeName = CALENDAR 49 | SUBSCRIBED -> typeName = SUBSCRIBED 50 | } 51 | type.types.add(typeName) 52 | } 53 | 54 | return type 55 | } 56 | 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package at.bitfire.dav4jvm 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.call.* 5 | import io.ktor.client.engine.mock.* 6 | import io.ktor.client.plugins.* 7 | import io.ktor.client.request.* 8 | import io.ktor.http.* 9 | import io.ktor.util.* 10 | import io.ktor.util.date.* 11 | import io.ktor.utils.io.* 12 | import korlibs.time.DateTime 13 | import kotlinx.coroutines.CoroutineName 14 | import kotlinx.coroutines.Job 15 | 16 | val HttpClient.lastMockRequest 17 | get() = (engine as MockEngine).requestHistory.last() 18 | 19 | val HttpClient.lastMockResponse 20 | get() = (engine as MockEngine).responseHistory.last() 21 | 22 | fun createMockClient( 23 | handler: MockRequestHandler = { 24 | respondError(HttpStatusCode.InternalServerError) 25 | } 26 | ) = HttpClient(MockEngine) { 27 | engine { 28 | addHandler(handler) 29 | } 30 | install(HttpRedirect) { 31 | checkHttpMethod = false 32 | } 33 | } 34 | 35 | fun HttpClient.changeMockHandler(handler: MockRequestHandler) { 36 | if (engine !is MockEngine) error("Only possible with MockEngine") 37 | val config = (engine as MockEngine).config 38 | config.requestHandlers.clear() 39 | config.addHandler(handler) 40 | } 41 | 42 | fun Url.resolve(path: String) = URLBuilder(this).apply { 43 | takeFrom(path) 44 | }.build() 45 | 46 | @OptIn(InternalAPI::class) 47 | suspend fun HttpClient.createResponse( 48 | request: HttpRequestBuilder, 49 | status: HttpStatusCode, 50 | headers: Headers = headersOf(), 51 | body: String? = null 52 | ) = HttpClientCall( 53 | this, 54 | request.build(), 55 | HttpResponseData( 56 | statusCode = status, 57 | requestTime = GMTDate(DateTime.nowUnixMillisLong()), 58 | headers = headers, 59 | version = HttpProtocolVersion.HTTP_1_1, 60 | body = body?.let { ByteReadChannel(it) } ?: ByteReadChannel.Empty, 61 | callContext = Job() + CoroutineName("Fake call response"), 62 | ) 63 | ).save().response 64 | 65 | fun buildRequest(block: HttpRequestBuilder.() -> Unit) = HttpRequestBuilder().apply(block) -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/SupportedReportSet.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.EventType 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | class SupportedReportSet : Property { 18 | 19 | companion object { 20 | 21 | @JvmField 22 | val NAME = QName(XmlUtils.NS_WEBDAV, "supported-report-set") 23 | 24 | val SUPPORTED_REPORT = QName(XmlUtils.NS_WEBDAV, "supported-report") 25 | val REPORT = QName(XmlUtils.NS_WEBDAV, "report") 26 | 27 | const val SYNC_COLLECTION = "DAV:sync-collection" // collection synchronization (RFC 6578) 28 | 29 | } 30 | 31 | val reports = mutableSetOf() 32 | 33 | override fun toString() = "[${reports.joinToString(", ")}]" 34 | 35 | 36 | object Factory : PropertyFactory { 37 | 38 | override fun getName() = NAME 39 | 40 | override fun create(parser: XmlReader): SupportedReportSet { 41 | /* 42 | 43 | 44 | */ 45 | 46 | val supported = SupportedReportSet() 47 | XmlUtils.processTag(parser, SUPPORTED_REPORT) { 48 | XmlUtils.processTag(parser, REPORT) { 49 | parser.nextTag() 50 | if (parser.eventType == EventType.TEXT) 51 | supported.reports += parser.text 52 | else if (parser.eventType == EventType.START_ELEMENT) 53 | supported.reports += "${parser.namespaceURI}${parser.localName}" 54 | } 55 | } 56 | return supported 57 | } 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/PropStat.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Ricki Hirner (bitfire web engineering). 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the GNU Public License v3.0 5 | * which accompanies this distribution, and is available at 6 | * http://www.gnu.org/licenses/gpl.html 7 | */ 8 | 9 | package at.bitfire.dav4jvm 10 | 11 | import at.bitfire.dav4jvm.Response.Companion.STATUS 12 | import at.bitfire.dav4jvm.XmlUtils.nextText 13 | import io.ktor.http.* 14 | import nl.adaptivity.xmlutil.QName 15 | import nl.adaptivity.xmlutil.XmlReader 16 | import kotlin.jvm.JvmField 17 | 18 | /** 19 | * Represents a WebDAV propstat XML element. 20 | * 21 | * 22 | */ 23 | data class PropStat( 24 | val properties: List, 25 | val status: HttpStatusCode, 26 | val error: List? = null 27 | ) { 28 | 29 | companion object { 30 | 31 | @JvmField 32 | val NAME = QName(XmlUtils.NS_WEBDAV, "propstat") 33 | 34 | private val ASSUMING_OK = HttpStatusCode(200, "Assuming OK") 35 | private val INVALID_STATUS = HttpStatusCode( 500, "Invalid status line") 36 | 37 | fun parse(parser: XmlReader): PropStat { 38 | var status: HttpStatusCode? = null 39 | val prop = mutableListOf() 40 | 41 | XmlUtils.processTag(parser) { 42 | when (parser.name) { 43 | DavResource.PROP -> 44 | prop.addAll(Property.parse(parser)) 45 | 46 | STATUS -> 47 | status = try { 48 | StatusLine.parse(parser.nextText()).status 49 | } catch (e: IllegalStateException) { 50 | // invalid status line, treat as 500 Internal Server Error 51 | INVALID_STATUS 52 | } 53 | } 54 | } 55 | 56 | return PropStat(prop, status ?: ASSUMING_OK) 57 | } 58 | 59 | } 60 | 61 | 62 | fun isSuccess() = status.isSuccess() 63 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CalendarColor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | data class CalendarColor( 18 | val color: Int 19 | ) : Property { 20 | 21 | companion object { 22 | @JvmField 23 | val NAME = QName(XmlUtils.NS_APPLE_ICAL, "calendar-color") 24 | 25 | private val PATTERN = "#?([A-Fa-f0-9]{6})([A-Fa-f0-9]{2})?".toRegex() 26 | 27 | /** 28 | * Converts a WebDAV color from one of these formats: 29 | * #RRGGBB (alpha = 0xFF) 30 | * RRGGBB (alpha = 0xFF) 31 | * #RRGGBBAA 32 | * RRGGBBAA 33 | * to an [Int] with alpha. 34 | */ 35 | @Throws(IllegalArgumentException::class) 36 | fun parseARGBColor(davColor: String): Int { 37 | val m = PATTERN.find(davColor) 38 | if (m != null) { 39 | val color_rgb = m.groupValues[1].toInt(16) 40 | val color_alpha = m.groupValues.elementAtOrNull(2)?.let { it.toInt( 16) and 0xFF } ?: 0xFF 41 | return (color_alpha shl 24) or color_rgb 42 | } else 43 | throw IllegalArgumentException("Couldn't parse color value: $davColor") 44 | } 45 | } 46 | 47 | 48 | object Factory : PropertyFactory { 49 | 50 | override fun getName() = NAME 51 | 52 | override fun create(parser: XmlReader): CalendarColor? { 53 | XmlUtils.readText(parser)?.let { 54 | try { 55 | return CalendarColor(parseARGBColor(it)) 56 | } catch (e: IllegalArgumentException) { 57 | Dav4jvm.log.warn("Couldn't parse color, ignoring", e) 58 | } 59 | } 60 | return null 61 | } 62 | 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/SupportedCalendarData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import io.ktor.http.* 14 | import nl.adaptivity.xmlutil.QName 15 | import nl.adaptivity.xmlutil.XmlException 16 | import nl.adaptivity.xmlutil.XmlReader 17 | import kotlin.jvm.JvmField 18 | 19 | class SupportedCalendarData : Property { 20 | 21 | companion object { 22 | 23 | @JvmField 24 | val NAME = QName(XmlUtils.NS_CALDAV, "supported-calendar-data") 25 | 26 | val CALENDAR_DATA_TYPE = QName(XmlUtils.NS_CALDAV, "calendar-data") 27 | const val CONTENT_TYPE = "content-type" 28 | const val VERSION = "version" 29 | 30 | val jCalType = ContentType("application", "calendar+json") 31 | } 32 | 33 | val types = mutableSetOf() 34 | 35 | fun hasJCal() = types.any { jCalType == it } 36 | 37 | override fun toString() = "[${types.joinToString(", ")}]" 38 | 39 | 40 | object Factory : PropertyFactory { 41 | 42 | override fun getName() = NAME 43 | 44 | override fun create(parser: XmlReader): SupportedCalendarData? { 45 | val supported = SupportedCalendarData() 46 | 47 | try { 48 | XmlUtils.processTag(parser, CALENDAR_DATA_TYPE) { 49 | parser.getAttributeValue(null, CONTENT_TYPE)?.let { contentType -> 50 | var type = contentType.run(ContentType::parse) 51 | type = parser.getAttributeValue(null, SupportedAddressData.VERSION) 52 | ?.let { version -> type.withParameter("version", version) } ?: type 53 | supported.types += type 54 | } 55 | } 56 | } catch (e: XmlException) { 57 | Dav4jvm.log.error("Couldn't parse ", e) 58 | return null 59 | } 60 | 61 | return supported 62 | } 63 | 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/exception/ServiceUnavailableExceptionTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import at.bitfire.dav4jvm.HttpUtils 10 | import at.bitfire.dav4jvm.buildRequest 11 | import at.bitfire.dav4jvm.createMockClient 12 | import at.bitfire.dav4jvm.createResponse 13 | import io.kotest.core.spec.style.FunSpec 14 | import io.kotest.matchers.longs.shouldBeLessThan 15 | import io.ktor.client.request.* 16 | import io.ktor.http.* 17 | import korlibs.time.DateTime 18 | import korlibs.time.DateTimeTz 19 | import korlibs.time.minutes 20 | import kotlin.math.abs 21 | import kotlin.test.assertNotNull 22 | import kotlin.test.assertNull 23 | 24 | object ServiceUnavailableExceptionTest : FunSpec({ 25 | 26 | val httpClient = createMockClient() 27 | 28 | suspend fun buildResponse(headers: Headers = headersOf()) = httpClient.createResponse( 29 | buildRequest { 30 | url("http://www.example.com") 31 | method = HttpMethod.Get 32 | }, 33 | HttpStatusCode.ServiceUnavailable.description("Try later"), 34 | headers 35 | ) 36 | 37 | fun withinTimeRange(d: DateTimeTz, seconds: Int) { 38 | val msCheck = d.utc.unixMillisLong 39 | val msShouldBe = DateTime.nowUnixMillisLong() + seconds * 1000 40 | // assume max. 5 seconds difference for test running 41 | abs(msCheck - msShouldBe).shouldBeLessThan(5000) 42 | } 43 | 44 | test("testRetryAfter_NoTime") { 45 | val e = ServiceUnavailableException(buildResponse()) 46 | assertNull(e.retryAfter) 47 | } 48 | 49 | test("testRetryAfter_Seconds") { 50 | val response = buildResponse(headersOf(HttpHeaders.RetryAfter, "120")) 51 | val e = ServiceUnavailableException(response) 52 | assertNotNull(e.retryAfter) 53 | withinTimeRange(e.retryAfter!!, 120) 54 | } 55 | 56 | test("testRetryAfter_Date") { 57 | val t = (DateTime.now() + 30.minutes) 58 | val response = buildResponse(headersOf(HttpHeaders.RetryAfter, HttpUtils.formatDate(t))) 59 | val e = ServiceUnavailableException(response) 60 | assertNotNull(e.retryAfter) 61 | withinTimeRange(e.retryAfter!!, 30 * 60) 62 | } 63 | 64 | }) -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/SupportedAddressData.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm 10 | import at.bitfire.dav4jvm.Property 11 | import at.bitfire.dav4jvm.PropertyFactory 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import io.ktor.http.* 14 | import nl.adaptivity.xmlutil.QName 15 | import nl.adaptivity.xmlutil.XmlException 16 | import nl.adaptivity.xmlutil.XmlReader 17 | import kotlin.jvm.JvmField 18 | 19 | class SupportedAddressData : Property { 20 | 21 | companion object { 22 | 23 | @JvmField 24 | val NAME = QName(XmlUtils.NS_CARDDAV, "supported-address-data") 25 | 26 | val ADDRESS_DATA_TYPE = QName(XmlUtils.NS_CARDDAV, "address-data-type") 27 | const val CONTENT_TYPE = "content-type" 28 | const val VERSION = "version" 29 | 30 | val jCardContentType = ContentType("application", "vcard+json") 31 | 32 | } 33 | 34 | val types = mutableSetOf() 35 | 36 | fun hasVCard4() = types.any { ContentType.Text.VCard.withParameter("version", "4.0") == it } 37 | fun hasJCard() = types.any { jCardContentType == it } 38 | 39 | override fun toString() = "[${types.joinToString(", ")}]" 40 | 41 | 42 | object Factory : PropertyFactory { 43 | 44 | override fun getName() = NAME 45 | 46 | override fun create(parser: XmlReader): SupportedAddressData? { 47 | val supported = SupportedAddressData() 48 | 49 | try { 50 | XmlUtils.processTag(parser, ADDRESS_DATA_TYPE) { 51 | parser.getAttributeValue(null, CONTENT_TYPE)?.let { contentType -> 52 | var type = contentType.run(ContentType::parse) 53 | type = parser.getAttributeValue(null, VERSION) 54 | ?.let { version -> type.withParameter("version", version) } ?: type 55 | supported.types += type 56 | } 57 | } 58 | } catch (e: XmlException) { 59 | Dav4jvm.log.warn("Couldn't parse ", e) 60 | return null 61 | } 62 | 63 | return supported 64 | } 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/GetETag.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.QuotedStringUtils 12 | import at.bitfire.dav4jvm.XmlUtils 13 | import io.ktor.client.statement.* 14 | import nl.adaptivity.xmlutil.QName 15 | import nl.adaptivity.xmlutil.XmlReader 16 | import kotlin.jvm.JvmField 17 | 18 | /** 19 | * The GetETag property. 20 | * 21 | * Can also be used to parse ETags from HTTP responses – just pass the raw ETag 22 | * header value to the constructor and then use [eTag] and [weak]. 23 | */ 24 | class GetETag( 25 | rawETag: String 26 | ): Property { 27 | 28 | companion object { 29 | @JvmField 30 | val NAME = QName(XmlUtils.NS_WEBDAV, "getetag") 31 | 32 | fun fromResponse(response: HttpResponse) = 33 | response.headers["ETag"]?.let { GetETag(it) } 34 | } 35 | 36 | /** 37 | * The parsed ETag value, excluding the weakness indicator and the quotes. 38 | */ 39 | val eTag: String 40 | 41 | /** 42 | * Whether the ETag is weak. 43 | */ 44 | var weak: Boolean 45 | 46 | init { 47 | /* entity-tag = [ weak ] opaque-tag 48 | weak = "W/" 49 | opaque-tag = quoted-string 50 | */ 51 | val tag: String 52 | 53 | // remove trailing "W/" 54 | if (rawETag.startsWith("W/") && rawETag.length >= 2) { 55 | // entity tag is weak 56 | tag = rawETag.substring(2) 57 | weak = true 58 | } else { 59 | tag = rawETag 60 | weak = false 61 | } 62 | 63 | eTag = QuotedStringUtils.decodeQuotedString(tag) 64 | } 65 | 66 | override fun toString() = "ETag(weak=${weak}, tag=$eTag)" 67 | 68 | override fun equals(other: Any?): Boolean { 69 | if (other !is GetETag) 70 | return false 71 | return eTag == other.eTag && weak == other.weak 72 | } 73 | 74 | override fun hashCode(): Int { 75 | return eTag.hashCode() xor weak.hashCode() 76 | } 77 | 78 | 79 | object Factory: PropertyFactory { 80 | 81 | override fun getName() = NAME 82 | 83 | override fun create(parser: XmlReader) = 84 | GetETag(XmlUtils.requireReadText(parser)) 85 | 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/SupportedCalendarComponentSet.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.EventType 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | data class SupportedCalendarComponentSet( 18 | var supportsEvents: Boolean, 19 | var supportsTasks: Boolean, 20 | var supportsJournal: Boolean 21 | ) : Property { 22 | 23 | companion object { 24 | 25 | @JvmField 26 | val NAME = QName(XmlUtils.NS_CALDAV, "supported-calendar-component-set") 27 | 28 | val ALLCOMP = QName(XmlUtils.NS_CALDAV, "allcomp") 29 | val COMP = QName(XmlUtils.NS_CALDAV, "comp") 30 | 31 | } 32 | 33 | 34 | object Factory : PropertyFactory { 35 | 36 | override fun getName() = NAME 37 | 38 | override fun create(parser: XmlReader): SupportedCalendarComponentSet { 39 | /* 40 | 41 | 42 | */ 43 | val components = SupportedCalendarComponentSet(false, false, false) 44 | 45 | val depth = parser.depth 46 | var eventType = parser.eventType 47 | while (!(eventType == EventType.END_ELEMENT && parser.depth == depth)) { 48 | if (eventType == EventType.START_ELEMENT && parser.depth == depth + 1) { 49 | when (parser.name) { 50 | ALLCOMP -> { 51 | components.supportsEvents = true 52 | components.supportsTasks = true 53 | components.supportsJournal = true 54 | } 55 | 56 | COMP -> 57 | when (parser.getAttributeValue(null, "name")?.uppercase()) { 58 | "VEVENT" -> components.supportsEvents = true 59 | "VTODO" -> components.supportsTasks = true 60 | "VJOURNAL" -> components.supportsJournal = true 61 | } 62 | } 63 | } 64 | eventType = parser.next() 65 | } 66 | 67 | return components 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/StatusLine.kt: -------------------------------------------------------------------------------- 1 | package at.bitfire.dav4jvm 2 | 3 | import io.ktor.client.statement.* 4 | import io.ktor.http.* 5 | import kotlin.jvm.JvmField 6 | 7 | class StatusLine( 8 | @JvmField val protocol: HttpProtocolVersion, 9 | @JvmField val status: HttpStatusCode 10 | ) { 11 | 12 | override fun toString(): String { 13 | return buildString { 14 | append(protocol.toString()) 15 | append(' ').append(status) 16 | } 17 | } 18 | 19 | companion object { 20 | fun get(response: HttpResponse): StatusLine { 21 | return StatusLine(response.version, response.status) 22 | } 23 | 24 | @Throws(IllegalStateException::class) 25 | fun parse(statusLine: String): StatusLine { 26 | // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t 27 | // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 28 | 29 | // Parse protocol like "HTTP/1.1" followed by a space. 30 | val codeStart: Int = statusLine.indexOf(" ") + 1 31 | val protocol: HttpProtocolVersion = if (statusLine.startsWith("HTTP/")) { 32 | HttpProtocolVersion.parse(statusLine.takeWhile { !it.isWhitespace() }) 33 | } else if (statusLine.startsWith("ICY ")) { 34 | // Shoutcast uses ICY instead of "HTTP/1.0". 35 | HttpProtocolVersion.HTTP_1_0 36 | } else if (statusLine.startsWith("SOURCETABLE ")) { 37 | // NTRIP r1 uses SOURCETABLE instead of HTTP/1.1 38 | HttpProtocolVersion.HTTP_1_1 39 | } else { 40 | throw IllegalStateException("Unexpected status line: $statusLine") 41 | } 42 | 43 | // Parse response code like "200". Always 3 digits. 44 | if (statusLine.length < codeStart + 3) { 45 | throw IllegalStateException("Unexpected status line: $statusLine") 46 | } 47 | val code = 48 | statusLine.substring(codeStart, codeStart + 3).toIntOrNull() 49 | ?: throw IllegalStateException( 50 | "Unexpected status line: $statusLine" 51 | ) 52 | 53 | // Parse an optional response message like "OK" or "Not Modified". If it 54 | // exists, it is separated from the response code by a space. 55 | var message = "" 56 | if (statusLine.length > codeStart + 3) { 57 | if (statusLine[codeStart + 3] != ' ') { 58 | throw IllegalStateException("Unexpected status line: $statusLine") 59 | } 60 | message = statusLine.substring(codeStart + 4) 61 | } 62 | 63 | return StatusLine(protocol, HttpStatusCode(code, message)) 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/XmlUtilsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import io.kotest.core.spec.style.FunSpec 10 | import nl.adaptivity.xmlutil.EventType 11 | import nl.adaptivity.xmlutil.QName 12 | import nl.adaptivity.xmlutil.localPart 13 | import kotlin.test.assertEquals 14 | import kotlin.test.assertTrue 15 | 16 | object XmlUtilsTest : FunSpec({ 17 | 18 | test("testProcessTagRoot") { 19 | val parser = XmlUtils.createReader("") 20 | // now on START_DOCUMENT [0] 21 | 22 | var processed = false 23 | XmlUtils.processTag(parser, QName("", "test")) { 24 | processed = true 25 | } 26 | assertTrue(processed) 27 | } 28 | 29 | test("testProcessTagDepth1") { 30 | val parser = XmlUtils.createReader("") 31 | parser.next() // now on START_TAG 32 | 33 | var processed = false 34 | XmlUtils.processTag(parser, QName("", "test")) { 35 | processed = true 36 | } 37 | assertTrue(processed) 38 | } 39 | 40 | test("testReadText") { 41 | val parser = XmlUtils.createReader("Test 1Test 2") 42 | parser.next() 43 | parser.next() // now on START_TAG 44 | 45 | assertEquals("Test 1", XmlUtils.readText(parser)) 46 | assertEquals(EventType.END_ELEMENT, parser.eventType) 47 | parser.next() 48 | 49 | assertEquals("Test 2", XmlUtils.readText(parser)) 50 | assertEquals(EventType.END_ELEMENT, parser.eventType) 51 | } 52 | 53 | test("testReadTextCDATA") { 54 | val parser = XmlUtils.createReader("Test 2]]>") 55 | parser.next() // now on START_TAG 56 | 57 | assertEquals("Test 1Test 2", XmlUtils.readText(parser)) 58 | assertEquals(EventType.END_ELEMENT, parser.eventType) 59 | } 60 | 61 | test("testReadTextPropertyRoot") { 62 | val parser = XmlUtils.createReader("Test 1Test 2") 63 | parser.next() // now on START_TAG 64 | 65 | val entries = mutableListOf() 66 | XmlUtils.readTextPropertyList(parser, QName("", "entry"), entries) 67 | assertEquals("Test 1", entries[0]) 68 | assertEquals("Test 2", entries[1]) 69 | 70 | parser.next() // END_TAG 71 | assertEquals(EventType.END_DOCUMENT, parser.eventType) 72 | } 73 | 74 | test("testReadTextPropertyListDepth1") { 75 | val parser = XmlUtils.createReader("Test 1Test 2") 76 | parser.next() // now on START_TAG [1] 77 | 78 | val entries = mutableListOf() 79 | XmlUtils.readTextPropertyList(parser, QName("", "entry"), entries) 80 | assertEquals("Test 1", entries[0]) 81 | assertEquals("Test 2", entries[1]) 82 | assertEquals(EventType.END_ELEMENT, parser.eventType) 83 | assertEquals("test", parser.name.localPart) 84 | } 85 | }) -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/PropertyRegistry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import at.bitfire.dav4jvm.property.* 10 | import nl.adaptivity.xmlutil.QName 11 | import nl.adaptivity.xmlutil.XmlException 12 | import nl.adaptivity.xmlutil.XmlReader 13 | 14 | object PropertyRegistry { 15 | 16 | private val factories = mutableMapOf() 17 | 18 | init { 19 | Dav4jvm.log.info("Registering DAV property factories") 20 | registerDefaultFactories() 21 | } 22 | 23 | private fun registerDefaultFactories() { 24 | register(listOf( 25 | AddMember.Factory, 26 | AddressbookDescription.Factory, 27 | AddressbookHomeSet.Factory, 28 | AddressData.Factory, 29 | CalendarColor.Factory, 30 | CalendarData.Factory, 31 | CalendarDescription.Factory, 32 | CalendarHomeSet.Factory, 33 | CalendarProxyReadFor.Factory, 34 | CalendarProxyWriteFor.Factory, 35 | CalendarTimezone.Factory, 36 | CalendarUserAddressSet.Factory, 37 | CreationDate.Factory, 38 | CurrentUserPrincipal.Factory, 39 | CurrentUserPrivilegeSet.Factory, 40 | DisplayName.Factory, 41 | GetContentLength.Factory, 42 | GetContentType.Factory, 43 | GetCTag.Factory, 44 | GetETag.Factory, 45 | GetLastModified.Factory, 46 | GroupMembership.Factory, 47 | MaxICalendarSize.Factory, 48 | MaxVCardSize.Factory, 49 | Owner.Factory, 50 | QuotaAvailableBytes.Factory, 51 | QuotaUsedBytes.Factory, 52 | ResourceType.Factory, 53 | ScheduleTag.Factory, 54 | Source.Factory, 55 | SupportedAddressData.Factory, 56 | SupportedCalendarComponentSet.Factory, 57 | SupportedCalendarData.Factory, 58 | SupportedReportSet.Factory, 59 | SyncToken.Factory 60 | )) 61 | } 62 | 63 | 64 | /** 65 | * Registers a property factory, so that objects for all WebDAV properties which are handled 66 | * by this factory can be created. 67 | * 68 | * @param factory property factory to be registered 69 | */ 70 | fun register(factory: PropertyFactory) { 71 | Dav4jvm.log.trace("Registering ${factory::class.simpleName} for ${factory.getName()}") 72 | factories[factory.getName()] = factory 73 | } 74 | 75 | /** 76 | * Registers some property factories, so that objects for all WebDAV properties which are handled 77 | * by these factories can be created. 78 | 79 | * @param factories property factories to be registered 80 | */ 81 | fun register(factories: Iterable) { 82 | factories.forEach { 83 | register(it) 84 | } 85 | } 86 | 87 | fun create(name: QName, parser: XmlReader) = 88 | try { 89 | factories[name]?.create(parser) 90 | } catch (e: XmlException) { 91 | Dav4jvm.log.warn("Couldn't parse $name", e) 92 | null 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/HttpUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | import io.ktor.utils.io.* 12 | import io.ktor.utils.io.bits.* 13 | import korlibs.time.* 14 | import korlibs.time.locale.ExtendedTimezoneNames 15 | 16 | 17 | object HttpUtils { 18 | 19 | private const val httpDateFormatStr = "EEE, dd MMM yyyy HH:mm:ss zzz" 20 | private val httpDateFormat = DateFormat(httpDateFormatStr).withTimezoneNames(ExtendedTimezoneNames) 21 | 22 | private val extendedDateFormats by lazy { 23 | listOf(httpDateFormatStr, // RFC 822, updated by RFC 1123 with any TZ 24 | "EEE, dd MMM yyyy HH:mm:ss zzz", 25 | "EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 850, obsoleted by RFC 1036 with any TZ. 26 | "EEE MMM d HH:mm:ss yyyy", // ANSI C's asctime() format 27 | // Alternative formats. 28 | "EEE, dd-MMM-yyyy HH:mm:ss z", 29 | "EEE, dd-MMM-yyyy HH-mm-ss z", 30 | "EEE, dd MMM yy HH:mm:ss z", 31 | "EEE dd-MMM-yyyy HH:mm:ss z", 32 | "EEE dd MMM yyyy HH:mm:ss z", 33 | "EEE dd-MMM-yyyy HH-mm-ss z", 34 | "EEE dd-MMM-yy HH:mm:ss z", 35 | "EEE dd MMM yy HH:mm:ss z", 36 | "EEE,dd-MMM-yy HH:mm:ss z", 37 | "EEE,dd-MMM-yyyy HH:mm:ss z", 38 | "EEE, dd-MM-yyyy HH:mm:ss z", 39 | /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */ 40 | "EEE MMM d yyyy HH:mm:ss z").map { DateFormat(it) } 41 | } 42 | 43 | /** 44 | * Gets the resource name (the last segment of the path) from an URL. 45 | * Empty if the resource is the base directory. 46 | * 47 | * * `dir` for `https://example.com/dir/` 48 | * * `file` for `https://example.com/file` 49 | * * `` for `https://example.com` or `https://example.com/` 50 | * 51 | * @return resource name 52 | */ 53 | fun fileName(url: Url): String { 54 | val pathSegments = url.pathSegments.dropLastWhile { it == "" } 55 | return pathSegments.lastOrNull() ?: "" 56 | } 57 | 58 | fun listHeader(response: HttpResponse, name: String): Array = 59 | response.headers.getAll(name)?.filter { it.isNotEmpty() }?.toTypedArray() ?: emptyArray() 60 | 61 | /** 62 | * Formats a date for use in HTTP headers using [httpDateFormat]. 63 | * 64 | * @param date date to be formatted 65 | * @return date in HTTP-date format 66 | */ 67 | fun formatDate(date: DateTime): String = httpDateFormat.format(date) 68 | 69 | /** 70 | * Parses a HTTP-date. 71 | * 72 | * @param dateStr date with format specified by RFC 7231 section 7.1.1.1 73 | * or in one of the obsolete formats (copied from okhttp internal date-parsing class) 74 | * 75 | * @return date, or null if date could not be parsed 76 | */ 77 | fun parseDate(dateStr: String): DateTimeTz? { 78 | for (df in extendedDateFormats) { 79 | try { 80 | return df.parse(dateStr) 81 | } catch (_: Exception) { 82 | } 83 | } 84 | Dav4jvm.log.warn("Couldn't parse date: $dateStr, ignoring") 85 | return null 86 | } 87 | 88 | } 89 | 90 | suspend fun ByteReadChannel.isEmpty() = isClosedForRead || withMemory(1) { mem -> peekTo(mem, 0, 0, 1, 1) } == 0L -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/UrlUtilsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import io.kotest.assertions.withClue 10 | import io.kotest.core.spec.style.FunSpec 11 | import io.kotest.datatest.withData 12 | import io.kotest.matchers.booleans.shouldBeFalse 13 | import io.kotest.matchers.booleans.shouldBeTrue 14 | import io.ktor.http.* 15 | import kotlin.test.assertEquals 16 | import kotlin.test.assertNull 17 | 18 | object UrlUtilsTest : FunSpec({ 19 | 20 | fun String.toHttpUrlOrNull() = Url(this) 21 | 22 | withData( 23 | "http://host/resource".toHttpUrlOrNull() to "http://host/resource".toHttpUrlOrNull(), 24 | "http://host:80/resource".toHttpUrlOrNull() to "http://host/resource".toHttpUrlOrNull(), 25 | "https://HOST:443/resource".toHttpUrlOrNull() to "https://host/resource".toHttpUrlOrNull(), 26 | "https://host:443/my@dav/".toHttpUrlOrNull() to "https://host/my%40dav/".toHttpUrlOrNull(), 27 | "http://host/resource".toHttpUrlOrNull() to "http://host/resource#frag1".toHttpUrlOrNull(), 28 | "https://host/%5bresource%5d/".toHttpUrlOrNull() to "https://host/[resource]/".toHttpUrlOrNull() 29 | ) { (a, b) -> 30 | withClue("$a shouldEqual $b") { 31 | UrlUtils.equals(a, b).shouldBeTrue() 32 | } 33 | } 34 | 35 | withData( 36 | "http://host/resource".toHttpUrlOrNull() to "http://host/resource/".toHttpUrlOrNull(), 37 | "http://host/resource".toHttpUrlOrNull() to "http://host:81/resource".toHttpUrlOrNull() 38 | ) { (a, b) -> 39 | withClue("$a shouldNotEqual $b") { 40 | UrlUtils.equals(a, b).shouldBeFalse() 41 | } 42 | } 43 | 44 | test("testHostToDomain") { 45 | assertNull(UrlUtils.hostToDomain(null)) 46 | assertEquals("", UrlUtils.hostToDomain(".")) 47 | assertEquals("com", UrlUtils.hostToDomain("com")) 48 | assertEquals("com", UrlUtils.hostToDomain("com.")) 49 | assertEquals("example.com", UrlUtils.hostToDomain("example.com")) 50 | assertEquals("example.com", UrlUtils.hostToDomain("example.com.")) 51 | assertEquals("example.com", UrlUtils.hostToDomain(".example.com")) 52 | assertEquals("example.com", UrlUtils.hostToDomain(".example.com.")) 53 | assertEquals("example.com", UrlUtils.hostToDomain("host.example.com")) 54 | assertEquals("example.com", UrlUtils.hostToDomain("host.example.com.")) 55 | assertEquals("example.com", UrlUtils.hostToDomain("sub.host.example.com")) 56 | assertEquals("example.com", UrlUtils.hostToDomain("sub.host.example.com.")) 57 | } 58 | 59 | test("testOmitTrailingSlash") { 60 | assertEquals( 61 | "http://host/resource".toHttpUrlOrNull(), 62 | UrlUtils.omitTrailingSlash("http://host/resource".toHttpUrlOrNull()) 63 | ) 64 | assertEquals( 65 | "http://host/resource".toHttpUrlOrNull(), 66 | UrlUtils.omitTrailingSlash("http://host/resource/".toHttpUrlOrNull()) 67 | ) 68 | } 69 | 70 | test("testWithTrailingSlash") { 71 | assertEquals( 72 | "http://host/resource/".toHttpUrlOrNull(), 73 | UrlUtils.withTrailingSlash("http://host/resource".toHttpUrlOrNull()) 74 | ) 75 | assertEquals( 76 | "http://host/resource/".toHttpUrlOrNull(), 77 | UrlUtils.withTrailingSlash("http://host/resource/".toHttpUrlOrNull()) 78 | ) 79 | } 80 | 81 | }) 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![License](https://img.shields.io/github/license/bitfireAT/dav4jvm)](https://github.com/bitfireAT/dav4jvm/blob/main/LICENSE) 3 | [![Tests](https://github.com/bitfireAT/dav4jvm/actions/workflows/test.yml/badge.svg)](https://github.com/bitfireAT/dav4jvm/actions/workflows/test.yml) 4 | [![JitPack](https://img.shields.io/jitpack/v/github/bitfireAT/dav4jvm)](https://jitpack.io/#bitfireAT/dav4jvm) 5 | [![KDoc](https://img.shields.io/badge/documentation-KDoc-informational)](https://bitfireat.github.io/dav4jvm/) 6 | 7 | 8 | # dav4jvm 9 | 10 | dav4jvm is a WebDAV/CalDAV/CardDAV library for JVM (Java/Kotlin). It has 11 | been developed for [DAVx⁵](https://www.davx5.com) initially. 12 | 13 | Repository: https://github.com/bitfireAT/dav4jvm/ 14 | 15 | Generated KDoc: https://bitfireat.github.io/dav4jvm/ 16 | 17 | For questions, suggestions etc. use [Github discussions](https://github.com/bitfireAT/dav4jvm/discussions). 18 | We're happy about contributions, but please let us know in the discussions before. Then make the changes 19 | in your own repository and send a pull request. 20 | 21 | 22 | ## Installation 23 | 24 | You can use jitpack.io to include dav4jvm: 25 | 26 | allprojects { 27 | repositories { 28 | maven { url 'https://jitpack.io' } 29 | } 30 | } 31 | dependencies { 32 | implementation 'com.github.bitfireAT:dav4jvm:' // see tags for latest version, like 1.0, or use the latest commit ID from main branch 33 | //implementation 'com.github.bitfireAT:dav4jvm:main-SNAPSHOT' // use it only for testing because it doesn't generate reproducible builds 34 | } 35 | 36 | dav4jvm needs a working XmlPullParser (XPP). On Android, the system already comes with 37 | XPP and you don't need to include one; on other systems, you may need to 38 | import for instance `org.ogce:xpp3` to get dav4jvm to work. 39 | 40 | 41 | ## Usage 42 | 43 | First, you'll need to set up an OkHttp instance. Use `BasicDigestAuthHandler` to configure the credentials: 44 | 45 | val authHandler = BasicDigestAuthHandler( 46 | domain = null, // Optional, to only authenticate against hosts with this domain. 47 | username = "user1", 48 | password = "pass1" 49 | ) 50 | val okHttpClient = OkHttpClient.Builder() 51 | .followRedirects(false) 52 | .authenticator(authHandler) 53 | .addNetworkInterceptor(authHandler) 54 | .build() 55 | 56 | 57 | ### Files 58 | 59 | Here's an example to create and download a file: 60 | 61 | val location = "https://example.com/webdav/hello.txt".toHttpUrl() 62 | val davCollection = DavCollection(account.requireClient(), location) 63 | 64 | // Create a text file 65 | davCollection.put("World".toRequestBody(contentType = "text/plain".toMediaType()) { response -> 66 | // Upload successful! 67 | } 68 | 69 | // Download a text file 70 | davCollection.get(accept = "", headers = null) { response -> 71 | response.body?.string() 72 | // Download successful! 73 | } 74 | 75 | To list a folder's contents, you need to pass in which properties to fetch: 76 | 77 | val location = "https://example.com/webdav/".toHttpUrl() 78 | val davCollection = DavCollection(account.requireClient(), location) 79 | 80 | davCollection.propfind(depth = 1, DisplayName.NAME, GetLastModified.NAME) { response, relation -> 81 | // This callback will be called for every file in the folder. 82 | // Use `response.properties` to access the successfully retrieved properties. 83 | } 84 | 85 | ## Custom properties 86 | 87 | If you use custom WebDAV properties, register the corresponding factories with `PropertyRegistry.register()` 88 | before calling other dav4jvm methods. 89 | 90 | 91 | ## Contact / License 92 | 93 | dav4jvm is licensed under [Mozilla Public License, v. 2.0](LICENSE). 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/property/CurrentUserPrivilegeSet.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.property 8 | 9 | import at.bitfire.dav4jvm.Property 10 | import at.bitfire.dav4jvm.PropertyFactory 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import nl.adaptivity.xmlutil.EventType 13 | import nl.adaptivity.xmlutil.QName 14 | import nl.adaptivity.xmlutil.XmlReader 15 | import kotlin.jvm.JvmField 16 | 17 | data class CurrentUserPrivilegeSet( 18 | // not all privileges from RFC 3744 are implemented by now 19 | // feel free to add more if you need them for your project 20 | var mayRead: Boolean = false, 21 | var mayWriteProperties: Boolean = false, 22 | var mayWriteContent: Boolean = false, 23 | var mayBind: Boolean = false, 24 | var mayUnbind: Boolean = false 25 | ) : Property { 26 | 27 | companion object { 28 | 29 | @JvmField 30 | val NAME = QName(XmlUtils.NS_WEBDAV, "current-user-privilege-set") 31 | 32 | val PRIVILEGE = QName(XmlUtils.NS_WEBDAV, "privilege") 33 | val READ = QName(XmlUtils.NS_WEBDAV, "read") 34 | val WRITE = QName(XmlUtils.NS_WEBDAV, "write") 35 | val WRITE_PROPERTIES = QName(XmlUtils.NS_WEBDAV, "write-properties") 36 | val WRITE_CONTENT = QName(XmlUtils.NS_WEBDAV, "write-content") 37 | val BIND = QName(XmlUtils.NS_WEBDAV, "bind") 38 | val UNBIND = QName(XmlUtils.NS_WEBDAV, "unbind") 39 | val ALL = QName(XmlUtils.NS_WEBDAV, "all") 40 | 41 | } 42 | 43 | 44 | object Factory : PropertyFactory { 45 | 46 | override fun getName() = NAME 47 | 48 | override fun create(parser: XmlReader): CurrentUserPrivilegeSet { 49 | // 50 | // 51 | val privs = CurrentUserPrivilegeSet() 52 | 53 | XmlUtils.processTag(parser, PRIVILEGE) { 54 | val depth = parser.depth 55 | var eventType = parser.eventType 56 | while (!(eventType == EventType.END_ELEMENT && parser.depth == depth)) { 57 | if (eventType == EventType.START_ELEMENT && parser.depth == depth + 1) 58 | when (parser.name) { 59 | READ -> 60 | privs.mayRead = true 61 | 62 | WRITE -> { 63 | privs.mayBind = true 64 | privs.mayUnbind = true 65 | privs.mayWriteProperties = true 66 | privs.mayWriteContent = true 67 | } 68 | 69 | WRITE_PROPERTIES -> 70 | privs.mayWriteProperties = true 71 | 72 | WRITE_CONTENT -> 73 | privs.mayWriteContent = true 74 | 75 | BIND -> 76 | privs.mayBind = true 77 | 78 | UNBIND -> 79 | privs.mayUnbind = true 80 | 81 | ALL -> { 82 | privs.mayRead = true 83 | privs.mayBind = true 84 | privs.mayUnbind = true 85 | privs.mayWriteProperties = true 86 | privs.mayWriteContent = true 87 | } 88 | } 89 | eventType = parser.next() 90 | } 91 | } 92 | 93 | return privs 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/UrlUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import io.ktor.http.* 10 | 11 | object UrlUtils { 12 | 13 | /** 14 | * Compares two URLs in WebDAV context. If two URLs are considered *equal*, both 15 | * represent the same WebDAV resource (e.g. `http://host:80/folder1` and `http://HOST/folder1#somefragment`). 16 | * 17 | * It decodes %xx entities in the path, so `/my@dav` and `/my%40dav` are considered the same. 18 | * This is important to process multi-status responses: some servers serve a multi-status 19 | * response with href `/my@dav` when you request `/my%40dav` and vice versa. 20 | * 21 | * This method does not deal with trailing slashes, so if you want to compare collection URLs, 22 | * make sure they both (don't) have a trailing slash before calling this method, for instance 23 | * with [omitTrailingSlash] or [withTrailingSlash]. 24 | * 25 | * @param url1 the first URL to be compared 26 | * @param url2 the second URL to be compared 27 | * 28 | * @return whether [url1] and [url2] (usually) represent the same WebDAV resource 29 | */ 30 | fun equals(url1: Url, url2: Url): Boolean { 31 | // if okhttp thinks the two URLs are equal, they're in any case 32 | // (and it's a simple String comparison) 33 | if (url1 == url2) 34 | return true 35 | 36 | // drop #fragment parts and convert to URI 37 | val uri1 = URLBuilder(url1.toString()).apply { 38 | host = host.lowercase() 39 | encodedPath = encodedPath.decodeURLPart() 40 | fragment = "" 41 | }.build() 42 | val uri2 = URLBuilder(url2.toString()).apply { 43 | host = host.lowercase() 44 | encodedPath = encodedPath.decodeURLPart() 45 | fragment = "" 46 | }.build() 47 | 48 | return uri1 == uri2 49 | } 50 | 51 | /** 52 | * Gets the first-level domain name (without subdomains) from a host name. 53 | * Also removes trailing dots. 54 | * 55 | * @param host name (e.g. `www.example.com.`) 56 | * 57 | * @return domain name (e.g. `example.com`) 58 | */ 59 | fun hostToDomain(host: String?): String? { 60 | if (host == null) 61 | return null 62 | 63 | // remove optional dot at end 64 | val withoutTrailingDot = host.removeSuffix(".") 65 | 66 | // split into labels 67 | val labels = withoutTrailingDot.split('.') 68 | return if (labels.size >= 2) { 69 | labels[labels.size - 2] + "." + labels[labels.size - 1] 70 | } else 71 | withoutTrailingDot 72 | } 73 | 74 | /** 75 | * Ensures that a given URL doesn't have a trailing slash after member names. 76 | * If the path is the root path (`/`), the slash is preserved. 77 | * 78 | * @param url URL to process (e.g. 'http://host/test1/') 79 | * 80 | * @return URL without trailing slash (except when the path is the root path), e.g. `http://host/test1` 81 | */ 82 | fun omitTrailingSlash(url: Url): Url { 83 | val hasTrailingSlash = url.pathSegments.last() == "" 84 | 85 | return if (hasTrailingSlash) 86 | URLBuilder(url).apply { pathSegments = pathSegments.dropLast(1) }.build() 87 | else 88 | url 89 | } 90 | 91 | /** 92 | * Ensures that a given URL has a trailing slash after member names. 93 | * 94 | * @param url URL to process (e.g. 'http://host/test1') 95 | * 96 | * @return URL with trailing slash, e.g. `http://host/test1/` 97 | */ 98 | fun withTrailingSlash(url: Url): Url { 99 | val hasTrailingSlash = url.pathSegments.last() == "" 100 | 101 | return if (hasTrailingSlash) 102 | url 103 | else 104 | URLBuilder(url).apply { pathSegments += "" }.build() 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/DavCollection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import at.bitfire.dav4jvm.XmlUtils.insertTag 10 | import at.bitfire.dav4jvm.exception.DavException 11 | import at.bitfire.dav4jvm.exception.HttpException 12 | import at.bitfire.dav4jvm.property.SyncToken 13 | import io.ktor.client.* 14 | import io.ktor.client.request.* 15 | import io.ktor.http.* 16 | import io.ktor.util.logging.* 17 | import io.ktor.utils.io.errors.* 18 | import nl.adaptivity.xmlutil.QName 19 | import kotlin.coroutines.cancellation.CancellationException 20 | import kotlin.jvm.JvmOverloads 21 | 22 | /** 23 | * Represents a WebDAV collection. 24 | */ 25 | open class DavCollection @JvmOverloads constructor( 26 | httpClient: HttpClient, 27 | location: Url, 28 | log: Logger = Dav4jvm.log 29 | ) : DavResource(httpClient, location, log) { 30 | 31 | companion object { 32 | val SYNC_COLLECTION = QName(XmlUtils.NS_WEBDAV, "sync-collection") 33 | val SYNC_LEVEL = QName(XmlUtils.NS_WEBDAV, "sync-level") 34 | val LIMIT = QName(XmlUtils.NS_WEBDAV, "limit") 35 | val NRESULTS = QName(XmlUtils.NS_WEBDAV, "nresults") 36 | } 37 | 38 | /** 39 | * Sends a POST request. Primarily intended to be used with an Add-Member URL (RFC 5995). 40 | */ 41 | @Throws(IOException::class, HttpException::class, CancellationException::class) 42 | suspend fun post(body: Any, contentType: ContentType, ifNoneMatch: Boolean = false, callback: ResponseCallback) { 43 | //TODO followRedirects { 44 | val response = httpClient.prepareRequest { 45 | method = HttpMethod.Post 46 | setBody(body) 47 | url(location) 48 | header(HttpHeaders.ContentType, contentType) 49 | if (ifNoneMatch) 50 | // don't overwrite anything existing 51 | header(HttpHeaders.IfNoneMatch, "*") 52 | }.execute() 53 | 54 | checkStatus(response) 55 | callback.onResponse(response) 56 | } 57 | 58 | /** 59 | * Sends a REPORT sync-collection request. 60 | * 61 | * @param syncToken sync-token to be sent with the request 62 | * @param infiniteDepth sync-level to be sent with the request: false = "1", true = "infinite" 63 | * @param limit maximum number of results (may cause truncation) 64 | * @param properties WebDAV properties to be requested 65 | * @param callback called for every WebDAV response XML element in the result 66 | * 67 | * @return list of properties which have been received in the Multi-Status response, but 68 | * are not part of response XML elements (like `sync-token` which is returned as [SyncToken]) 69 | * 70 | * @throws java.io.IOException on I/O error 71 | * @throws HttpException on HTTP error 72 | * @throws DavException on WebDAV error 73 | */ 74 | suspend fun reportChanges( 75 | syncToken: String?, 76 | infiniteDepth: Boolean, 77 | limit: Int?, 78 | vararg properties: QName, 79 | callback: MultiResponseCallback 80 | ): List { 81 | /* 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | */ 91 | val writer = StringBuilder() 92 | val serializer = XmlUtils.createWriter(writer) 93 | serializer.startDocument(encoding = "UTF-8") 94 | serializer.setPrefix("", XmlUtils.NS_WEBDAV) 95 | serializer.insertTag(SYNC_COLLECTION) { 96 | insertTag(SyncToken.NAME) { 97 | if (syncToken != null) 98 | text(syncToken) 99 | } 100 | insertTag(SYNC_LEVEL) { 101 | text(if (infiniteDepth) "infinite" else "1") 102 | } 103 | if (limit != null) 104 | insertTag(LIMIT) { 105 | insertTag(NRESULTS) { 106 | text(limit.toString()) 107 | } 108 | } 109 | insertTag(PROP) { 110 | for (prop in properties) 111 | insertTag(prop) 112 | } 113 | } 114 | serializer.endDocument() 115 | 116 | //TODO followRedirects { 117 | val response = httpClient.prepareRequest { 118 | url(location) 119 | method = Report 120 | setBody(writer.toString()) 121 | header(HttpHeaders.ContentType, MIME_XML) 122 | header("Depth", "0") 123 | }.execute() 124 | return processMultiStatus(response, callback) 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/XmlUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import at.bitfire.dav4jvm.exception.InvalidPropertyException 10 | import io.ktor.utils.io.errors.* 11 | import nl.adaptivity.xmlutil.* 12 | import nl.adaptivity.xmlutil.core.KtXmlWriter 13 | import nl.adaptivity.xmlutil.core.XmlVersion 14 | 15 | object XmlUtils { 16 | 17 | const val NS_WEBDAV = "DAV:" 18 | const val NS_CALDAV = "urn:ietf:params:xml:ns:caldav" 19 | const val NS_CARDDAV = "urn:ietf:params:xml:ns:carddav" 20 | const val NS_APPLE_ICAL = "http://apple.com/ns/ical/" 21 | const val NS_CALENDARSERVER = "http://calendarserver.org/ns/" 22 | 23 | fun createReader(source: String) = XmlStreaming.newGenericReader(source).also { /*Initialize*/ it.next() } 24 | fun createWriter(destination: Appendable) = KtXmlWriter( 25 | destination, 26 | isRepairNamespaces = true, 27 | xmlDeclMode = XmlDeclMode.Auto, 28 | xmlVersion = XmlVersion.XML10 29 | ) 30 | 31 | @Throws(IOException::class, XmlException::class) 32 | fun processTag( 33 | parser: XmlReader, 34 | name: QName? = null, 35 | eventType: EventType = EventType.START_ELEMENT, 36 | targetDepth: Int = parser.depth + 1, 37 | processor: () -> Unit 38 | ) = processTag(parser, { d, e, n -> d == targetDepth && e == eventType && (name == null || name == n) }, processor) 39 | 40 | fun processTag( 41 | parser: XmlReader, 42 | selector: (depth: Int, eventType: EventType, name: QName?) -> Boolean, 43 | processor: () -> Unit 44 | ) { 45 | if (!parser.isStarted) parser.next() 46 | val endTagDepth = parser.depth 47 | var mEventType = parser.eventType 48 | if (mEventType != EventType.START_ELEMENT && mEventType != EventType.START_DOCUMENT) throw XmlException("Need to be at the start of a tag or document to process it! Was $mEventType") 49 | val processingDoc = mEventType == EventType.START_DOCUMENT 50 | val endTagName: QName? = if (!processingDoc) parser.name else null 51 | var cName = if (!processingDoc) parser.name else null 52 | do { 53 | if (selector(parser.depth, mEventType, cName)) { 54 | processor() 55 | } 56 | mEventType = parser.next() 57 | cName = when (mEventType) { 58 | EventType.END_ELEMENT, EventType.START_ELEMENT, EventType.ENTITY_REF -> parser.name 59 | else -> null 60 | } 61 | } while ( 62 | !((mEventType == EventType.END_ELEMENT && parser.name == endTagName && parser.depth <= endTagDepth) || 63 | (processingDoc && mEventType == EventType.END_DOCUMENT)) 64 | ) 65 | } 66 | 67 | @Throws(IOException::class, XmlException::class) 68 | fun readText(parser: XmlReader): String? { 69 | var text: String? = null 70 | val cDepth = parser.depth 71 | processTag(parser, { d, e, _ -> d == cDepth && (e == EventType.TEXT || e == EventType.CDSECT) }) { 72 | text = parser.text 73 | } 74 | 75 | return text 76 | } 77 | 78 | /** 79 | * Same as [readText], but requires a [XmlPullParser.TEXT] value. 80 | * 81 | * @throws InvalidPropertyException when no text could be read 82 | */ 83 | @Throws(InvalidPropertyException::class, IOException::class, XmlException::class) 84 | fun requireReadText(parser: XmlReader): String = 85 | readText(parser) 86 | ?: throw InvalidPropertyException("XML text for ${parser.namespaceURI}:${parser.name} must not be empty") 87 | 88 | @Throws(IOException::class, XmlException::class) 89 | fun readTextProperty(parser: XmlReader, name: QName): String? { 90 | var result: String? = null 91 | processTag(parser, name) { result = parser.nextText() } 92 | return result 93 | } 94 | 95 | @Throws(IOException::class, XmlException::class) 96 | fun readTextPropertyList(parser: XmlReader, name: QName, list: MutableCollection) { 97 | processTag(parser, name) { list.add(parser.nextText()) } 98 | } 99 | 100 | 101 | fun XmlWriter.insertTag(name: QName, contentGenerator: XmlWriter.() -> Unit = {}) { 102 | 103 | if (name.namespaceURI == XMLConstants.XML_NS_URI || name.namespaceURI == XMLConstants.XMLNS_ATTRIBUTE_NS_URI) { 104 | val namespace = namespaceContext.getNamespaceURI(name.prefix) ?: XMLConstants.NULL_NS_URI 105 | startTag(namespace, name.localPart, name.prefix) 106 | } else { 107 | var writeNs = false 108 | 109 | val usedPrefix = getPrefix(name.namespaceURI) ?: run { 110 | val currentNs = getNamespaceUri(name.prefix) ?: XMLConstants.NULL_NS_URI 111 | if (name.namespaceURI != currentNs) { 112 | writeNs = true 113 | } 114 | if (name.prefix != XMLConstants.DEFAULT_NS_PREFIX) name.prefix else generateAutoPrefix() 115 | } 116 | startTag(name.namespaceURI, name.localPart, usedPrefix) 117 | if (writeNs) this.namespaceAttr(usedPrefix, name.namespaceURI) 118 | } 119 | 120 | contentGenerator(this) 121 | endTag(name) 122 | } 123 | 124 | private fun XmlWriter.generateAutoPrefix(): String { 125 | var prefix: String 126 | var prefixN = 1 127 | do { 128 | prefix = "n${prefixN++}" 129 | } while (getNamespaceUri(prefix) != null) 130 | return prefix 131 | } 132 | 133 | @Throws(XmlException::class) 134 | fun XmlReader.nextText(): String { 135 | require(EventType.START_ELEMENT, null) 136 | return when (next()) { 137 | EventType.TEXT, EventType.CDSECT -> { 138 | val rText = text 139 | if (next() != EventType.END_ELEMENT) throw XmlException() 140 | rText 141 | } 142 | 143 | EventType.END_ELEMENT -> "" 144 | else -> throw XmlException() 145 | } 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/DavAddressBook.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import at.bitfire.dav4jvm.XmlUtils.insertTag 10 | import at.bitfire.dav4jvm.exception.DavException 11 | import at.bitfire.dav4jvm.exception.HttpException 12 | import at.bitfire.dav4jvm.property.AddressData 13 | import at.bitfire.dav4jvm.property.GetContentType 14 | import at.bitfire.dav4jvm.property.GetETag 15 | import io.ktor.client.* 16 | import io.ktor.client.request.* 17 | import io.ktor.http.* 18 | import io.ktor.util.logging.* 19 | import io.ktor.utils.io.charsets.* 20 | import nl.adaptivity.xmlutil.QName 21 | import kotlin.jvm.JvmOverloads 22 | 23 | class DavAddressBook @JvmOverloads constructor( 24 | httpClient: HttpClient, 25 | location: Url, 26 | log: Logger = Dav4jvm.log 27 | ) : DavCollection(httpClient, location, log) { 28 | 29 | companion object { 30 | val MIME_JCARD = ContentType("application", "vcard+json") 31 | val MIME_VCARD3_UTF8 = ContentType.Text.VCard.withCharset(Charsets.UTF_8) 32 | val MIME_VCARD4 = ContentType.Text.VCard.withParameter("version", "4.0") 33 | 34 | val ADDRESSBOOK_QUERY = QName(XmlUtils.NS_CARDDAV, "addressbook-query") 35 | val ADDRESSBOOK_MULTIGET = QName(XmlUtils.NS_CARDDAV, "addressbook-multiget") 36 | val FILTER = QName(XmlUtils.NS_CARDDAV, "filter") 37 | } 38 | 39 | /** 40 | * Sends an addressbook-query REPORT request to the resource. 41 | * 42 | * @param callback called for every WebDAV response XML element in the result 43 | * 44 | * @return list of properties which have been received in the Multi-Status response, but 45 | * are not part of response XML elements 46 | * 47 | * @throws IOException on I/O error 48 | * @throws HttpException on HTTP error 49 | * @throws DavException on WebDAV error 50 | */ 51 | suspend fun addressbookQuery(callback: MultiResponseCallback): List { 52 | /* 55 | 56 | */ 57 | val writer = StringBuilder() 58 | val serializer = XmlUtils.createWriter(writer) 59 | serializer.startDocument(encoding = "UTF-8") 60 | serializer.setPrefix("", XmlUtils.NS_WEBDAV) 61 | serializer.setPrefix("CARD", XmlUtils.NS_CARDDAV) 62 | serializer.insertTag(ADDRESSBOOK_QUERY) { 63 | insertTag(PROP) { 64 | insertTag(GetETag.NAME) 65 | } 66 | insertTag(FILTER) 67 | } 68 | serializer.endDocument() 69 | val response = httpClient.prepareRequest { 70 | url(location) 71 | method = Report 72 | //.method("REPORT", writer.toString().toRequestBody(MIME_XML)) 73 | header(HttpHeaders.ContentType, MIME_XML.toString()) 74 | setBody(writer.toString()) 75 | header("Depth", "1") 76 | }.execute() 77 | return processMultiStatus(response, callback) 78 | } 79 | 80 | /** 81 | * Sends an addressbook-multiget REPORT request to the resource. 82 | * 83 | * @param urls list of vCard URLs to be requested 84 | * @param contentType MIME type of requested format; may be "text/vcard" for vCard or 85 | * "application/vcard+json" for jCard. *null*: don't request specific representation type 86 | * @param version vCard version subtype of the requested format. Should only be specified together with a [contentType] of "text/vcard". 87 | * Currently only useful value: "4.0" for vCard 4. *null*: don't request specific version 88 | * @param callback called for every WebDAV response XML element in the result 89 | * 90 | * @return list of properties which have been received in the Multi-Status response, but 91 | * are not part of response XML elements 92 | * 93 | * @throws IOException on I/O error 94 | * @throws HttpException on HTTP error 95 | * @throws DavException on WebDAV error 96 | */ 97 | suspend fun multiget( 98 | urls: List, 99 | contentType: String? = null, 100 | version: String? = null, 101 | callback: MultiResponseCallback 102 | ): List { 103 | /* 107 | */ 108 | val writer = StringBuilder() 109 | val serializer = XmlUtils.createWriter(writer) 110 | serializer.startDocument(encoding = "UTF-8") 111 | serializer.setPrefix("", XmlUtils.NS_WEBDAV) 112 | serializer.setPrefix("CARD", XmlUtils.NS_CARDDAV) 113 | serializer.insertTag(ADDRESSBOOK_MULTIGET) { 114 | insertTag(PROP) { 115 | insertTag(GetContentType.NAME) 116 | insertTag(GetETag.NAME) 117 | insertTag(AddressData.NAME) { 118 | if (contentType != null) 119 | attribute(null, AddressData.CONTENT_TYPE, null, contentType) 120 | if (version != null) 121 | attribute(null, AddressData.VERSION, null, version) 122 | } 123 | } 124 | for (url in urls) 125 | insertTag(HREF) { 126 | text(url.encodedPath) 127 | } 128 | } 129 | serializer.endDocument() 130 | 131 | //TODO followRedirects { 132 | val request = httpClient.prepareRequest { 133 | url(location) 134 | method = Report 135 | setBody(writer.toString()) 136 | header(HttpHeaders.ContentType, MIME_XML) 137 | header("Depth", "0") // "The request MUST include a Depth: 0 header [...]" 138 | }.execute() 139 | return processMultiStatus(request, callback) 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/exception/DavException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import at.bitfire.dav4jvm.Dav4jvm 10 | import at.bitfire.dav4jvm.Error 11 | import at.bitfire.dav4jvm.XmlUtils 12 | import at.bitfire.dav4jvm.isEmpty 13 | import io.ktor.client.statement.* 14 | import io.ktor.http.* 15 | import io.ktor.http.content.* 16 | import io.ktor.util.* 17 | import io.ktor.utils.io.core.* 18 | import io.ktor.utils.io.errors.* 19 | import nl.adaptivity.xmlutil.XmlException 20 | import kotlin.math.min 21 | 22 | /** 23 | * Signals that an error occurred during a WebDAV-related operation. 24 | * 25 | * This could be a logical error like when a required ETag has not been 26 | * received, but also an explicit HTTP error. 27 | */ 28 | open class DavException internal constructor( 29 | message: String, 30 | ex: Throwable? = null, 31 | exceptionData: ExceptionData 32 | ) : Exception(message, ex) { 33 | 34 | data class ExceptionData( 35 | val request: String? = null, 36 | val response: String? = null, 37 | val requestBody: String? = null, 38 | val responseBody: String? = null, 39 | val errors: List = emptyList() 40 | ) 41 | 42 | companion object { 43 | 44 | const val MAX_EXCERPT_SIZE = 10 * 1024 // don't dump more than 20 kB 45 | 46 | fun isPlainText(type: ContentType) = 47 | type.match(ContentType.Text.Any) || 48 | (type.contentType == "application" && type.contentSubtype in arrayOf("html", "xml")) 49 | 50 | suspend operator fun invoke( 51 | message: String, 52 | ex: Throwable? = null, 53 | httpResponse: HttpResponse? = null 54 | ): DavException = DavException(message, ex, createExceptionData(httpResponse)) 55 | 56 | @OptIn(InternalAPI::class) 57 | internal suspend fun createExceptionData( 58 | httpResponse: HttpResponse? = null 59 | ): ExceptionData { 60 | var response: String? = null 61 | var request: String? = null 62 | var requestBody: String? = null 63 | var responseBody: String? = null 64 | var errors = emptyList() 65 | 66 | if (httpResponse != null) { 67 | response = httpResponse.toString() 68 | Dav4jvm.log.trace("Reading request") 69 | try { 70 | request = httpResponse.request.toString() 71 | 72 | httpResponse.request.content.let { body -> 73 | httpResponse.request.contentType()?.let { type -> 74 | if (isPlainText(type)) { 75 | requestBody = when (body) { 76 | is OutgoingContent.ByteArrayContent -> { 77 | val bytes = body.bytes() 78 | bytes.copyOfRange(0, min(MAX_EXCERPT_SIZE, bytes.size)).decodeToString() 79 | } 80 | 81 | is OutgoingContent.ReadChannelContent -> body.readFrom() 82 | .readRemaining(MAX_EXCERPT_SIZE.toLong()).readText() 83 | 84 | else -> "Unknown outgoing content ${body::class.simpleName}" 85 | } 86 | } 87 | } 88 | } 89 | } catch (e: Exception) { 90 | Dav4jvm.log.warn("Couldn't read HTTP request", e) 91 | requestBody = "Couldn't read HTTP request: ${e.message}" 92 | } 93 | Dav4jvm.log.trace("Reading response $response") 94 | try { 95 | // save response body excerpt 96 | val bodyChannel = httpResponse.content 97 | val contentType = httpResponse.contentType() 98 | if (!bodyChannel.isEmpty() && contentType != null && isPlainText(contentType)) { 99 | // Read a length limited version of the body 100 | val read = bodyChannel.readRemaining(MAX_EXCERPT_SIZE.toLong()) 101 | responseBody = read.readBytes().decodeToString() 102 | if (contentType.match(ContentType.Application.Xml) || contentType.match(ContentType.Text.Xml)) { 103 | val xmlBody = responseBody + bodyChannel.readRemaining().readBytes().decodeToString() 104 | try { 105 | val parser = XmlUtils.createReader(xmlBody) 106 | XmlUtils.processTag(parser, name = Error.NAME) { 107 | errors = Error.parseError(parser) 108 | } 109 | } catch (e: XmlException) { 110 | Dav4jvm.log.warn("Couldn't parse XML response", e) 111 | } 112 | } 113 | } 114 | } catch (e: IOException) { 115 | Dav4jvm.log.warn("Couldn't read HTTP response", e) 116 | responseBody = "Couldn't read HTTP response: ${e.message}" 117 | } 118 | } else 119 | response = null 120 | return ExceptionData(request, response, requestBody, responseBody, errors) 121 | } 122 | 123 | } 124 | 125 | val request: String? = exceptionData.request 126 | val response: String? = exceptionData.response 127 | 128 | /** 129 | * Body excerpt of [request] (up to [MAX_EXCERPT_SIZE] characters). Only available 130 | * if the HTTP request body was textual content and could be read again. 131 | */ 132 | val requestBody: String? = exceptionData.requestBody 133 | 134 | /** 135 | * Body excerpt of [response] (up to [MAX_EXCERPT_SIZE] characters). Only available 136 | * if the HTTP response body was textual content. 137 | */ 138 | val responseBody: String? = exceptionData.responseBody 139 | 140 | /** 141 | * Precondition/postcondition XML elements which have been found in the XML response. 142 | */ 143 | val errors: List = exceptionData.errors 144 | 145 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/exception/DavExceptionTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm.exception 8 | 9 | import at.bitfire.dav4jvm.* 10 | import at.bitfire.dav4jvm.property.ResourceType 11 | import io.kotest.core.spec.style.FunSpec 12 | import io.kotest.matchers.collections.shouldBeEmpty 13 | import io.kotest.matchers.shouldBe 14 | import io.kotest.matchers.string.shouldContain 15 | import io.kotest.matchers.string.shouldHaveLength 16 | import io.ktor.client.engine.mock.* 17 | import io.ktor.client.request.* 18 | import io.ktor.http.* 19 | import io.ktor.http.content.* 20 | import io.ktor.util.* 21 | import io.ktor.utils.io.charsets.* 22 | import nl.adaptivity.xmlutil.QName 23 | import kotlin.test.assertEquals 24 | import kotlin.test.assertNull 25 | import kotlin.test.assertTrue 26 | import kotlin.test.fail 27 | 28 | 29 | @OptIn(InternalAPI::class) 30 | object DavExceptionTest : FunSpec({ 31 | val sampleText = "SAMPLE RESPONSE" 32 | 33 | val sampleUrl = Url("http://mock-server.com/dav/") 34 | 35 | val httpClient = createMockClient() 36 | 37 | /** 38 | * Test truncation of a too large plain text request in [DavException]. 39 | */ 40 | test("testRequestLargeTextError") { 41 | val url = sampleUrl 42 | val dav = DavResource(httpClient, url) 43 | 44 | val builder = StringBuilder() 45 | builder.append(CharArray(DavException.MAX_EXCERPT_SIZE + 100) { '*' }) 46 | val body = builder.toString() 47 | 48 | val response = httpClient.createResponse( 49 | buildRequest { 50 | url("http://example.com") 51 | method = HttpMethod.Post 52 | setBody(TextContent(body, ContentType.Text.Plain)) 53 | header(HttpHeaders.ContentType, ContentType.Text.Plain.toString()) 54 | }, 55 | HttpStatusCode.NoContent 56 | ) 57 | 58 | val e = DavException("Error with large request body", null, response) 59 | 60 | assertTrue(e.errors.isEmpty()) 61 | assertEquals( 62 | body.substring(0, DavException.MAX_EXCERPT_SIZE), 63 | e.requestBody 64 | ) 65 | } 66 | 67 | /** 68 | * Test a large HTML response which has a multi-octet UTF-8 character 69 | * exactly at the cut-off position. 70 | */ 71 | test("testResponseLargeTextError") { 72 | val url = sampleUrl 73 | val dav = DavResource(httpClient, url) 74 | 75 | val builder = StringBuilder() 76 | builder.append(CharArray(DavException.MAX_EXCERPT_SIZE - 1) { '*' }) 77 | builder.append("\u03C0") // Pi 78 | val body = builder.toString() 79 | 80 | httpClient.changeMockHandler { request -> 81 | if (request.url == sampleUrl) { 82 | respond( 83 | body, 84 | HttpStatusCode.NotFound, 85 | headersOf(HttpHeaders.ContentType, ContentType.Text.Html.toString()) 86 | ) 87 | } else { 88 | respondError(HttpStatusCode.BadRequest) 89 | } 90 | } 91 | 92 | try { 93 | dav.propfind(0, ResourceType.NAME) { _, _ -> } 94 | fail("Expected HttpException") 95 | } catch (e: HttpException) { 96 | e.code.shouldBe(404) 97 | e.errors.shouldBeEmpty() 98 | e.responseBody.shouldHaveLength(DavException.MAX_EXCERPT_SIZE) 99 | e.responseBody!!.substring(0, DavException.MAX_EXCERPT_SIZE - 1) 100 | .shouldBe(body.substring(0, DavException.MAX_EXCERPT_SIZE - 1)) 101 | } 102 | } 103 | 104 | test("testResponseNonTextError") { 105 | val url = sampleUrl 106 | val dav = DavResource(httpClient, url) 107 | 108 | httpClient.changeMockHandler { request -> 109 | if (request.url == sampleUrl) { 110 | respond( 111 | "12345", 112 | HttpStatusCode.Forbidden, 113 | headersOf(HttpHeaders.ContentType, ContentType.Application.OctetStream.toString()) 114 | ) 115 | } else { 116 | respondError(HttpStatusCode.BadRequest) 117 | } 118 | } 119 | try { 120 | dav.propfind(0, ResourceType.NAME) { _, _ -> } 121 | fail("Expected HttpException") 122 | } catch (e: HttpException) { 123 | assertEquals(e.code, 403) 124 | assertTrue(e.errors.isEmpty()) 125 | assertNull(e.responseBody) 126 | } 127 | } 128 | 129 | test("testSerialization") { 130 | val url = sampleUrl 131 | val dav = DavResource(httpClient, url) 132 | 133 | httpClient.changeMockHandler { request -> 134 | if (request.url == sampleUrl) { 135 | respond( 136 | "12345", 137 | HttpStatusCode.InternalServerError, 138 | headersOf(HttpHeaders.ContentType, ContentType.Text.Plain.toString()) 139 | ) 140 | } else { 141 | respondError(HttpStatusCode.BadRequest) 142 | } 143 | } 144 | try { 145 | dav.propfind(0, ResourceType.NAME) { _, _ -> } 146 | fail("Expected DavException") 147 | } catch (e: DavException) { 148 | val response = httpClient.lastMockResponse 149 | 150 | assertEquals(HttpStatusCode.InternalServerError, response.statusCode) 151 | e.responseBody.shouldContain("12345") 152 | } 153 | } 154 | 155 | /** 156 | * Test precondition XML element (sample from RFC 4918 16) 157 | */ 158 | test("testXmlError") { 159 | val url = sampleUrl 160 | val dav = DavResource(httpClient, url) 161 | 162 | val body = "\n" + 163 | "\n" + 164 | " \n" + 165 | " /workspace/webdav/\n" + 166 | " \n" + 167 | "\n" 168 | httpClient.changeMockHandler { request -> 169 | if (request.url == sampleUrl) { 170 | respond( 171 | body, 172 | HttpStatusCode.Locked, 173 | headersOf( 174 | HttpHeaders.ContentType, 175 | ContentType.Application.Xml.withCharset(Charsets.UTF_8).toString() 176 | ) 177 | ) 178 | } else { 179 | respondError(HttpStatusCode.BadRequest) 180 | } 181 | } 182 | try { 183 | dav.propfind(0, ResourceType.NAME) { _, _ -> } 184 | fail("Expected HttpException") 185 | } catch (e: HttpException) { 186 | assertEquals(e.code, 423) 187 | assertTrue(e.errors.any { it.name == QName(XmlUtils.NS_WEBDAV, "lock-token-submitted") }) 188 | assertEquals(body, e.responseBody) 189 | } 190 | } 191 | 192 | }) -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/DavCalendar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import at.bitfire.dav4jvm.XmlUtils.insertTag 10 | import at.bitfire.dav4jvm.exception.DavException 11 | import at.bitfire.dav4jvm.exception.HttpException 12 | import at.bitfire.dav4jvm.property.CalendarData 13 | import at.bitfire.dav4jvm.property.GetContentType 14 | import at.bitfire.dav4jvm.property.GetETag 15 | import at.bitfire.dav4jvm.property.ScheduleTag 16 | import io.ktor.client.* 17 | import io.ktor.client.request.* 18 | import io.ktor.http.* 19 | import io.ktor.util.logging.* 20 | import io.ktor.utils.io.charsets.* 21 | import korlibs.time.Date 22 | import korlibs.time.DateFormat 23 | import korlibs.time.format 24 | import nl.adaptivity.xmlutil.QName 25 | import kotlin.jvm.JvmOverloads 26 | 27 | class DavCalendar @JvmOverloads constructor( 28 | httpClient: HttpClient, 29 | location: Url, 30 | log: Logger = Dav4jvm.log 31 | ) : DavCollection(httpClient, location, log) { 32 | 33 | companion object { 34 | val MIME_ICALENDAR = ContentType("text", "calendar") 35 | val MIME_ICALENDAR_UTF8 = MIME_ICALENDAR.withCharset(Charsets.UTF_8) 36 | 37 | val CALENDAR_QUERY = QName(XmlUtils.NS_CALDAV, "calendar-query") 38 | val CALENDAR_MULTIGET = QName(XmlUtils.NS_CALDAV, "calendar-multiget") 39 | 40 | val FILTER = QName(XmlUtils.NS_CALDAV, "filter") 41 | val COMP_FILTER = QName(XmlUtils.NS_CALDAV, "comp-filter") 42 | const val COMP_FILTER_NAME = "name" 43 | val TIME_RANGE = QName(XmlUtils.NS_CALDAV, "time-range") 44 | const val TIME_RANGE_START = "start" 45 | const val TIME_RANGE_END = "end" 46 | 47 | private val timeFormatUTC = DateFormat("yyyyMMdd'T'HHmmssZ") 48 | } 49 | 50 | 51 | /** 52 | * Sends a calendar-query REPORT to the resource. 53 | * 54 | * @param component requested component name (like VEVENT or VTODO) 55 | * @param start time-range filter: start date (optional) 56 | * @param end time-range filter: end date (optional) 57 | * @param callback called for every WebDAV response XML element in the result 58 | * 59 | * @return list of properties which have been received in the Multi-Status response, but 60 | * are not part of response XML elements 61 | * 62 | * @throws IOException on I/O error 63 | * @throws HttpException on HTTP error 64 | * @throws DavException on WebDAV error 65 | */ 66 | suspend fun calendarQuery( 67 | component: String, 68 | start: Date?, 69 | end: Date?, 70 | callback: MultiResponseCallback 71 | ): List { 72 | /* 75 | 76 | 78 | 79 | name value: a calendar object or calendar component 80 | type (e.g., VEVENT) 81 | 82 | */ 83 | val writer = StringBuilder() 84 | val serializer = XmlUtils.createWriter(writer) 85 | serializer.startDocument(encoding = "UTF-8") 86 | serializer.setPrefix("", XmlUtils.NS_WEBDAV) 87 | serializer.setPrefix("CAL", XmlUtils.NS_CALDAV) 88 | serializer.insertTag(CALENDAR_QUERY) { 89 | insertTag(PROP) { 90 | insertTag(GetETag.NAME) 91 | } 92 | insertTag(FILTER) { 93 | insertTag(COMP_FILTER) { 94 | attribute(null, COMP_FILTER_NAME, null, "VCALENDAR") 95 | insertTag(COMP_FILTER) { 96 | attribute(null, COMP_FILTER_NAME, null, component) 97 | if (start != null || end != null) { 98 | insertTag(TIME_RANGE) { 99 | if (start != null) 100 | attribute(null, TIME_RANGE_START, null, timeFormatUTC.format(start)) 101 | if (end != null) 102 | attribute(null, TIME_RANGE_END, null, timeFormatUTC.format(end)) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | serializer.endDocument() 110 | 111 | //TODO followRedirects { 112 | val response = httpClient.prepareRequest { 113 | url(location) 114 | method = Report 115 | setBody(writer.toString()) 116 | header(HttpHeaders.ContentType, MIME_XML) 117 | header("Depth", "1") 118 | }.execute() 119 | return processMultiStatus(response, callback) 120 | } 121 | 122 | /** 123 | * Sends a calendar-multiget REPORT to the resource. Received responses are sent 124 | * to the callback, whether they are successful (2xx) or not. 125 | * 126 | * @param urls list of iCalendar URLs to be requested 127 | * @param contentType MIME type of requested format; may be "text/calendar" for iCalendar or 128 | * "application/calendar+json" for jCard. *null*: don't request specific representation type 129 | * @param version Version subtype of the requested format, like "2.0" for iCalendar 2. *null*: don't request specific version 130 | * @param callback called for every WebDAV response XML element in the result 131 | * 132 | * @return list of properties which have been received in the Multi-Status response, but 133 | * are not part of response XML elements 134 | * 135 | * @throws IOException on I/O error 136 | * @throws HttpException on HTTP error 137 | * @throws DavException on WebDAV error 138 | */ 139 | suspend fun multiget( 140 | urls: List, 141 | contentType: String? = null, 142 | version: String? = null, 143 | callback: MultiResponseCallback 144 | ): List { 145 | /* 148 | */ 149 | val writer = StringBuilder() 150 | val serializer = XmlUtils.createWriter(writer) 151 | serializer.startDocument(encoding = "UTF-8") 152 | serializer.setPrefix("", XmlUtils.NS_WEBDAV) 153 | serializer.setPrefix("CAL", XmlUtils.NS_CALDAV) 154 | serializer.insertTag(CALENDAR_MULTIGET) { 155 | insertTag(PROP) { 156 | insertTag(GetContentType.NAME) // to determine the character set 157 | insertTag(GetETag.NAME) 158 | insertTag(ScheduleTag.NAME) 159 | insertTag(CalendarData.NAME) { 160 | if (contentType != null) 161 | attribute(null, CalendarData.CONTENT_TYPE, null, contentType) 162 | if (version != null) 163 | attribute(null, CalendarData.VERSION, null, version) 164 | } 165 | } 166 | for (url in urls) 167 | insertTag(HREF) { 168 | serializer.text(url.encodedPath) 169 | } 170 | } 171 | serializer.endDocument() 172 | 173 | //TODO followRedirects { 174 | val response = httpClient.prepareRequest { 175 | url(location) 176 | method = Report 177 | setBody(writer.toString()) 178 | header(HttpHeaders.ContentType, MIME_XML) 179 | }.execute() 180 | return processMultiStatus(response, callback) 181 | 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/at/bitfire/dav4jvm/Response.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Ricki Hirner (bitfire web engineering). 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the GNU Public License v3.0 5 | * which accompanies this distribution, and is available at 6 | * http://www.gnu.org/licenses/gpl.html 7 | */ 8 | 9 | package at.bitfire.dav4jvm 10 | 11 | import at.bitfire.dav4jvm.Dav4jvm.log 12 | import at.bitfire.dav4jvm.XmlUtils.nextText 13 | import at.bitfire.dav4jvm.property.ResourceType 14 | import io.ktor.http.* 15 | import nl.adaptivity.xmlutil.QName 16 | import nl.adaptivity.xmlutil.XmlReader 17 | import kotlin.reflect.KClass 18 | 19 | /** 20 | * Represents a WebDAV response XML Element. 21 | * 22 | * 24 | */ 25 | data class Response( 26 | /** 27 | * URL of the requested resource. For instance, if `this` is a result 28 | * of a PROPFIND request, the `requestedUrl` would be the URL where the 29 | * PROPFIND request has been sent to (usually the collection URL). 30 | */ 31 | val requestedUrl: Url, 32 | 33 | /** 34 | * URL of this response (`href` element) 35 | */ 36 | val href: Url, 37 | 38 | /** 39 | * status of this response (`status` XML element) 40 | */ 41 | val status: StatusLine?, 42 | 43 | /** 44 | * property/status elements (`propstat` XML elements) 45 | */ 46 | val propstat: List, 47 | 48 | /** 49 | * list of precondition/postcondition elements (`error` XML elements) 50 | */ 51 | val error: List? = null, 52 | 53 | /** 54 | * new location of this response (`location` XML element), used for redirects 55 | */ 56 | val newLocation: Url? = null 57 | ) { 58 | 59 | enum class HrefRelation { 60 | SELF, MEMBER, OTHER 61 | } 62 | 63 | /** 64 | * All properties from propstat elements with empty status or status code 2xx. 65 | */ 66 | val properties: List by lazy { 67 | if (isSuccess()) 68 | propstat.filter { it.isSuccess() }.map { it.properties }.flatten() 69 | else 70 | emptyList() 71 | } 72 | 73 | /** 74 | * Convenience method to get a certain property with empty status or status code 2xx 75 | * from the current response. 76 | */ 77 | inline operator fun get(clazz: KClass) = 78 | properties.filterIsInstance().firstOrNull() 79 | 80 | /** 81 | * Returns whether the request was successful. 82 | * 83 | * @return true: no status XML element or status code 2xx; false: otherwise 84 | */ 85 | fun isSuccess() = status?.status?.isSuccess() ?: true 86 | 87 | /** 88 | * Returns the name (last path segment) of the resource. 89 | */ 90 | fun hrefName() = HttpUtils.fileName(href) 91 | 92 | 93 | companion object { 94 | 95 | val RESPONSE = QName(XmlUtils.NS_WEBDAV, "response") 96 | val MULTISTATUS = QName(XmlUtils.NS_WEBDAV, "multistatus") 97 | val STATUS = QName(XmlUtils.NS_WEBDAV, "status") 98 | val LOCATION = QName(XmlUtils.NS_WEBDAV, "location") 99 | 100 | /** 101 | * Parses an XML response element. 102 | */ 103 | fun parse(parser: XmlReader, location: Url, callback: MultiResponseCallback) { 104 | 105 | var href: Url? = null 106 | var status: StatusLine? = null 107 | val propStat = mutableListOf() 108 | var error: List? = null 109 | var newLocation: Url? = null 110 | 111 | XmlUtils.processTag(parser) { 112 | when (parser.name) { 113 | DavResource.HREF -> { 114 | var sHref = parser.nextText() 115 | if (!sHref.startsWith("/")) { 116 | /* According to RFC 4918 8.3 URL Handling, only absolute paths are allowed as relative 117 | URLs. However, some servers reply with relative paths. */ 118 | val firstColon = sHref.indexOf(':') 119 | if (firstColon != -1) { 120 | /* There are some servers which return not only relative paths, but relative paths like "a:b.vcf", 121 | which would be interpreted as scheme: "a", scheme-specific part: "b.vcf" normally. 122 | For maximum compatibility, we prefix all relative paths which contain ":" (but not "://"), 123 | with the root path to allow resolving by Url. */ 124 | var hierarchical = false 125 | try { 126 | if (sHref.substring(firstColon, firstColon + 3) == "://") 127 | hierarchical = true 128 | } catch (e: IndexOutOfBoundsException) { 129 | // no "://" 130 | } 131 | if (!hierarchical) 132 | sHref = "/${location.encodedPath.drop(1)}$sHref" 133 | } 134 | } 135 | href = URLBuilder(location).takeFrom(sHref).build() 136 | } 137 | 138 | STATUS -> 139 | status = try { 140 | StatusLine.parse(parser.nextText()) 141 | } catch (e: IllegalStateException) { 142 | log.warn("Invalid status line, treating as HTTP error 500") 143 | StatusLine(HttpProtocolVersion.HTTP_1_1, HttpStatusCode(500, "Invalid status line")) 144 | } 145 | 146 | PropStat.NAME -> 147 | PropStat.parse(parser).let { propStat += it } 148 | 149 | Error.NAME -> 150 | error = Error.parseError(parser) 151 | 152 | LOCATION -> 153 | //TODO are invalid urls possible? 154 | newLocation = Url(parser.nextText()) 155 | } 156 | } 157 | 158 | if (href == null) { 159 | log.warn("Ignoring XML response element without valid href") 160 | return 161 | } 162 | 163 | // if we know this resource is a collection, make sure href has a trailing slash 164 | // (for clarity and resolving relative paths) 165 | propStat.filter { it.isSuccess() } 166 | .map { it.properties } 167 | .filterIsInstance() 168 | .firstOrNull() 169 | ?.let { type -> 170 | if (type.types.contains(ResourceType.COLLECTION)) 171 | href = UrlUtils.withTrailingSlash(href!!) 172 | } 173 | 174 | //log.log(Level.FINE, "Received properties for $href", if (status != null) status else propStat) 175 | 176 | // Which resource does this represent? 177 | val relation = when { 178 | UrlUtils.equals(UrlUtils.omitTrailingSlash(href!!), UrlUtils.omitTrailingSlash(location)) -> 179 | HrefRelation.SELF 180 | 181 | else -> { 182 | if (location.protocol == href!!.protocol && location.host == href!!.host && location.port == href!!.port) { 183 | val locationSegments = location.pathSegments 184 | val hrefSegments = href!!.pathSegments 185 | 186 | // don't compare trailing slash segment ("") 187 | var nBasePathSegments = locationSegments.size 188 | if (locationSegments[nBasePathSegments - 1] == "") 189 | nBasePathSegments-- 190 | 191 | /* example: locationSegments = [ "davCollection", "" ] 192 | nBasePathSegments = 1 193 | hrefSegments = [ "davCollection", "aMember" ] 194 | */ 195 | var relation = HrefRelation.OTHER 196 | if (hrefSegments.size > nBasePathSegments) { 197 | val sameBasePath = 198 | (0 until nBasePathSegments).none { locationSegments[it] != hrefSegments[it] } 199 | if (sameBasePath) 200 | relation = HrefRelation.MEMBER 201 | } 202 | 203 | relation 204 | } else 205 | HrefRelation.OTHER 206 | } 207 | } 208 | 209 | callback.onResponse( 210 | Response( 211 | location, 212 | href!!, 213 | status, 214 | propStat, 215 | error, 216 | newLocation 217 | ), 218 | relation 219 | ) 220 | } 221 | 222 | } 223 | 224 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/BasicDigestAuthHandlerTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | //TODO 10 | 11 | /*class BasicDigestAuthHandlerTest { 12 | 13 | @Test 14 | fun testBasic() { 15 | var authenticator = BasicDigestAuthHandler(null, "user", "password") 16 | val original = Request.Builder() 17 | .url("http://example.com") 18 | .build() 19 | var response = Builder() 20 | .request(original) 21 | .protocol(Protocol.HTTP_1_1) 22 | .code(401).message("Authentication required") 23 | .header("WWW-Authenticate", "Basic realm=\"WallyWorld\"") 24 | .build() 25 | var request = authenticator.authenticateRequest(original, response) 26 | assertEquals("Basic dXNlcjpwYXNzd29yZA==", request!!.header("Authorization")) 27 | 28 | // special characters: always use UTF-8 (and don't crash on RFC 7617 charset header) 29 | authenticator = BasicDigestAuthHandler(null, "username", "paßword") 30 | response = response.newBuilder() 31 | .header("WWW-Authenticate", "Basic realm=\"WallyWorld\",charset=UTF-8") 32 | .build() 33 | request = authenticator.authenticateRequest(original, response) 34 | assertEquals("Basic dXNlcm5hbWU6cGHDn3dvcmQ=", request!!.header("Authorization")) 35 | } 36 | 37 | @Test 38 | fun testDigestRFCExample() { 39 | // use cnonce from example 40 | val authenticator = BasicDigestAuthHandler(null, "Mufasa", "Circle Of Life") 41 | BasicDigestAuthHandler.clientNonce = "0a4f113b" 42 | BasicDigestAuthHandler.nonceCount.set(1) 43 | 44 | // construct WWW-Authenticate 45 | val authScheme = Challenge("Digest", mapOf( 46 | Pair("realm", "testrealm@host.com"), 47 | Pair("qop", "auth"), 48 | Pair("nonce", "dcd98b7102dd2f0e8b11d0f600bfb0c093"), 49 | Pair("opaque", "5ccc069c403ebaf9f0171e9517f40e41") 50 | )) 51 | 52 | val original = Request.Builder() 53 | .get() 54 | .url("http://www.nowhere.org/dir/index.html") 55 | .build() 56 | val request = authenticator.digestRequest(original, authScheme) 57 | val auth = request!!.header("Authorization") 58 | assertTrue(auth!!.contains("username=\"Mufasa\"")) 59 | assertTrue(auth.contains("realm=\"testrealm@host.com\"")) 60 | assertTrue(auth.contains("nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\"")) 61 | assertTrue(auth.contains("uri=\"/dir/index.html\"")) 62 | assertTrue(auth.contains("qop=auth")) 63 | assertTrue(auth.contains("nc=00000001")) 64 | assertTrue(auth.contains("cnonce=\"0a4f113b\"")) 65 | assertTrue(auth.contains("response=\"6629fae49393a05397450978507c4ef1\"")) 66 | assertTrue(auth.contains("opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"")) 67 | } 68 | 69 | @Test 70 | fun testDigestRealWorldExamples() { 71 | var authenticator = BasicDigestAuthHandler(null, "demo", "demo") 72 | BasicDigestAuthHandler.clientNonce = "MDI0ZDgxYTNmZDk4MTA1ODM0NDNjNmJjNDllYjQ1ZTI=" 73 | BasicDigestAuthHandler.nonceCount.set(1) 74 | 75 | // example 1 76 | var authScheme = Challenge("Digest", mapOf( 77 | Pair("realm", "Group-Office"), 78 | Pair("qop", "auth"), 79 | Pair("nonce", "56212407212c8"), 80 | Pair("opaque", "df58bdff8cf60599c939187d0b5c54de") 81 | )) 82 | 83 | var original = Request.Builder() 84 | .method("PROPFIND", null) 85 | .url("https://demo.group-office.eu/caldav/") 86 | .build() 87 | var request = authenticator.digestRequest(original, authScheme) 88 | var auth = request!!.header("Authorization") 89 | assertTrue(auth!!.contains("username=\"demo\"")) 90 | assertTrue(auth.contains("realm=\"Group-Office\"")) 91 | assertTrue(auth.contains("nonce=\"56212407212c8\"")) 92 | assertTrue(auth.contains("uri=\"/caldav/\"")) 93 | assertTrue(auth.contains("cnonce=\"MDI0ZDgxYTNmZDk4MTA1ODM0NDNjNmJjNDllYjQ1ZTI=\"")) 94 | assertTrue(auth.contains("nc=00000001")) 95 | assertTrue(auth.contains("qop=auth")) 96 | assertTrue(auth.contains("response=\"de3b3b194d85ddc62537208c9c3637dc\"")) 97 | assertTrue(auth.contains("opaque=\"df58bdff8cf60599c939187d0b5c54de\"")) 98 | 99 | // example 2 100 | authenticator = BasicDigestAuthHandler(null, "test", "test") 101 | authScheme = Challenge("digest", mapOf( // lower case 102 | Pair("nonce", "87c4c2aceed9abf30dd68c71"), 103 | Pair("algorithm", "md5"), 104 | Pair("opaque", "571609eb7058505d35c7bf7288fbbec4-ODdjNGMyYWNlZWQ5YWJmMzBkZDY4YzcxLDAuMC4wLjAsMTQ0NTM3NzE0Nw=="), 105 | Pair("realm", "ieddy.ru") 106 | )) 107 | original = Request.Builder() 108 | .method("OPTIONS", null) 109 | .url("https://ieddy.ru/") 110 | .build() 111 | request = authenticator.digestRequest(original, authScheme) 112 | auth = request!!.header("Authorization") 113 | assertTrue(auth!!.contains("algorithm=\"MD5\"")) // some servers require it 114 | assertTrue(auth.contains("username=\"test\"")) 115 | assertTrue(auth.contains("realm=\"ieddy.ru\"")) 116 | assertTrue(auth.contains("nonce=\"87c4c2aceed9abf30dd68c71\"")) 117 | assertTrue(auth.contains("uri=\"/\"")) 118 | assertFalse(auth.contains("cnonce=")) 119 | assertFalse(auth.contains("nc=00000001")) 120 | assertFalse(auth.contains("qop=")) 121 | assertTrue(auth.contains("response=\"d42a39f25f80b0d6907286a960ff9c7d\"")) 122 | assertTrue(auth.contains("opaque=\"571609eb7058505d35c7bf7288fbbec4-ODdjNGMyYWNlZWQ5YWJmMzBkZDY4YzcxLDAuMC4wLjAsMTQ0NTM3NzE0Nw==\"")) 123 | } 124 | 125 | @Test 126 | fun testDigestMD5Sess() { 127 | val authenticator = BasicDigestAuthHandler(null, "admin", "12345") 128 | BasicDigestAuthHandler.clientNonce = "hxk1lu63b6c7vhk" 129 | BasicDigestAuthHandler.nonceCount.set(1) 130 | 131 | val authScheme = Challenge("Digest", mapOf( 132 | Pair("realm", "MD5-sess Example"), 133 | Pair("qop", "auth"), 134 | Pair("algorithm", "MD5-sess"), 135 | Pair("nonce", "dcd98b7102dd2f0e8b11d0f600bfb0c093"), 136 | Pair("opaque", "5ccc069c403ebaf9f0171e9517f40e41") 137 | )) 138 | 139 | /* A1 = h("admin:MD5-sess Example:12345"):dcd98b7102dd2f0e8b11d0f600bfb0c093:hxk1lu63b6c7vhk = 140 | 4eaed818bc587129e73b39c8d3e8425a:dcd98b7102dd2f0e8b11d0f600bfb0c093:hxk1lu63b6c7vhk a994ee9d33e2f077d3a6e13e882f6686 141 | A2 = POST:/plain.txt 1b557703454e1aa1230c5523f54380ed 142 | 143 | h("a994ee9d33e2f077d3a6e13e882f6686:dcd98b7102dd2f0e8b11d0f600bfb0c093:00000001:hxk1lu63b6c7vhk:auth:1b557703454e1aa1230c5523f54380ed") = 144 | af2a72145775cfd08c36ad2676e89446 145 | */ 146 | 147 | val original = Request.Builder() 148 | .method("POST", "PLAIN TEXT".toRequestBody("text/plain".toMediaType())) 149 | .url("http://example.com/plain.txt") 150 | .build() 151 | val request = authenticator.digestRequest(original, authScheme) 152 | val auth = request!!.header("Authorization") 153 | assertTrue(auth!!.contains("username=\"admin\"")) 154 | assertTrue(auth.contains("realm=\"MD5-sess Example\"")) 155 | assertTrue(auth.contains("nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\"")) 156 | assertTrue(auth.contains("uri=\"/plain.txt\"")) 157 | assertTrue(auth.contains("cnonce=\"hxk1lu63b6c7vhk\"")) 158 | assertTrue(auth.contains("nc=00000001")) 159 | assertTrue(auth.contains("qop=auth")) 160 | assertTrue(auth.contains("response=\"af2a72145775cfd08c36ad2676e89446\"")) 161 | assertTrue(auth.contains("opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"")) 162 | } 163 | 164 | @Test 165 | fun testDigestMD5AuthInt() { 166 | val authenticator = BasicDigestAuthHandler(null, "admin", "12435") 167 | BasicDigestAuthHandler.clientNonce = "hxk1lu63b6c7vhk" 168 | BasicDigestAuthHandler.nonceCount.set(1) 169 | 170 | val authScheme = Challenge("Digest", mapOf( 171 | Pair("realm", "AuthInt Example"), 172 | Pair("qop", "auth-int"), 173 | Pair("nonce", "367sj3265s5"), 174 | Pair("opaque", "87aaxcval4gba36") 175 | )) 176 | 177 | /* A1 = admin:AuthInt Example:12345 380dc3fc1305127cd2aa81ab68ef3f34 178 | 179 | h("PLAIN TEXT") = 20296edbd4c4275fb416b64e4be752f9 180 | A2 = POST:/plain.txt:20296edbd4c4275fb416b64e4be752f9 a71c4c86e18b3993ffc98c6e426fe4b0 181 | 182 | h(380dc3fc1305127cd2aa81ab68ef3f34:367sj3265s5:00000001:hxk1lu63b6c7vhk:auth-int:a71c4c86e18b3993ffc98c6e426fe4b0) = 183 | 81d07cb3b8d412b34144164124c970cb 184 | */ 185 | 186 | val original = Request.Builder() 187 | .method("POST", "PLAIN TEXT".toRequestBody("text/plain".toMediaType())) 188 | .url("http://example.com/plain.txt") 189 | .build() 190 | val request = authenticator.digestRequest(original, authScheme) 191 | val auth = request!!.header("Authorization") 192 | assertTrue(auth!!.contains("username=\"admin\"")) 193 | assertTrue(auth.contains("realm=\"AuthInt Example\"")) 194 | assertTrue(auth.contains("nonce=\"367sj3265s5\"")) 195 | assertTrue(auth.contains("uri=\"/plain.txt\"")) 196 | assertTrue(auth.contains("cnonce=\"hxk1lu63b6c7vhk\"")) 197 | assertTrue(auth.contains("nc=00000001")) 198 | assertTrue(auth.contains("qop=auth-int")) 199 | assertTrue(auth.contains("response=\"5ab6822b9d906cc711760a7783b28dca\"")) 200 | assertTrue(auth.contains("opaque=\"87aaxcval4gba36\"")) 201 | } 202 | 203 | @Test 204 | fun testDigestLegacy() { 205 | val authenticator = BasicDigestAuthHandler(null, "Mufasa", "CircleOfLife") 206 | 207 | // construct WWW-Authenticate 208 | val authScheme = Challenge("Digest", mapOf( 209 | Pair("realm", "testrealm@host.com"), 210 | Pair("nonce", "dcd98b7102dd2f0e8b11d0f600bfb0c093"), 211 | Pair("opaque", "5ccc069c403ebaf9f0171e9517f40e41") 212 | )) 213 | 214 | val original = Request.Builder() 215 | .get() 216 | .url("http://www.nowhere.org/dir/index.html") 217 | .build() 218 | val request = authenticator.digestRequest(original, authScheme) 219 | val auth = request!!.header("Authorization") 220 | assertTrue(auth!!.contains("username=\"Mufasa\"")) 221 | assertTrue(auth.contains("realm=\"testrealm@host.com\"")) 222 | assertTrue(auth.contains("nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\"")) 223 | assertTrue(auth.contains("uri=\"/dir/index.html\"")) 224 | assertFalse(auth.contains("qop=")) 225 | assertFalse(auth.contains("nc=")) 226 | assertFalse(auth.contains("cnonce=")) 227 | assertTrue(auth.contains("response=\"1949323746fe6a43ef61f9606e7febea\"")) 228 | assertTrue(auth.contains("opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"")) 229 | } 230 | 231 | @Test 232 | fun testIncompleteAuthenticationRequests() { 233 | val authenticator = BasicDigestAuthHandler(null, "demo", "demo") 234 | 235 | val original = Request.Builder() 236 | .get() 237 | .url("http://www.nowhere.org/dir/index.html") 238 | .build() 239 | 240 | assertNull(authenticator.digestRequest(original, Challenge("Digest", mapOf()))) 241 | 242 | assertNull(authenticator.digestRequest(original, Challenge("Digest", mapOf( 243 | Pair("realm", "Group-Office") 244 | )))) 245 | 246 | assertNull(authenticator.digestRequest(original, Challenge("Digest", mapOf( 247 | Pair("realm", "Group-Office"), 248 | Pair("qop", "auth") 249 | )))) 250 | 251 | assertNotNull(authenticator.digestRequest(original, Challenge("Digest", mapOf( 252 | Pair("realm", "Group-Office"), 253 | Pair("qop", "auth"), 254 | Pair("nonce", "56212407212c8") 255 | )))) 256 | } 257 | 258 | @Test 259 | fun testAuthenticateNull() { 260 | val authenticator = BasicDigestAuthHandler(null, "demo", "demo") 261 | // must not crash (route may be null) 262 | val request = Request.Builder() 263 | .get() 264 | .url("http://example.com") 265 | .build() 266 | val response = Response.Builder() 267 | .request(request) 268 | .protocol(Protocol.HTTP_2) 269 | .code(200).message("OK") 270 | .build() 271 | authenticator.authenticate(null, response) 272 | } 273 | 274 | }*/ 275 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/at/bitfire/dav4jvm/DavCollectionTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Source Code Form is subject to the terms of the Mozilla Public 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | package at.bitfire.dav4jvm 8 | 9 | import at.bitfire.dav4jvm.exception.HttpException 10 | import at.bitfire.dav4jvm.property.GetETag 11 | import at.bitfire.dav4jvm.property.SyncToken 12 | import io.kotest.core.spec.style.FunSpec 13 | import io.ktor.client.engine.mock.* 14 | import io.ktor.http.* 15 | import io.ktor.utils.io.charsets.* 16 | import nl.adaptivity.xmlutil.QName 17 | import kotlin.test.assertEquals 18 | import kotlin.test.assertFalse 19 | import kotlin.test.assertTrue 20 | import kotlin.test.fail 21 | 22 | object DavCollectionTest : FunSpec({ 23 | 24 | val sampleText = "SAMPLE RESPONSE" 25 | val sampleUrl = Url("http://mock-server.com/dav/") 26 | val httpClient = createMockClient() 27 | 28 | 29 | /** 30 | * Test sample response for an initial sync-collection report from RFC 6578 3.8. 31 | */ 32 | test("testInitialSyncCollectionReport") { 33 | val url = sampleUrl 34 | httpClient.changeMockHandler { request -> 35 | if (request.url == sampleUrl) { 36 | respond( 37 | "\n" + 38 | " \n" + 39 | " \n" + 40 | " ${sampleUrl}test.doc\n" + 42 | " \n" + 43 | " \n" + 44 | " \"00001-abcd1\"\n" + 45 | " \n" + 46 | " Box type A\n" + 47 | " \n" + 48 | " \n" + 49 | " HTTP/1.1 200 OK\n" + 50 | " \n" + 51 | " \n" + 52 | " \n" + 53 | " ${sampleUrl}vcard.vcf\n" + 55 | " \n" + 56 | " \n" + 57 | " \"00002-abcd1\"\n" + 58 | " \n" + 59 | " HTTP/1.1 200 OK\n" + 60 | " \n" + 61 | " \n" + 62 | " \n" + 63 | " \n" + 64 | " \n" + 65 | " HTTP/1.1 404 Not Found\n" + 66 | " \n" + 67 | " \n" + 68 | " \n" + 69 | " ${sampleUrl}calendar.ics\n" + 71 | " \n" + 72 | " \n" + 73 | " \"00003-abcd1\"\n" + 74 | " \n" + 75 | " HTTP/1.1 200 OK\n" + 76 | " \n" + 77 | " \n" + 78 | " \n" + 79 | " \n" + 80 | " \n" + 81 | " HTTP/1.1 404 Not Found\n" + 82 | " \n" + 83 | " \n" + 84 | " http://example.com/ns/sync/1234\n" + 85 | " ", 86 | HttpStatusCode.MultiStatus, 87 | headersOf(HttpHeaders.ContentType, ContentType.Text.Xml.withCharset(Charsets.UTF_8).toString()) 88 | ) 89 | } else { 90 | respondError(HttpStatusCode.BadRequest) 91 | } 92 | } 93 | val collection = DavCollection(httpClient, url) 94 | 95 | var nrCalled = 0 96 | val result = collection.reportChanges(null, false, null, GetETag.NAME) { response, relation -> 97 | when (response.href) { 98 | url.resolve("/dav/test.doc") -> { 99 | assertTrue(response.isSuccess()) 100 | assertEquals(Response.HrefRelation.MEMBER, relation) 101 | val eTag = response[GetETag::class] 102 | assertEquals("00001-abcd1", eTag!!.eTag) 103 | assertFalse(eTag.weak) 104 | nrCalled++ 105 | } 106 | 107 | url.resolve("/dav/vcard.vcf") -> { 108 | assertTrue(response.isSuccess()) 109 | assertEquals(Response.HrefRelation.MEMBER, relation) 110 | val eTag = response[GetETag::class] 111 | assertEquals("00002-abcd1", eTag!!.eTag) 112 | assertFalse(eTag.weak) 113 | nrCalled++ 114 | } 115 | 116 | url.resolve("/dav/calendar.ics") -> { 117 | assertTrue(response.isSuccess()) 118 | assertEquals(Response.HrefRelation.MEMBER, relation) 119 | val eTag = response[GetETag::class] 120 | assertEquals("00003-abcd1", eTag!!.eTag) 121 | assertFalse(eTag.weak) 122 | nrCalled++ 123 | } 124 | } 125 | } 126 | assertEquals(3, nrCalled) 127 | assertEquals("http://example.com/ns/sync/1234", result.filterIsInstance().first().token) 128 | } 129 | 130 | /** 131 | * Test sample response for an initial sync-collection report with truncation from RFC 6578 3.10. 132 | */ 133 | test("testInitialSyncCollectionReportWithTruncation") { 134 | val url = sampleUrl 135 | httpClient.changeMockHandler { request -> 136 | if (request.url == sampleUrl) { 137 | respond( 138 | "\n" + 139 | " \n" + 140 | " \n" + 141 | " ${sampleUrl}test.doc\n" + 142 | " \n" + 143 | " \n" + 144 | " \"00001-abcd1\"\n" + 145 | " \n" + 146 | " HTTP/1.1 200 OK\n" + 147 | " \n" + 148 | " \n" + 149 | " \n" + 150 | " ${sampleUrl}vcard.vcf\n" + 151 | " \n" + 152 | " \n" + 153 | " \"00002-abcd1\"\n" + 154 | " \n" + 155 | " HTTP/1.1 200 OK\n" + 156 | " \n" + 157 | " \n" + 158 | " \n" + 159 | " ${sampleUrl}removed.txt\n" + 160 | " HTTP/1.1 404 Not Found\n" + 161 | " " + 162 | " \n" + 163 | " ${sampleUrl}\n" + 164 | " HTTP/1.1 507 Insufficient Storage\n" + 165 | " \n" + 166 | " " + 167 | " http://example.com/ns/sync/1233\n" + 168 | " ", 169 | HttpStatusCode.MultiStatus, 170 | headersOf(HttpHeaders.ContentType, ContentType.Text.Xml.withCharset(Charsets.UTF_8).toString()) 171 | ) 172 | } else { 173 | respondError(HttpStatusCode.BadRequest) 174 | } 175 | } 176 | 177 | val collection = DavCollection(httpClient, url) 178 | 179 | var nrCalled = 0 180 | val result = collection.reportChanges(null, false, null, GetETag.NAME) { response, relation -> 181 | when (response.href) { 182 | url.resolve("/dav/test.doc") -> { 183 | assertTrue(response.isSuccess()) 184 | assertEquals(Response.HrefRelation.MEMBER, relation) 185 | val eTag = response[GetETag::class] 186 | assertEquals("00001-abcd1", eTag?.eTag) 187 | assertTrue(eTag?.weak == false) 188 | nrCalled++ 189 | } 190 | 191 | url.resolve("/dav/vcard.vcf") -> { 192 | assertTrue(response.isSuccess()) 193 | assertEquals(Response.HrefRelation.MEMBER, relation) 194 | val eTag = response[GetETag::class] 195 | assertEquals("00002-abcd1", eTag?.eTag) 196 | assertTrue(eTag?.weak == false) 197 | nrCalled++ 198 | } 199 | 200 | url.resolve("/dav/removed.txt") -> { 201 | assertFalse(response.isSuccess()) 202 | assertEquals(HttpStatusCode.NotFound, response.status?.status) 203 | assertEquals(Response.HrefRelation.MEMBER, relation) 204 | nrCalled++ 205 | } 206 | 207 | url.resolve("/dav/") -> { 208 | assertFalse(response.isSuccess()) 209 | assertEquals(HttpStatusCode.InsufficientStorage, response.status?.status) 210 | assertEquals(Response.HrefRelation.SELF, relation) 211 | nrCalled++ 212 | } 213 | } 214 | } 215 | assertEquals("http://example.com/ns/sync/1233", result.filterIsInstance().first().token) 216 | assertEquals(4, nrCalled) 217 | } 218 | 219 | /** 220 | * Test sample response for a sync-collection report with unsupported limit from RFC 6578 3.12. 221 | */ 222 | test("testSyncCollectionReportWithUnsupportedLimit") { 223 | val url = sampleUrl 224 | 225 | httpClient.changeMockHandler { request -> 226 | if (request.url == sampleUrl) { 227 | respond( 228 | "\n" + 229 | " \n" + 230 | " \n" + 231 | " ", 232 | HttpStatusCode.InsufficientStorage, 233 | headersOf(HttpHeaders.ContentType, ContentType.Text.Xml.withCharset(Charsets.UTF_8).toString()) 234 | ) 235 | } else { 236 | respondError(HttpStatusCode.BadRequest) 237 | } 238 | } 239 | 240 | val collection = DavCollection(httpClient, url) 241 | 242 | try { 243 | collection.reportChanges("http://example.com/ns/sync/1232", false, 100, GetETag.NAME) { _, _ -> } 244 | fail("Expected HttpException") 245 | } catch (e: HttpException) { 246 | assertEquals(507, e.code) 247 | assertTrue(e.errors.any { it.name == QName(XmlUtils.NS_WEBDAV, "number-of-matches-within-limits") }) 248 | assertEquals(1, e.errors.size) 249 | } 250 | } 251 | 252 | test("testPost") { 253 | val url = sampleUrl 254 | httpClient.changeMockHandler { request -> 255 | if (request.url == sampleUrl) { 256 | respond("", HttpStatusCode.Created) 257 | } else { 258 | respondError(HttpStatusCode.BadRequest) 259 | } 260 | } 261 | val dav = DavCollection(httpClient, url) 262 | 263 | var called = false 264 | dav.post(sampleText, ContentType.Text.Plain) { response -> 265 | val request = httpClient.lastMockRequest 266 | assertEquals(HttpMethod.Post, request.method) 267 | assertEquals(request.url, dav.location) 268 | called = true 269 | } 270 | assertTrue(called) 271 | } 272 | 273 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | --------------------------------------------------------------------------------