diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ef7f8af7d..31c6e642fe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -178,13 +178,10 @@ org-hibernate-orm-core = { module = "org.hibernate.orm:hibernate-core", version. org-hibernate-search-backend-lucene = { module = "org.hibernate.search:hibernate-search-backend-lucene", version.ref = "org-hibernate-search" } org-hibernate-search-mapper-orm = { module = "org.hibernate.search:hibernate-search-mapper-orm", version.ref = "org-hibernate-search" } org-hsqldb-hsqldb = { module = "org.hsqldb:hsqldb", version.ref = "org-hsqldb-hsqldb" } -org-jetbrains-intellij-deps-trove4j = { module = "org.jetbrains.intellij.deps:trove4j", version.ref = "org-jetbrains-intellij-deps-trove4j" } org-jetbrains-kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "org-jetbrains-kotlin" } -org-jetbrains-kotlin-script-runtime = { module = "org.jetbrains.kotlin:kotlin-script-runtime", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-scripting-common = { module = "org.jetbrains.kotlin:kotlin-scripting-common", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-scripting-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable", version.ref = "org-jetbrains-kotlin" } -org-jetbrains-kotlin-scripting-compiler-impl-embeddable = { module = "org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-scripting-jvm = { module = "org.jetbrains.kotlin:kotlin-scripting-jvm", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-scripting-jvm-host = { module = "org.jetbrains.kotlin:kotlin-scripting-jvm-host", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "org-jetbrains-kotlin" } diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/address/PersonalAddressDao.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/address/PersonalAddressDao.kt index e5de8c6c80..3100ad8304 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/address/PersonalAddressDao.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/address/PersonalAddressDao.kt @@ -276,14 +276,17 @@ class PersonalAddressDao { * @return the list of all PersonalAddressDO entries for the context user without any check access (addresses might be also deleted). */ get() { - val owner = requiredLoggedInUser - return persistenceService.executeNamedQuery( - PersonalAddressDO.FIND_FAVORITE_ADDRESS_IDS_BY_OWNER, - Long::class.java, - Pair("ownerId", owner.id) - ) + return getFavoriteAddressIdList(requiredLoggedInUser) } + fun getFavoriteAddressIdList(owner: PFUserDO): List { + return persistenceService.executeNamedQuery( + PersonalAddressDO.FIND_FAVORITE_ADDRESS_IDS_BY_OWNER, + Long::class.java, + Pair("ownerId", owner.id) + ) + } + val personalAddressByAddressId: Map /** * @return the list of all address ids of personal address book for the context user (isFavorite() must be true). diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavFilter.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavFilter.kt index 1028d4512d..cf8dd344d3 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavFilter.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavFilter.kt @@ -44,6 +44,7 @@ private val log = KotlinLogging.logger {} class CardDavFilter : Filter { private lateinit var springContext: WebApplicationContext + @Suppress("SpringJavaInjectionPointsAutowiringInspection") @Autowired private lateinit var cardDavService: CardDavService @@ -125,7 +126,7 @@ class CardDavFilter : Filter { return urlMatches(normalizedUri, "users", "principals") } - "GET" -> { + "GET", "DELETE" -> { log.debug { "GET call detected: $normalizedUri" } // /carddav/users/admin/addressbooks/ProjectForge-129.vcf return normalizedUri.matches(NORMALIZED_GET_REQUEST_REGEX) diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavService.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavService.kt index 8942e41bff..31c5976d94 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavService.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/CardDavService.kt @@ -52,6 +52,9 @@ class CardDavService { @Autowired private lateinit var addressService: AddressService + @Autowired + private lateinit var deleteRequestHandler: DeleteRequestHandler + @Autowired private lateinit var domainService: DomainService @@ -136,6 +139,9 @@ class CardDavService { // /carddav/users/admin/addressbooks/ProjectForge-129.vcf writerContext.contactList = getContactList(userDO) GetRequestHandler.handleGetCall(writerContext) + } else if (method == "DELETE") { + // /carddav/users/admin/addressbooks/ProjectForge-129.vcf + deleteRequestHandler.handleDeleteCall(writerContext) } else { log.warn { "Method not supported: $method" } ResponseUtils.setValues( @@ -146,9 +152,7 @@ class CardDavService { } private fun getContactList(userDO: PFUserDO): List { - val user = User(userDO.username) - val addressBook = AddressBook(user) - return addressService.getContactList(addressBook) + return addressService.getContactList(userDO) } private fun authenticate(request: HttpServletRequest, response: HttpServletResponse): RestAuthenticationInfo { diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/DeleteRequestHandler.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/DeleteRequestHandler.kt new file mode 100644 index 0000000000..62ecdbe73b --- /dev/null +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/DeleteRequestHandler.kt @@ -0,0 +1,64 @@ +///////////////////////////////////////////////////////////////////////////// +// +// Project ProjectForge Community Edition +// www.projectforge.org +// +// Copyright (C) 2001-2024 Micromata GmbH, Germany (www.micromata.com) +// +// ProjectForge is dual-licensed. +// +// This community edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as published +// by the Free Software Foundation; version 3 of the License. +// +// This community edition is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, see http://www.gnu.org/licenses/. +// +///////////////////////////////////////////////////////////////////////////// + +package org.projectforge.carddav + +import mu.KotlinLogging +import org.projectforge.carddav.service.AddressService +import org.projectforge.rest.utils.ResponseUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service + +private val log = KotlinLogging.logger {} + +@Service +internal class DeleteRequestHandler { + @Autowired + private lateinit var addressService: AddressService + + /** + * Handles uri=/carddav/users/admin/addressbooks/ProjectForge-128.vcf, method=DELETE + * @param writerContext The writer context. + */ + fun handleDeleteCall(writerContext: WriterContext) { + val requestWrapper = writerContext.requestWrapper + val response = writerContext.response + val contactList = writerContext.contactList ?: emptyList() + log.debug { "handleDeleteCall: ${requestWrapper.request.method}: '${requestWrapper.requestURI}' body=[${requestWrapper.body}]" } + val requestedPath = requestWrapper.requestURI + val contactId = CardDavUtils.extractContactId(requestedPath) + val contact = contactList.find { it.id == contactId } + if (contactId == null) { + log.info { "Contact with id=$contactId not found in personal contact list. Can't delete it." } + ResponseUtils.setValues(response, content = "The resource does not exist.", status = HttpStatus.NOT_FOUND) + return + } + if (addressService.deleteContact(contactId, contact)) { + ResponseUtils.setValues(response, HttpStatus.NO_CONTENT) + } else { + ResponseUtils.setValues(response, content = "The resource does not exist.", status = HttpStatus.NOT_FOUND) + } + log.debug { "handleGetCall: response.content=[]" } + } +} diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/GetRequestHandler.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/GetRequestHandler.kt index 20ce3371f2..922eb51d20 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/GetRequestHandler.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/GetRequestHandler.kt @@ -23,20 +23,16 @@ package org.projectforge.carddav -import jakarta.servlet.http.HttpServletResponse import mu.KotlinLogging -import org.projectforge.carddav.model.Contact import org.projectforge.rest.utils.ResponseUtils import org.springframework.http.HttpStatus -import org.springframework.http.MediaType private val log = KotlinLogging.logger {} internal object GetRequestHandler { /** - * Handles /carddav/users/admin/addressbooks/ProjectForge-129.vcf, method=[GET] - * @param requestWrapper The request wrapper. - * @param response The response. + * Handles /carddav/users/admin/addressbooks/ProjectForge-129.vcf, method=GET + * @param writerContext The writer context. */ fun handleGetCall(writerContext: WriterContext) { val requestWrapper = writerContext.requestWrapper @@ -56,12 +52,7 @@ internal object GetRequestHandler { // A unique identifier for this version of the resource. This allows clients to detect changes efficiently. response.addHeader("ETag", contact.etag) response.addHeader("Last-Modified", contact.lastModifiedAsHttpDate) - ResponseUtils.setValues( - response, - HttpStatus.OK, - contentType = "text/vcard", - content = content, - ) + ResponseUtils.setValues(response, HttpStatus.OK, contentType = "text/vcard", content = content) log.debug { "handleGetCall: response=${ResponseUtils.asJson(response)}, content=[$content]" } } } diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/PropFindRequestHandler.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/PropFindRequestHandler.kt index a4b8da7c36..88fca0865c 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/PropFindRequestHandler.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/PropFindRequestHandler.kt @@ -45,7 +45,7 @@ internal object PropFindRequestHandler { log.debug { "handlePropFindCall: ${requestWrapper.request.method}: '${requestWrapper.requestURI}' body=[${requestWrapper.body}]" } CardDavUtils.handleProps(requestWrapper, response) ?: return // No properties response is handled in handleProps. val content = generatePropFindResponse(writerContext) - log.debug { "handlePropFindCall: response=[$content]" } + log.debug { "handlePropFindCall: response.content=[$content]" } CardDavUtils.setMultiStatusResponse(response, content) } diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/ReportRequestHandler.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/ReportRequestHandler.kt index b01a828bc1..3d6de980ab 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/ReportRequestHandler.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/ReportRequestHandler.kt @@ -23,7 +23,6 @@ package org.projectforge.carddav -import jakarta.servlet.http.HttpServletResponse import mu.KotlinLogging import org.projectforge.carddav.CardDavUtils.CARD import org.projectforge.carddav.CardDavUtils.D @@ -33,7 +32,6 @@ import org.projectforge.carddav.model.Contact import org.projectforge.rest.utils.ResponseUtils import org.springframework.http.HttpStatus import org.springframework.http.MediaType -import java.util.Date private val log = KotlinLogging.logger {} @@ -56,9 +54,7 @@ internal object ReportRequestHandler { * * ``` * - * @param requestWrapper The request wrapper. - * @param response The response. - * @see CardDavXmlUtils.generatePropFindResponse + * @param writerContext The writer context. */ fun handleSyncReportCall(writerContext: WriterContext) { val requestWrapper = writerContext.requestWrapper @@ -72,7 +68,7 @@ internal object ReportRequestHandler { if (rootElement == "sync-collection") { val syncToken = props.find { it.type == PropType.SYNCTOKEN }?.value val syncTokenMillis = CardDavUtils.getMillisFromSyncToken(syncToken) - val lastUpdated = CardDavUtils.getLastUpdated(contactList)?.time ?: 0L + val lastUpdated = CardDavUtils.getLastUpdated(contactList)?.time ?: 0L val newSyncToken = CardDavUtils.getSyncToken() if (syncTokenMillis != null && syncTokenMillis >= lastUpdated) { // 1. No modifications since the last sync-token. @@ -137,7 +133,7 @@ internal object ReportRequestHandler { | <$D:href>${href}${CardDavUtils.getVcfFileName(contact)} | <$D:propstat> | <$D:prop> - | <$D:getetag>"${contact.etag}" + | <$D:getetag>${contact.etag} """.trimMargin() ) if (fullVCards) { diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/model/Contact.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/model/Contact.kt index 3dba6e6648..d3c5bdb0e8 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/model/Contact.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/model/Contact.kt @@ -39,13 +39,14 @@ data class Contact( /** * A unique identifier for this version of the resource. This allows clients to detect changes efficiently. * The hashcode of the vcard, embedded in quotes. + * Please note: the etag is embedded in quotes. Must be used in xml as well as in http response of GET call. * @return The ETag. */ val etag: String by lazy { vcardData?.let { val digest = MessageDigest.getInstance("SHA-256") val hashBytes = digest.digest(it.toByteArray()) - hashBytes.joinToString("") { "%02x".format(it) } + "\"${hashBytes.joinToString("") { "%02x".format(it) }}\"" } ?: "\"null\"" } diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressDAVCache.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressDAVCache.kt index e66a4fe5b7..13a8c5ab6b 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressDAVCache.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressDAVCache.kt @@ -29,7 +29,6 @@ import org.projectforge.business.address.AddressDO import org.projectforge.business.address.AddressDao import org.projectforge.business.address.AddressImageDao import org.projectforge.business.address.vcard.VCardUtils -import org.projectforge.carddav.model.AddressBook import org.projectforge.carddav.model.Contact import org.projectforge.framework.access.OperationType import org.projectforge.framework.cache.AbstractCache @@ -56,7 +55,7 @@ open class AddressDAVCache : AbstractCache(TICKS_PER_HOUR), BaseDOModifiedListen return getCachedAddress(id) } - fun getContacts(addressBook: AddressBook, ids: List): List { + fun getContacts(ids: List): List { val result = mutableListOf() val missedInCache = mutableListOf() ids.forEach { @@ -116,7 +115,7 @@ open class AddressDAVCache : AbstractCache(TICKS_PER_HOUR), BaseDOModifiedListen } override fun refresh() { - org.projectforge.carddav.service.log.info("Clearing cache ${this::class.java.simpleName}.") + log.info("Clearing cache ${this::class.java.simpleName}.") synchronized(contactMap) { contactMap.clear() } diff --git a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressService.kt b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressService.kt index c2dc46f1b6..dc76d3689d 100644 --- a/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressService.kt +++ b/projectforge-carddav/src/main/kotlin/org/projectforge/carddav/service/AddressService.kt @@ -26,9 +26,9 @@ package org.projectforge.carddav.service import mu.KotlinLogging import org.projectforge.business.address.PersonalAddressDao import org.projectforge.business.address.vcard.VCardUtils -import org.projectforge.carddav.model.AddressBook import org.projectforge.carddav.model.Contact import org.projectforge.framework.persistence.user.api.ThreadLocalUserContext +import org.projectforge.framework.persistence.user.entities.PFUserDO import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @@ -42,65 +42,60 @@ class AddressService { @Autowired private lateinit var personalAddressDao: PersonalAddressDao - fun getContactList(addressBook: AddressBook): List { - val favorites = personalAddressDao.favoriteAddressIdList - return addressDAVCache.getContacts(addressBook, favorites) + fun getContactList(userDO: PFUserDO): List { + val favorites = personalAddressDao.getFavoriteAddressIdList(userDO) + return addressDAVCache.getContacts(favorites) } - fun getContact(id: Long): Contact? { - val favorites = personalAddressDao.favoriteAddressIdList - if (!favorites.contains(id)) { - log.info { "Contact with id=$id is not in favorites. Don't returning contact." } - return null + /* + fun getContact(id: Long): Contact? { + val favorites = personalAddressDao.favoriteAddressIdList + if (!favorites.contains(id)) { + log.info { "Contact with id=$id is not in favorites. Don't returning contact." } + return null + } + return addressDAVCache.getContact(id) } - return addressDAVCache.getContact(id) - } - - @Suppress("UNUSED_PARAMETER") - fun createContact(ab: AddressBook, vcardBytearray: ByteArray): Contact { - log.warn("Creation of contacts not supported.") - /*try { - val vcard = vCardService.getVCardFromByteArray(vcardBytearray) ?: return Contact() - val address = vCardService.buildAddressDO(vcard) - addressDao.save(address) - personalAddressDao.save(...) // Add this new address to user's favorites. - } catch (e: Exception) { - log.error("Exception while creating contact.", e) - }*/ - return Contact() - } - @Suppress("UNUSED_PARAMETER") - fun updateContact(contact: Contact, vcardBytearray: ByteArray?): Contact { - log.warn("Updating of contacts not supported.") - /*try { - val vcard = vCardService.getVCardFromByteArray(vcardBytearray) ?: return Contact() - val address = vCardService.buildAddressDO(vcard) - addressDao.update(address) - } catch (e: Exception) { - log.error("Exception while updating contact: " + contact.name, e) - }*/ - return Contact() - } + @Suppress("UNUSED_PARAMETER") + fun createContact(ab: AddressBook, vcardBytearray: ByteArray): Contact { + log.warn("Creation of contacts not supported.") + /*try { + val vcard = vCardService.getVCardFromByteArray(vcardBytearray) ?: return Contact() + val address = vCardService.buildAddressDO(vcard) + addressDao.save(address) + personalAddressDao.save(...) // Add this new address to user's favorites. + } catch (e: Exception) { + log.error("Exception while creating contact.", e) + }*/ + return Contact() + } - fun deleteContact(contact: Contact) { + @Suppress("UNUSED_PARAMETER") + fun updateContact(contact: Contact, vcardBytearray: ByteArray?): Contact { + log.warn("Updating of contacts not supported.") + /*try { + val vcard = vCardService.getVCardFromByteArray(vcardBytearray) ?: return Contact() + val address = vCardService.buildAddressDO(vcard) + addressDao.update(address) + } catch (e: Exception) { + log.error("Exception while updating contact: " + contact.name, e) + }*/ + return Contact() + } + */ + fun deleteContact(contactId: Long, contact: Contact?): Boolean { try { - val list = VCardUtils.parseVCardsFromString(contact.vcardData) - if (list.isEmpty()) { - return // Nothing to do. - } - if (list.size > 1) { - log.warn { "More than one vcard found in contact. Deleting only the first one." } - } - val vcard = list[0] - val personalAddress = personalAddressDao.getByAddressUid(vcard.uid.value) + val personalAddress = personalAddressDao.getByAddressId(contactId) if (personalAddress?.isFavorite == true) { personalAddress.isFavoriteCard = false personalAddressDao.saveOrUpdate(personalAddress) - log.info("Contact '${vcard.formattedName.value} removed from ${ThreadLocalUserContext.loggedInUser!!.username}'s favorite list.") + log.info("Contact #$contactId '${contact?.displayName}' removed from favorite list.") } + return true } catch (e: Exception) { - log.error(e) { "Exception while deleting contact: id=${contact.id}, name=[${contact.lastName}, ${contact.firstName}]" } + log.error(e) { "Exception while deleting contact: id=${contactId}, name=[${contact?.displayName}]" } + return false } } }