Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 1 addition & 42 deletions src/main/kotlin/at/bitfire/dav4jvm/ktor/DavResource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
package at.bitfire.dav4jvm.ktor

import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.XmlReader
import at.bitfire.dav4jvm.XmlUtils
import at.bitfire.dav4jvm.XmlUtils.insertTag
import at.bitfire.dav4jvm.XmlUtils.propertyName
Expand Down Expand Up @@ -735,23 +734,6 @@ open class DavResource(
// verify that the response is 207 Multi-Status
assertMultiStatus(response, bodyChannel)

return processMultiStatus(bodyChannel, callback)
}

/**
* Processes a Multi-Status response.
*
* @param bodyChannel the response body channel to read the Multi-Status response from
* @param callback called for every XML response element in the Multi-Status response
*
* @return list of properties which have been received in the Multi-Status response, but
* are not part of response XML elements (like `sync-token` which is returned as [SyncToken])
*
* @throws IOException on I/O error
* @throws HttpException on HTTP error
* @throws DavException on WebDAV error (like an invalid XML response)
*/
protected suspend fun processMultiStatus(bodyChannel: ByteReadChannel, callback: MultiResponseCallback): List<Property> {
val parser = XmlUtils.newPullParser()

try {
Expand All @@ -762,7 +744,7 @@ open class DavResource(
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && parser.depth == 1)
if (parser.propertyName() == WebDAV.MultiStatus) {
return parseMultiStatus(parser, callback)
return MultiStatusParser(location).parseResponse(parser, callback)
// further <multistatus> elements are ignored
}

Expand All @@ -779,27 +761,4 @@ open class DavResource(
}
}

private suspend fun parseMultiStatus(parser: XmlPullParser, callback: MultiResponseCallback): List<Property> {
val responseProperties = mutableListOf<Property>()

// <!ELEMENT multistatus (response*, responsedescription?,
// sync-token?) >
val depth = parser.depth
var eventType = parser.eventType
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
when (parser.propertyName()) {
WebDAV.Response ->
Response.parse(parser, location, callback)
WebDAV.SyncToken ->
XmlReader(parser).readText()?.let {
responseProperties += SyncToken(it)
}
}
eventType = parser.next()
}

return responseProperties
}

}
53 changes: 53 additions & 0 deletions src/main/kotlin/at/bitfire/dav4jvm/ktor/MultiStatusParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/

package at.bitfire.dav4jvm.ktor

import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.XmlReader
import at.bitfire.dav4jvm.XmlUtils.propertyName
import at.bitfire.dav4jvm.property.webdav.SyncToken
import at.bitfire.dav4jvm.property.webdav.WebDAV
import io.ktor.http.Url
import org.xmlpull.v1.XmlPullParser

/**
* Parses a WebDAV `<multistatus>` XML response.
*
* @param location location of the request (used to resolve possible relative `<href>` in responses)
*/
class MultiStatusParser(
private val location: Url
) {

suspend fun parseResponse(parser: XmlPullParser, callback: MultiResponseCallback): List<Property> {
val responseProperties = mutableListOf<Property>()

// <!ELEMENT multistatus (response*, responsedescription?,
// sync-token?) >
val depth = parser.depth
var eventType = parser.eventType
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
when (parser.propertyName()) {
WebDAV.Response ->
ResponseParser(location).parseResponse(parser, callback)
WebDAV.SyncToken ->
XmlReader(parser).readText()?.let {
responseProperties += SyncToken(it)
}
}
eventType = parser.next()
}

return responseProperties
}

}
34 changes: 1 addition & 33 deletions src/main/kotlin/at/bitfire/dav4jvm/ktor/PropStat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ package at.bitfire.dav4jvm.ktor

import at.bitfire.dav4jvm.Error
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.XmlUtils.propertyName
import at.bitfire.dav4jvm.property.webdav.WebDAV
import io.ktor.http.HttpStatusCode
import org.xmlpull.v1.XmlPullParser
import java.util.LinkedList

/**
* Represents a WebDAV propstat XML element.
Expand All @@ -27,32 +23,4 @@ data class PropStat(
val properties: List<Property>,
val status: HttpStatusCode,
val error: List<Error>? = null
) {

companion object {

private val ASSUMING_OK = HttpStatusCode(200, "Assuming OK")

fun parse(parser: XmlPullParser): PropStat {
val depth = parser.depth

var status: HttpStatusCode? = null
val prop = LinkedList<Property>()

var eventType = parser.eventType
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
when (parser.propertyName()) {
WebDAV.Prop ->
prop.addAll(Property.parse(parser))
WebDAV.Status ->
status = KtorHttpUtils.parseStatusLine(parser.nextText())
}
eventType = parser.next()
}

return PropStat(prop, status ?: ASSUMING_OK)
}

}
}
)
50 changes: 50 additions & 0 deletions src/main/kotlin/at/bitfire/dav4jvm/ktor/PropStatParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/

package at.bitfire.dav4jvm.ktor

import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.XmlUtils.propertyName
import at.bitfire.dav4jvm.property.webdav.WebDAV
import io.ktor.http.HttpStatusCode
import org.xmlpull.v1.XmlPullParser
import java.util.LinkedList

class PropStatParser {

fun parse(parser: XmlPullParser): PropStat {
val depth = parser.depth

var status: HttpStatusCode? = null
val prop = LinkedList<Property>()

var eventType = parser.eventType
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.depth == depth + 1)
when (parser.propertyName()) {
WebDAV.Status ->
status = KtorHttpUtils.parseStatusLine(parser.nextText())
WebDAV.Prop ->
prop.addAll(Property.parse(parser))
}
eventType = parser.next()
}

return PropStat(prop, status ?: ASSUMING_OK)
}


companion object {

private val ASSUMING_OK = HttpStatusCode(200, "Assuming OK")

}

}
147 changes: 0 additions & 147 deletions src/main/kotlin/at/bitfire/dav4jvm/ktor/Response.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,9 @@ package at.bitfire.dav4jvm.ktor

import at.bitfire.dav4jvm.Error
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.XmlUtils.propertyName
import at.bitfire.dav4jvm.property.webdav.ResourceType
import at.bitfire.dav4jvm.property.webdav.WebDAV
import io.ktor.http.HttpStatusCode
import io.ktor.http.URLBuilder
import io.ktor.http.Url
import io.ktor.http.isSuccess
import io.ktor.http.takeFrom
import org.xmlpull.v1.XmlPullParser
import java.util.logging.Logger

/**
* Represents a WebDAV response XML Element.
Expand Down Expand Up @@ -97,144 +90,4 @@ data class Response(
*/
fun hrefName() = KtorHttpUtils.fileName(href)


