diff --git a/app/appearance/langs/de_DE.json b/app/appearance/langs/de_DE.json
index 3f314f9e7..0f423d316 100644
--- a/app/appearance/langs/de_DE.json
+++ b/app/appearance/langs/de_DE.json
@@ -1,6 +1,6 @@
{
"fields": "Attribut",
- "dynamicEmoji": "Icône dynamique",
+ "dynamicEmoji": "Dynamisches Icon",
"backlinkContainChildren": "Enthalten Rückverweise untergeordnete Blöcke",
"backlinkContainChildrenTip": "Wenn aktiviert, werden untergeordnete Blöcke in die Berechnung der Rückverweise einbezogen",
"entryNum": "Anzahl der Einträge",
diff --git a/app/appearance/langs/es_ES.json b/app/appearance/langs/es_ES.json
index 0d0c9023e..9de756a24 100644
--- a/app/appearance/langs/es_ES.json
+++ b/app/appearance/langs/es_ES.json
@@ -1,6 +1,6 @@
{
"fields": "Atributo",
- "dynamicEmoji": "Icône dynamique",
+ "dynamicEmoji": "Icono dinámico",
"backlinkContainChildren": "¿Los enlaces inversos contienen bloques secundarios?",
"backlinkContainChildrenTip": "Una vez habilitado, los bloques secundarios se incluirán en el cálculo de los enlaces inversos",
"entryNum": "Número de entradas",
diff --git a/app/appearance/langs/it_IT.json b/app/appearance/langs/it_IT.json
index 7c0130e5a..1bdacf124 100644
--- a/app/appearance/langs/it_IT.json
+++ b/app/appearance/langs/it_IT.json
@@ -1,6 +1,6 @@
{
- "fields": "Attributo",
- "dynamicEmoji": "動的アイコン",
+ "fields": "Campi",
+ "dynamicEmoji": "Emoji dinamica",
"backlinkContainChildren": "I backlink contengono blocchi figli",
"backlinkContainChildrenTip": "Dopo l'attivazione, i blocchi figli saranno inclusi nel calcolo dei backlink",
"entryNum": "Numero di voci",
@@ -40,7 +40,7 @@
"confirmDeleteTip": "Sei sicuro di voler eliminare ${x}?",
"rollbackTip": "Dopo l'eliminazione, può essere ripristinato in [Cronologia dati], conservato ${x} giorni in base a [Impostazioni - Editor - Giorni di conservazione della cronologia]",
"newView": "Aggiungi vista",
- "newCol": "Aggiungi colonna",
+ "newCol": "Aggiungi campo",
"newRow": "Aggiungi voce",
"enterKey": "Invio",
"doubleClick": "Doppio clic",
@@ -87,7 +87,7 @@
"lastReviewTime": "Ultima revisione",
"cardStatus": "Stato della carta",
"noSupportTip": "Questa funzione non supporta l'uso di mazzi di carte",
- "insertRowTip": "Le righe appena aggiunte sono state filtrate e possono essere visualizzate annullando il filtraggio/ricerca/ordinamento",
+ "insertRowTip": "L'elemento aggiunto è stato filtrato, è possibile annullare filtro/ricerca/ordinamento per visualizzarlo",
"insertPhoto": "Scatta una foto e inseriscila",
"relativeToToday": "Relativo a oggi",
"current": "Questo",
@@ -112,7 +112,7 @@
"dragFill": "Trascina verticalmente per riempire i valori",
"switchReadonly": "Passa alla modalità di sola lettura",
"original": "Originale",
- "selectRelation": "Seleziona prima la colonna correlata",
+ "selectRelation": "Seleziona prima il campo correlato",
"backRelation": "Bidirezionale",
"thisDatabase": "Questo database",
"relatedTo": "Relazionato a",
@@ -163,8 +163,8 @@
"unsplit": "Dividi",
"unsplitAll": "Dividi tutto",
"resetCardTip": "Sei sicuro di voler reimpostare ${x} flashcard?",
- "freezeCol": "Blocca colonna",
- "unfreezeCol": "Sblocca colonna",
+ "freezeCol": "Blocca campo",
+ "unfreezeCol": "Sblocca campo",
"snippetsTip": "Lo snippet di codice è stato aggiornato, vuoi salvarlo?",
"addBelowAbove": "Clicca Aggiungi sotto
⌥Clicca Aggiungi sopra",
"imported": "Importazione completata",
@@ -275,10 +275,10 @@
"filterOperatorIsOnOrAfter": "È il giorno o dopo di",
"asc": "Ascendente",
"desc": "Discendente",
- "hideCol": "Nascondi colonna",
+ "hideCol": "Nascondi campo",
"hideAll": "Nascondi tutto",
"showAll": "Mostra tutto",
- "showCol": "Mostra colonna",
+ "showCol": "Mostra campo",
"number": "Numero",
"date": "Data",
"select": "Seleziona",
@@ -1261,7 +1261,7 @@
"uploadError": "Errore di caricamento",
"uploading": "Caricamento in corso.",
"wysiwyg": "WYSIWYG",
- "_label": "Inglese",
+ "_label": "Italiano",
"_time": {
"albl": "fa",
"blbl": "da adesso",
diff --git a/app/appearance/langs/pl_PL.json b/app/appearance/langs/pl_PL.json
index 46337e4e9..aab82c0ec 100644
--- a/app/appearance/langs/pl_PL.json
+++ b/app/appearance/langs/pl_PL.json
@@ -1,6 +1,6 @@
{
"fields": "Atrybut",
- "dynamicEmoji": "Динамическая иконка",
+ "dynamicEmoji": "Ikona dynamiczna",
"backlinkContainChildren": "Czy linki zwrotne zawierają bloki podrzędne",
"backlinkContainChildrenTip": "Po włączeniu bloki podrzędne zostaną uwzględnione w obliczeniach linków zwrotnych",
"entryNum": "Количество записей",
diff --git a/app/electron/main.js b/app/electron/main.js
index ac42e8632..c52a36234 100644
--- a/app/electron/main.js
+++ b/app/electron/main.js
@@ -48,6 +48,9 @@ let resetWindowStateOnRestart = false;
remote.initialize();
+app.setPath("userData", app.getPath("userData") + "-Electron"); // `~/.config` 下 Electron 相关文件夹名称改为 `SiYuan-Electron` https://github.com/siyuan-note/siyuan/issues/3349
+fs.rmdirSync(app.getPath("appData") + "/" + app.name, {recursive: true}); // 删除自动创建的应用目录 https://github.com/siyuan-note/siyuan/issues/13150
+
if (!app.requestSingleInstanceLock()) {
app.quit();
return;
@@ -621,7 +624,6 @@ const initKernel = (workspace, port, lang) => {
};
app.setAsDefaultProtocolClient("siyuan");
-app.setPath("userData", app.getPath("userData") + "-Electron"); // `~/.config` 下 Electron 相关文件夹名称改为 `SiYuan-Electron` https://github.com/siyuan-note/siyuan/issues/3349
app.commandLine.appendSwitch("disable-web-security");
app.commandLine.appendSwitch("auto-detect", "false");
diff --git a/kernel/go.mod b/kernel/go.mod
index 40a50d2d4..84fb8243d 100644
--- a/kernel/go.mod
+++ b/kernel/go.mod
@@ -23,6 +23,8 @@ require (
github.com/denisbrodbeck/machineid v1.0.1
github.com/dgraph-io/ristretto v1.0.0
github.com/djherbis/times v1.6.0
+ github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
+ github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135
github.com/emirpasic/gods v1.18.1
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
github.com/flopp/go-findfont v0.1.0
diff --git a/kernel/go.sum b/kernel/go.sum
index bef35047e..a28f63693 100644
--- a/kernel/go.sum
+++ b/kernel/go.sum
@@ -89,6 +89,12 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
+github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
+github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
+github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
+github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135 h1:Ssk00uh7jhctJ23eclGxhhGqplSQB+wCt6fmbjhnOS8=
+github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM=
@@ -367,6 +373,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
+github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go
new file mode 100644
index 000000000..8b2aad21a
--- /dev/null
+++ b/kernel/model/carddav.go
@@ -0,0 +1,880 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package model
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/88250/gulu"
+ "github.com/emersion/go-vcard"
+ "github.com/emersion/go-webdav/carddav"
+ "github.com/siyuan-note/logging"
+ "github.com/siyuan-note/siyuan/kernel/util"
+)
+
+const (
+ // REF: https://developers.google.com/people/carddav#resources
+ CardDavPrefixPath = "/carddav"
+ CardDavPrincipalsPath = CardDavPrefixPath + "/principals" // 0 resourceTypeRoot
+ CardDavUserPrincipalPath = CardDavPrincipalsPath + "/main" // 1 resourceTypeUserPrincipal
+ CardDavHomeSetPath = CardDavUserPrincipalPath + "/contacts" // 2 resourceTypeAddressBookHomeSet
+
+ CardDavDefaultAddressBookPath = CardDavHomeSetPath + "/default" // 3 resourceTypeAddressBook
+ CardDavDefaultAddressBookName = "default"
+
+ CardDavAddressBooksMetaDataFilePath = CardDavHomeSetPath + "/address-books.json"
+)
+
+type PathDepth int
+
+const (
+ pathDepth_Root PathDepth = 1 + iota // /carddav
+ pathDepth_Principals // /carddav/principals
+ pathDepth_UserPrincipal // /carddav/principals/main
+ pathDepth_HomeSet // /carddav/principals/main/contacts
+ pathDepth_AddressBook // /carddav/principals/main/contacts/default
+ pathDepth_Address // /carddav/principals/main/contacts/default/id.vcf
+)
+
+var (
+ defaultAddressBook = carddav.AddressBook{
+ Path: CardDavDefaultAddressBookPath,
+ Name: CardDavDefaultAddressBookName,
+ Description: "Default address book",
+ MaxResourceSize: 0,
+ }
+ contacts = Contacts{
+ loaded: false,
+ changed: false,
+ lock: sync.Mutex{},
+ books: sync.Map{},
+ booksMetaData: []*carddav.AddressBook{},
+ }
+
+ ErrorNotFound = errors.New("CardDAV: not found")
+ ErrorPathInvalid = errors.New("CardDAV: path is invalid")
+
+ ErrorBookNotFound = errors.New("CardDAV: address book not found")
+ ErrorBookPathInvalid = errors.New("CardDAV: address book path is invalid")
+
+ ErrorAddressNotFound = errors.New("CardDAV: address not found")
+ ErrorAddressFileExtensionNameInvalid = errors.New("CardDAV: address file extension name is invalid")
+)
+
+// CardDavPath2DirectoryPath converts CardDAV path to absolute path of the file system
+func CardDavPath2DirectoryPath(cardDavPath string) string {
+ return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(cardDavPath, "/"))
+}
+
+// HomeSetPathPath returns the absolute path of the address book home set directory
+func HomeSetPathPath() string {
+ return CardDavPath2DirectoryPath(CardDavHomeSetPath)
+}
+
+// AddressBooksMetaDataFilePath returns the absolute path of the address books meta data file
+func AddressBooksMetaDataFilePath() string {
+ return CardDavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath)
+}
+
+func GetPathDepth(urlPath string) PathDepth {
+ urlPath = path.Clean(urlPath)
+ return PathDepth(len(strings.Split(urlPath, "/")) - 1)
+}
+
+// ParseAddressPath parses address path to address book path and address ID
+func ParseAddressPath(addressPath string) (addressBookPath string, addressID string, err error) {
+ addressBookPath, addressFileName := path.Split(addressPath)
+ addressID = path.Base(addressFileName)
+ addressFileExt := path.Ext(addressFileName)
+
+ if GetPathDepth(addressBookPath) != pathDepth_AddressBook {
+ err = ErrorBookPathInvalid
+ return
+ }
+
+ if addressFileExt != ".vcf" {
+ err = ErrorAddressFileExtensionNameInvalid
+ return
+ }
+
+ return
+}
+
+// AddressPropsFilter filters address properties
+func AddressPropsFilter(address *carddav.AddressObject, req *carddav.AddressDataRequest) *carddav.AddressObject {
+ var card *vcard.Card
+ card = &address.Card
+
+ // if req.AllProp {
+ // card = &address.Card
+ // } else {
+ // card = &vcard.Card{}
+ // for _, prop := range req.Props {
+ // fields := address.Card[prop]
+ // if fields != nil {
+ // for _, field := range fields {
+ // card.Add(prop, field)
+ // }
+ // }
+ // }
+ // }
+
+ return &carddav.AddressObject{
+ Path: address.Path,
+ ModTime: address.ModTime,
+ ContentLength: address.ContentLength,
+ ETag: address.ETag,
+ Card: *card,
+ }
+}
+
+func LoadCards(filePath string) (cards []*vcard.Card, err error) {
+ data, err := os.ReadFile(filePath)
+ if err != nil {
+ logging.LogErrorf("read vCard file [%s] failed: %s", filePath, err)
+ return
+ }
+
+ decoder := vcard.NewDecoder(bytes.NewReader(data))
+ for {
+ card, err := decoder.Decode()
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ logging.LogErrorf("decode vCard file [%s] failed: %s", filePath, err)
+ return nil, err
+ }
+ cards = append(cards, &card)
+ }
+
+ return
+}
+
+type Contacts struct {
+ loaded bool
+ changed bool
+ lock sync.Mutex // load & save
+ books sync.Map // Path -> *AddressBook
+ booksMetaData []*carddav.AddressBook
+}
+
+// load all contacts
+func (c *Contacts) load() error {
+ c.books.Clear()
+ addressBooksMetaDataFilePath := AddressBooksMetaDataFilePath()
+ metaData, err := os.ReadFile(addressBooksMetaDataFilePath)
+ if os.IsNotExist(err) {
+ c.booksMetaData = []*carddav.AddressBook{&defaultAddressBook}
+ if err := c.saveAddressBooksMetaData(); err != nil {
+ return err
+ }
+ } else {
+ // load meta data file
+ c.booksMetaData = []*carddav.AddressBook{}
+ if err = gulu.JSON.UnmarshalJSON(metaData, &c.booksMetaData); err != nil {
+ logging.LogErrorf("unmarshal address books meta data failed: %s", err)
+ return err
+ }
+ }
+
+ wg := &sync.WaitGroup{}
+ wg.Add(len(c.booksMetaData))
+ for _, addressBookMetaData := range c.booksMetaData {
+ addressBook := &AddressBook{
+ Changed: false,
+ DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path),
+ MetaData: addressBookMetaData,
+ Addresses: sync.Map{},
+ }
+ c.books.Store(addressBookMetaData.Path, addressBook)
+ go func() {
+ defer wg.Done()
+ addressBook.load()
+ }()
+ }
+ wg.Wait()
+
+ c.loaded = true
+ c.changed = false
+ return nil
+}
+
+// save all contacts
+func (c *Contacts) save(force bool) error {
+ if force || c.changed {
+ // save address books meta data
+ if err := c.saveAddressBooksMetaData(); err != nil {
+ return err
+ }
+
+ // save all address to *.vbf files
+ wg := &sync.WaitGroup{}
+ c.books.Range(func(path any, book any) bool {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ // path_ := path.(string)
+ book_ := book.(*AddressBook)
+ book_.save(force)
+ }()
+ return true
+ })
+ wg.Wait()
+ c.changed = false
+ }
+ return nil
+}
+
+// save all contacts
+func (c *Contacts) saveAddressBooksMetaData() error {
+ data, err := gulu.JSON.MarshalIndentJSON(c.booksMetaData, "", " ")
+ if err != nil {
+ logging.LogErrorf("marshal address books meta data failed: %s", err)
+ return err
+ }
+
+ dirPath := HomeSetPathPath()
+ if err := os.MkdirAll(dirPath, 0755); err != nil {
+ logging.LogErrorf("create directory [%s] failed: %s", dirPath, err)
+ return err
+ }
+
+ filePath := AddressBooksMetaDataFilePath()
+ if err := os.WriteFile(filePath, data, 0755); err != nil {
+ logging.LogErrorf("write file [%s] failed: %s", filePath, err)
+ return err
+ }
+
+ return nil
+}
+
+func (c *Contacts) Load() error {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ if !c.loaded {
+ return c.load()
+ }
+ return nil
+}
+
+func (c *Contacts) GetAddress(addressPath string) (addressBook *AddressBook, addressObject *AddressObject, err error) {
+ bookPath, addressID, err := ParseAddressPath(addressPath)
+ if err != nil {
+ logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err)
+ return
+ }
+
+ if value, ok := c.books.Load(bookPath); ok {
+ addressBook = value.(*AddressBook)
+ } else {
+ err = ErrorBookNotFound
+ return
+ }
+
+ if value, ok := addressBook.Addresses.Load(addressID); ok {
+ addressObject = value.(*AddressObject)
+ } else {
+ err = ErrorAddressNotFound
+ return
+ }
+
+ return
+}
+
+func (c *Contacts) ListAddressBooks() (addressBooks []carddav.AddressBook, err error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ for _, addressBook := range contacts.booksMetaData {
+ addressBooks = append(addressBooks, *addressBook)
+ }
+ return
+}
+
+func (c *Contacts) GetAddressBook(path string) (addressBook *carddav.AddressBook, err error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ if book, ok := contacts.books.Load(path); ok {
+ addressBook = book.(*AddressBook).MetaData
+ return
+ }
+
+ err = ErrorBookNotFound
+ return
+}
+
+func (c *Contacts) CreateAddressBook(addressBookMetaData *carddav.AddressBook) (err error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ var addressBook *AddressBook
+
+ // update map
+ if value, ok := c.books.Load(addressBookMetaData.Path); ok {
+ // update map item
+ addressBook = value.(*AddressBook)
+ addressBook.MetaData = addressBookMetaData
+ } else {
+ // insert map item
+ addressBook = &AddressBook{
+ Changed: false,
+ DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path),
+ MetaData: addressBookMetaData,
+ Addresses: sync.Map{},
+ }
+ c.books.Store(addressBookMetaData.Path, addressBook)
+ }
+
+ var index = -1
+ for i, item := range c.booksMetaData {
+ if item.Path == addressBookMetaData.Path {
+ index = i
+ break
+ }
+ }
+
+ if index >= 0 {
+ // update list
+ c.booksMetaData[index] = addressBookMetaData
+ } else {
+ // insert list
+ c.booksMetaData = append(c.booksMetaData, addressBookMetaData)
+ }
+
+ // create address book directory
+ if err = os.MkdirAll(addressBook.DirectoryPath, 0755); err != nil {
+ logging.LogErrorf("create directory [%s] failed: %s", addressBook, err)
+ return
+ }
+
+ // save meta data
+ if err = c.saveAddressBooksMetaData(); err != nil {
+ return
+ }
+
+ return
+}
+
+func (c *Contacts) DeleteAddressBook(path string) (err error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ var addressBook *AddressBook
+
+ // delete map item
+ if value, loaded := c.books.LoadAndDelete(path); loaded {
+ addressBook = value.(*AddressBook)
+ }
+
+ // delete list item
+ for i, item := range c.booksMetaData {
+ if item.Path == path {
+ c.booksMetaData = append(c.booksMetaData[:i], c.booksMetaData[i+1:]...)
+ break
+ }
+ }
+
+ // remove address book directory
+ if err = os.RemoveAll(addressBook.DirectoryPath); err != nil {
+ logging.LogErrorf("remove directory [%s] failed: %s", addressBook, err)
+ return
+ }
+
+ // save meta data
+ if err = c.saveAddressBooksMetaData(); err != nil {
+ return
+ }
+
+ return nil
+}
+
+func (c *Contacts) GetAddressObject(addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ _, address, err := c.GetAddress(addressPath)
+ if err != nil {
+ return
+ }
+
+ addressObject = AddressPropsFilter(address.Data, req)
+ return
+}
+
+func (c *Contacts) ListAddressObjects(bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ var addressBook *AddressBook
+ if value, ok := c.books.Load(bookPath); ok {
+ addressBook = value.(*AddressBook)
+ } else {
+ err = ErrorBookNotFound
+ return
+ }
+
+ addressBook.Addresses.Range(func(id any, address any) bool {
+ addressObjects = append(addressObjects, *AddressPropsFilter(address.(*AddressObject).Data, req))
+ return true
+ })
+
+ return
+}
+
+func (c *Contacts) QueryAddressObjects(urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ switch GetPathDepth(urlPath) {
+ case pathDepth_Root, pathDepth_Principals, pathDepth_UserPrincipal, pathDepth_HomeSet:
+ c.books.Range(func(path any, book any) bool {
+ addressBook := book.(*AddressBook)
+ addressBook.Addresses.Range(func(id any, address any) bool {
+ addressObjects = append(addressObjects, *address.(*AddressObject).Data)
+ return true
+ })
+ return true
+ })
+ case pathDepth_AddressBook:
+ if value, ok := c.books.Load(urlPath); ok {
+ addressBook := value.(*AddressBook)
+ addressBook.Addresses.Range(func(id any, address any) bool {
+ addressObjects = append(addressObjects, *address.(*AddressObject).Data)
+ return true
+ })
+ }
+ case pathDepth_Address:
+ if _, address, _ := c.GetAddress(urlPath); address != nil {
+ addressObjects = append(addressObjects, *address.Data)
+ }
+ default:
+ err = ErrorPathInvalid
+ return
+ }
+
+ addressObjects, err = carddav.Filter(query, addressObjects)
+ return
+}
+
+func (c *Contacts) PutAddressObject(addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ bookPath, addressID, err := ParseAddressPath(addressPath)
+ if err != nil {
+ logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err)
+ return
+ }
+
+ var addressBook *AddressBook
+ if value, ok := c.books.Load(bookPath); ok {
+ addressBook = value.(*AddressBook)
+ } else {
+ err = ErrorBookNotFound
+ return
+ }
+
+ var address *AddressObject
+ if value, ok := addressBook.Addresses.Load(addressID); ok {
+ address = value.(*AddressObject)
+
+ if opts.IfNoneMatch.IsSet() {
+ addressObject = address.Data
+ return
+ }
+
+ address.Data.Card = card
+ address.Changed = true
+ } else {
+ address = &AddressObject{
+ Changed: true,
+ FilePath: CardDavPath2DirectoryPath(addressPath),
+ BookPath: bookPath,
+ Data: &carddav.AddressObject{
+ Card: card,
+ },
+ }
+ }
+
+ err = address.save(true)
+ if err != nil {
+ return
+ }
+
+ err = address.update()
+ if err != nil {
+ return
+ }
+
+ addressBook.Addresses.Store(addressID, address)
+ addressObject = address.Data
+ return
+}
+
+func (c *Contacts) DeleteAddressObject(addressPath string) (err error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ _, address, err := c.GetAddress(addressPath)
+ if err != nil && err != ErrorAddressNotFound {
+ return
+ }
+
+ if err = os.Remove(address.FilePath); err != nil {
+ logging.LogErrorf("remove file [%s] failed: %s", address.FilePath, err)
+ return
+ }
+
+ return
+}
+
+type AddressBook struct {
+ Changed bool
+ DirectoryPath string
+ MetaData *carddav.AddressBook
+ Addresses sync.Map // id -> *AddressObject
+}
+
+// load an address book from multiple *.vcf files
+func (b *AddressBook) load() error {
+ if err := os.MkdirAll(b.DirectoryPath, 0755); err != nil {
+ logging.LogErrorf("create directory [%s] failed: %s", b.DirectoryPath, err)
+ return err
+ }
+
+ entries, err := os.ReadDir(b.DirectoryPath)
+ if err != nil {
+ logging.LogErrorf("read dir [%s] failed: %s", b.DirectoryPath, err)
+ return err
+ }
+
+ wg := &sync.WaitGroup{}
+ for _, entry := range entries {
+ if !entry.IsDir() {
+ filename := entry.Name()
+ ext := path.Ext(filename)
+ if ext == ".vcf" {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ // load cards
+ addressFilePath := path.Join(b.DirectoryPath, filename)
+ vCards, err := LoadCards(addressFilePath)
+ if err != nil {
+ return
+ }
+
+ switch len(vCards) {
+ case 0: // invalid file
+ case 1: // file contain 1 card
+ address := &AddressObject{
+ FilePath: addressFilePath,
+ BookPath: b.MetaData.Path,
+ Data: &carddav.AddressObject{
+ Card: *vCards[0],
+ },
+ }
+ if err := address.update(); err != nil {
+ return
+ }
+
+ id := path.Base(filename)
+ b.Addresses.Store(id, address)
+ default: // file contain multiple cards
+ // Create a file for each card
+ addressesWaitGroup := &sync.WaitGroup{}
+ for _, vCard := range vCards {
+ addressesWaitGroup.Add(1)
+ go func() {
+ defer addressesWaitGroup.Done()
+ filename_ := util.AssetName(filename)
+ address := &AddressObject{
+ FilePath: path.Join(b.DirectoryPath, filename_),
+ BookPath: b.MetaData.Path,
+ Data: &carddav.AddressObject{
+ Card: *vCard,
+ },
+ }
+ if err := address.save(true); err != nil {
+ return
+ }
+ if err := address.update(); err != nil {
+ return
+ }
+
+ id := path.Base(filename)
+ b.Addresses.Store(id, address)
+ }()
+ }
+
+ addressesWaitGroup.Wait()
+ // Delete original file with multiple cards
+ if err := os.Remove(addressFilePath); err != nil {
+ logging.LogErrorf("remove file [%s] failed: %s", addressFilePath, err)
+ return
+ }
+ }
+ }()
+ }
+ }
+ }
+ wg.Wait()
+ return nil
+}
+
+// save an address book to multiple *.vcf files
+func (b *AddressBook) save(force bool) error {
+ if force || b.Changed {
+ // create directory
+ if err := os.MkdirAll(b.DirectoryPath, 0755); err != nil {
+ logging.LogErrorf("create directory [%s] failed: %s", b.DirectoryPath, err)
+ return err
+ }
+
+ wg := &sync.WaitGroup{}
+ b.Addresses.Range(func(id any, address any) bool {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ // id_ := id.(string)
+ address_ := address.(*AddressObject)
+ address_.save(force)
+ address_.update()
+ }()
+ return true
+ })
+ wg.Wait()
+ b.Changed = false
+ }
+
+ return nil
+}
+
+type AddressObject struct {
+ Changed bool
+ FilePath string
+ BookPath string
+ Data *carddav.AddressObject
+}
+
+// load an address from *.vcf file
+func (o *AddressObject) load() error {
+ // get file info
+ addressFileInfo, err := os.Stat(o.FilePath)
+ if err != nil {
+ logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err)
+ return err
+ }
+
+ // read file
+ addressData, err := os.ReadFile(o.FilePath)
+ if err != nil {
+ logging.LogErrorf("read file [%s] failed: %s", o.FilePath, err)
+ return err
+ }
+
+ // decode file
+ reader := bytes.NewReader(addressData)
+ decoder := vcard.NewDecoder(reader)
+ card, err := decoder.Decode()
+ if err != nil {
+ logging.LogErrorf("decode file [%s] failed: %s", o.FilePath, err)
+ return err
+ }
+
+ // load data
+ o.Changed = false
+ o.Data = &carddav.AddressObject{
+ Path: path.Join(o.BookPath, addressFileInfo.Name()),
+ ModTime: addressFileInfo.ModTime(),
+ ContentLength: addressFileInfo.Size(),
+ ETag: fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()),
+ Card: card,
+ }
+ return nil
+}
+
+// save an address to *.vcf file
+func (o *AddressObject) save(force bool) error {
+ if force || o.Changed {
+ var addressData bytes.Buffer
+
+ // encode data
+ encoder := vcard.NewEncoder(&addressData)
+ if err := encoder.Encode(o.Data.Card); err != nil {
+ logging.LogErrorf("encode card [%s] failed: %s", o.Data.Path, err)
+ return err
+ }
+
+ // create directory
+ dirPath := path.Dir(o.FilePath)
+ if err := os.MkdirAll(dirPath, 0755); err != nil {
+ logging.LogErrorf("create directory [%s] failed: %s", dirPath, err)
+ return err
+ }
+
+ // write file
+ if err := os.WriteFile(o.FilePath, addressData.Bytes(), 0755); err != nil {
+ logging.LogErrorf("write file [%s] failed: %s", o.FilePath, err)
+ return err
+ }
+
+ o.Changed = false
+ }
+ return nil
+}
+
+// update file info
+func (o *AddressObject) update() error {
+ // update file info
+ addressFileInfo, err := os.Stat(o.FilePath)
+ if err != nil {
+ logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err)
+ return err
+ }
+
+ o.Data.Path = path.Join(o.BookPath, addressFileInfo.Name())
+ o.Data.ModTime = addressFileInfo.ModTime()
+ o.Data.ContentLength = addressFileInfo.Size()
+ o.Data.ETag = fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size())
+
+ return nil
+}
+
+type CardDavBackend struct{}
+
+func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
+ // logging.LogDebugf("CardDAV CurrentUserPrincipal")
+ return CardDavUserPrincipalPath, nil
+}
+
+func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) {
+ // logging.LogDebugf("CardDAV AddressBookHomeSetPath")
+ return CardDavHomeSetPath, nil
+}
+
+func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []carddav.AddressBook, err error) {
+ // logging.LogDebugf("CardDAV ListAddressBooks")
+ if err = contacts.Load(); err != nil {
+ return
+ }
+
+ addressBooks, err = contacts.ListAddressBooks()
+ // logging.LogDebugf("CardDAV ListAddressBooks <- addressBooks: %#v, err: %s", addressBooks, err)
+ return
+}
+
+func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (addressBook *carddav.AddressBook, err error) {
+ // logging.LogDebugf("CardDAV GetAddressBook -> bookPath: %s", bookPath)
+ if err = contacts.Load(); err != nil {
+ return
+ }
+
+ addressBook, err = contacts.GetAddressBook(bookPath)
+ // logging.LogDebugf("CardDAV GetAddressBook <- addressBook: %#v, err: %s", addressBook, err)
+ return
+}
+
+func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) {
+ // logging.LogDebugf("CardDAV CreateAddressBook -> addressBook: %#v", addressBook)
+ if err = contacts.Load(); err != nil {
+ return
+ }
+
+ err = contacts.CreateAddressBook(addressBook)
+ // logging.LogDebugf("CardDAV CreateAddressBook <- err: %s", err)
+ return
+}
+
+func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) (err error) {
+ // logging.LogDebugf("CardDAV DeleteAddressBook -> bookPath: %s", bookPath)
+ if err = contacts.Load(); err != nil {
+ return
+ }
+
+ err = contacts.DeleteAddressBook(bookPath)
+ // logging.LogDebugf("CardDAV DeleteAddressBook <- err: %s", err)
+ return
+}
+
+func (b *CardDavBackend) GetAddressObject(ctx context.Context, addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
+ // logging.LogDebugf("CardDAV GetAddressObject -> addressPath: %s, req: %#v", addressPath, req)
+ if err = contacts.Load(); err != nil {
+ return
+ }
+
+ addressObject, err = contacts.GetAddressObject(addressPath, req)
+ // logging.LogDebugf("CardDAV GetAddressObject <- addressObject: %#v, err: %s", addressObject, err)
+ return
+}
+
+func (b *CardDavBackend) ListAddressObjects(ctx context.Context, bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
+ // logging.LogDebugf("CardDAV ListAddressObjects -> bookPath: %s, req: %#v", bookPath, req)
+ if err = contacts.Load(); err != nil {
+ return
+ }
+
+ addressObjects, err = contacts.ListAddressObjects(bookPath, req)
+ // logging.LogDebugf("CardDAV ListAddressObjects <- addressObjects: %#v, err: %s", addressObjects, err)
+ return
+}
+
+func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
+ // logging.LogDebugf("CardDAV QueryAddressObjects -> urlPath: %s, query: %#v", urlPath, query)
+ if err = contacts.Load(); err != nil {
+ return
+ }
+
+ addressObjects, err = contacts.QueryAddressObjects(urlPath, query)
+ // logging.LogDebugf("CardDAV QueryAddressObjects <- addressObjects: %#v, err: %s", addressObjects, err)
+ return
+}
+
+func (b *CardDavBackend) PutAddressObject(ctx context.Context, addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
+ // logging.LogDebugf("CardDAV PutAddressObject -> addressPath: %s, card: %#v, opts: %#v", addressPath, card, opts)
+ if err = contacts.Load(); err != nil {
+ return
+ }
+
+ addressObject, err = contacts.PutAddressObject(addressPath, card, opts)
+ // logging.LogDebugf("CardDAV PutAddressObject <- addressObject: %#v, err: %s", addressObject, err)
+ return
+}
+
+func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, addressPath string) (err error) {
+ // logging.LogDebugf("CardDAV DeleteAddressObject -> addressPath: %s", addressPath)
+ if err = contacts.Load(); err != nil {
+ return
+ }
+
+ err = contacts.DeleteAddressObject(addressPath)
+ // logging.LogDebugf("CardDAV DeleteAddressObject <- err: %s", err)
+ return
+}
diff --git a/kernel/model/import.go b/kernel/model/import.go
index 58956edcf..bb6e43d3c 100644
--- a/kernel/model/import.go
+++ b/kernel/model/import.go
@@ -27,6 +27,7 @@ import (
"image/png"
"io"
"io/fs"
+ "net/url"
"os"
"path"
"path/filepath"
@@ -798,6 +799,11 @@ func ImportFromLocalPath(boxID, localPath string, toPath string) (err error) {
if "" != yfmTitle {
title = yfmTitle
}
+ unescapedTitle, unescapeErr := url.QueryUnescape(title)
+ if nil == unescapeErr {
+ title = unescapedTitle
+ }
+ hPath = path.Join(path.Dir(hPath), title)
updated := yfmUpdated
fname := path.Base(targetPath)
targetPath = strings.ReplaceAll(targetPath, fname, id+".sy")
@@ -915,6 +921,10 @@ func ImportFromLocalPath(boxID, localPath string, toPath string) (err error) {
if "" != yfmTitle {
title = yfmTitle
}
+ unescapedTitle, unescapeErr := url.QueryUnescape(title)
+ if nil == unescapeErr {
+ title = unescapedTitle
+ }
updated := yfmUpdated
fname := path.Base(targetPath)
targetPath = strings.ReplaceAll(targetPath, fname, id+".sy")
@@ -1344,8 +1354,8 @@ func convertTags(text string) (ret string) {
// buildBlockRefInText 将文本节点进行结构化处理。
func buildBlockRefInText() {
- lute := NewLute()
- lute.SetHTMLTag2TextMark(true)
+ luteEngine := NewLute()
+ luteEngine.SetHTMLTag2TextMark(true)
for _, tree := range importTrees {
tree.MergeText()
@@ -1359,7 +1369,7 @@ func buildBlockRefInText() {
return ast.WalkContinue
}
- t := parse.Inline("", n.Tokens, lute.ParseOptions) // 使用行级解析
+ t := parse.Inline("", n.Tokens, luteEngine.ParseOptions) // 使用行级解析
parse.NestedInlines2FlattedSpans(t, false)
var children []*ast.Node
for c := t.Root.FirstChild.FirstChild; nil != c; c = c.Next {
diff --git a/kernel/model/session.go b/kernel/model/session.go
index ef0d42440..a86d3268a 100644
--- a/kernel/model/session.go
+++ b/kernel/model/session.go
@@ -34,6 +34,11 @@ import (
"github.com/steambap/captcha"
)
+var (
+ BasicAuthHeaderKey = "WWW-Authenticate"
+ BasicAuthHeaderValue = "Basic realm=\"SiYuan Authorization Require\", charset=\"UTF-8\""
+)
+
func LogoutAuth(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
@@ -300,8 +305,8 @@ func CheckAuth(c *gin.Context) {
}
// WebDAV BasicAuth Authenticate
- if strings.HasPrefix(c.Request.RequestURI, "/webdav") {
- c.Header("WWW-Authenticate", "Basic realm=Authorization Required")
+ if strings.HasPrefix(c.Request.RequestURI, "/webdav") || strings.HasPrefix(c.Request.RequestURI, "/carddav") {
+ c.Header(BasicAuthHeaderKey, BasicAuthHeaderValue)
c.AbortWithStatus(http.StatusUnauthorized)
return
}
diff --git a/kernel/server/proxy/publish.go b/kernel/server/proxy/publish.go
index 52506270e..65d9a7666 100644
--- a/kernel/server/proxy/publish.go
+++ b/kernel/server/proxy/publish.go
@@ -21,7 +21,6 @@ import (
"net"
"net/http"
"net/http/httputil"
- "strconv"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/model"
@@ -143,7 +142,7 @@ func (PublishServiceTransport) RoundTrip(request *http.Request) (response *http.
ProtoMinor: request.ProtoMinor,
Request: request,
Header: http.Header{
- "WWW-Authenticate": {"Basic realm=" + strconv.Quote("Authorization Required")},
+ model.BasicAuthHeaderKey: {model.BasicAuthHeaderValue},
},
Close: false,
ContentLength: -1,
diff --git a/kernel/server/serve.go b/kernel/server/serve.go
index 4a42d1691..3d1bb676f 100644
--- a/kernel/server/serve.go
+++ b/kernel/server/serve.go
@@ -32,6 +32,7 @@ import (
"time"
"github.com/88250/gulu"
+ "github.com/emersion/go-webdav/carddav"
"github.com/gin-contrib/gzip"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
@@ -47,17 +48,63 @@ import (
"golang.org/x/net/webdav"
)
+const (
+ MethodMkcol = "MKCOL"
+ MethodCopy = "COPY"
+ MethodMove = "MOVE"
+ MethodLock = "LOCK"
+ MethodUnlock = "UNLOCK"
+ MethodPropFind = "PROPFIND"
+ MethodPropPatch = "PROPPATCH"
+ MethodReport = "REPORT"
+)
+
var (
- cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
- WebDavMethod = []string{
- "OPTIONS",
- "GET", "HEAD",
- "POST", "PUT",
- "DELETE",
- "MKCOL",
- "COPY", "MOVE",
- "LOCK", "UNLOCK",
- "PROPFIND", "PROPPATCH",
+ cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf"))
+ HttpMethods = []string{
+ http.MethodGet,
+ http.MethodHead,
+ http.MethodPost,
+ http.MethodPut,
+ http.MethodPatch,
+ http.MethodDelete,
+ http.MethodConnect,
+ http.MethodOptions,
+ http.MethodTrace,
+ }
+ WebDavMethods = []string{
+ http.MethodOptions,
+ http.MethodHead,
+ http.MethodGet,
+ http.MethodPost,
+ http.MethodPut,
+ http.MethodDelete,
+
+ MethodMkcol,
+ MethodCopy,
+ MethodMove,
+ MethodLock,
+ MethodUnlock,
+ MethodPropFind,
+ MethodPropPatch,
+ }
+ CardDavMethods = []string{
+ http.MethodOptions,
+ http.MethodHead,
+ http.MethodGet,
+ http.MethodPost,
+ http.MethodPut,
+ http.MethodDelete,
+
+ MethodMkcol,
+ // MethodCopy,
+ // MethodMove,
+ // MethodLock,
+ // MethodUnlock,
+ MethodPropFind,
+ MethodPropPatch,
+
+ MethodReport,
}
)
@@ -88,6 +135,7 @@ func Serve(fastMode bool) {
serveAppearance(ginServer)
serveWebSocket(ginServer)
serveWebDAV(ginServer)
+ serveCardDAV(ginServer)
serveExport(ginServer)
serveWidgets(ginServer)
servePlugins(ginServer)
@@ -616,10 +664,19 @@ func serveWebDAV(ginServer *gin.Engine) {
}
ginGroup := ginServer.Group("/webdav", model.CheckAuth, model.CheckAdminRole)
- ginGroup.Match(WebDavMethod, "/*path", func(c *gin.Context) {
+ // ginGroup.Any NOT support extension methods (PROPFIND etc.)
+ ginGroup.Match(WebDavMethods, "/*path", func(c *gin.Context) {
if util.ReadOnly {
switch c.Request.Method {
- case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH":
+ case http.MethodPost,
+ http.MethodPut,
+ http.MethodDelete,
+ MethodMkcol,
+ MethodCopy,
+ MethodMove,
+ MethodLock,
+ MethodUnlock,
+ MethodPropPatch:
c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34)))
return
}
@@ -628,6 +685,40 @@ func serveWebDAV(ginServer *gin.Engine) {
})
}
+func serveCardDAV(ginServer *gin.Engine) {
+ // REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
+ handler := carddav.Handler{
+ Backend: &model.CardDavBackend{},
+ Prefix: model.CardDavPrincipalsPath,
+ }
+
+ ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) {
+ handler.ServeHTTP(c.Writer, c.Request)
+ })
+
+ ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole)
+ ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) {
+ // logging.LogDebugf("CardDAV -> [%s] %s", c.Request.Method, c.Request.URL.String())
+ if util.ReadOnly {
+ switch c.Request.Method {
+ case http.MethodPost,
+ http.MethodPut,
+ http.MethodDelete,
+ MethodMkcol,
+ MethodCopy,
+ MethodMove,
+ MethodLock,
+ MethodUnlock,
+ MethodPropPatch:
+ c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34)))
+ return
+ }
+ }
+ handler.ServeHTTP(c.Writer, c.Request)
+ // logging.LogDebugf("CardDAV <- [%s] %v", c.Request.Method, c.Writer.Status())
+ })
+}
+
func shortReqMsg(msg []byte) []byte {
s := gulu.Str.FromBytes(msg)
max := 128
@@ -644,15 +735,32 @@ func shortReqMsg(msg []byte) []byte {
}
func corsMiddleware() gin.HandlerFunc {
- return func(c *gin.Context) {
+ allowMethods := strings.Join(HttpMethods, ", ")
+ allowWebDavMethods := strings.Join(WebDavMethods, ", ")
+ allowCardDavMethods := strings.Join(CardDavMethods, ", ")
+ return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers", "origin, Content-Length, Content-Type, Authorization")
- c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS")
c.Header("Access-Control-Allow-Private-Network", "true")
- if c.Request.Method == "OPTIONS" {
+ if strings.HasPrefix(c.Request.RequestURI, "/webdav/") {
+ c.Header("Access-Control-Allow-Methods", allowWebDavMethods)
+ c.Next()
+ return
+ }
+
+ if strings.HasPrefix(c.Request.RequestURI, "/carddav/") {
+ c.Header("Access-Control-Allow-Methods", allowCardDavMethods)
+ c.Next()
+ return
+ }
+
+ c.Header("Access-Control-Allow-Methods", allowMethods)
+
+ switch c.Request.Method {
+ case http.MethodOptions:
c.Header("Access-Control-Max-Age", "600")
c.AbortWithStatus(204)
return