companion object {

/**
* Parses an XML response element and calls the [callback] for it (when it has a `<href>`).
* The arguments of the [MultiResponseCallback.onResponse] are set accordingly.
*
* If the [at.bitfire.dav4jvm.property.webdav.ResourceType] of the queried resource is known (= was queried and returned by the server)
* and it contains [at.bitfire.dav4jvm.property.webdav.ResourceType.Companion.COLLECTION], the `href` property of the callback will automatically
* have a trailing slash.
*
* So if you want PROPFIND results to have a trailing slash when they are collections, make sure
* that you query [at.bitfire.dav4jvm.property.webdav.ResourceType].
*/
suspend fun parse(parser: XmlPullParser, location: Url, callback: MultiResponseCallback) {
val logger = Logger.getLogger(Response::javaClass.name)

val depth = parser.depth

var hrefOrNull: Url? = null
var status: HttpStatusCode? = null
val propStat = mutableListOf<PropStat>()
var error: List<Error>? = null
var newLocation: Url? = null

var eventType = parser.eventType
while (!(eventType == XmlPullParser.END_TAG && parser.depth == depth)) {
if (eventType == XmlPullParser.START_TAG && parser.depth == depth+1)
when (parser.propertyName()) {
WebDAV.Href -> {
var sHref = parser.nextText()
var hierarchical = false
if (!sHref.startsWith("/")) {
/* According to RFC 4918 8.3 URL Handling, only absolute paths are allowed as relative
URLs. However, some servers reply with relative paths. */
val firstColon = sHref.indexOf(':')
if (firstColon != -1) {
/* There are some servers which return not only relative paths, but relative paths like "a:b.vcf",
which would be interpreted as scheme: "a", scheme-specific part: "b.vcf" normally.
For maximum compatibility, we prefix all relative paths which contain ":" (but not "://"),
with "./" to allow resolving by HttpUrl. */
try {
if (sHref.substring(firstColon, firstColon + 3) == "://")
hierarchical = true
} catch (e: IndexOutOfBoundsException) {
// no "://"
}
if (!hierarchical)
sHref = "./$sHref"

}
}


if(!hierarchical) {
val urlBuilder = URLBuilder(location).takeFrom(sHref)
urlBuilder.pathSegments = urlBuilder.pathSegments.filterNot { it == "." } // Drop segments that are "./"
hrefOrNull = urlBuilder.build()
} else {
hrefOrNull = URLBuilder(location).takeFrom(sHref).build()
}
}
WebDAV.Status ->
status = KtorHttpUtils.parseStatusLine(parser.nextText())
WebDAV.PropStat ->
PropStat.parse(parser).let { propStat += it }
WebDAV.Error ->
error = Error.parseError(parser)
WebDAV.Location ->
newLocation = Url(parser.nextText()) // TODO: Need to catch exception here?
}
eventType = parser.next()
}

if (hrefOrNull == null) {
logger.warning("Ignoring XML response element without valid href")
return
}
var href: Url = hrefOrNull // guaranteed to be not null

// if we know this resource is a collection, make sure href has a trailing slash
// (for clarity and resolving relative paths)
propStat.filter { it.status.isSuccess() }
.flatMap { it.properties }
.filterIsInstance<ResourceType>()
.firstOrNull()
?.let { type ->
if (type.types.contains(WebDAV.Collection))
href = UrlUtils.withTrailingSlash(href)
}

//log.log(Level.FINE, "Received properties for $href", if (status != null) status else propStat)

// Which resource does this <response> represent?
val relation = when {
UrlUtils.omitTrailingSlash(href).equalsForWebDAV(UrlUtils.omitTrailingSlash(location)) ->
HrefRelation.SELF

else -> {
if (location.protocol.name == href.protocol.name && location.host == href.host && location.port == href.port) {
val locationSegments = location.rawSegments
val hrefSegments = href.rawSegments

// don't compare trailing slash segment ("")
var nBasePathSegments = locationSegments.size
if (locationSegments.lastOrNull() == "")
nBasePathSegments--

/* example: locationSegments = [ "davCollection", "" ]
nBasePathSegments = 1
hrefSegments = [ "davCollection", "aMember" ]
*/
var relation = HrefRelation.OTHER
if (hrefSegments.size > nBasePathSegments) {
val sameBasePath = (0 until nBasePathSegments).none { locationSegments[it] != hrefSegments[it] }
if (sameBasePath)
relation = HrefRelation.MEMBER
}

relation
} else
HrefRelation.OTHER
}
}

callback.onResponse(
Response(
requestedUrl = location,
href = href,
status = status,
propstat = propStat,
error = error,
newLocation = newLocation
),
relation
)
}

}

}
Loading