Files
siyuan/kernel/api/search.go
Yuxin Zhao 3facc37df9 Publish service supports document-level access control (#16041)
* 🎨 improve publish service https://github.com/siyuan-note/siyuan/issues/11928

* fix: change publish access storage

* 🎨 publish access control #16041

* fix: publish access in embed block and search

* fix: multiple lock in pubish service & insert locked content in publish service

* fix: multiple lock

* fix: password in embed block

* fix: locked and forbidden docs in gallery

* fix: dont use publish access block

* fix: attribute view of publish disabled docs

* fix: disable docs in table and gallery

* fix: locked docs in attribute view

* fix: purge publish access

* fix: disable docs in table and gallery groups

* fix: locked and disabled docs in attribute view, outline and preview

* fix: create publish_access.go

* fix: move publish auth to publish_access.go

* fix: tag and ref of locked docs

* fix: backlink of locked docs

* fix: search in locked docs

* fix: search history and asset

* fix: copy to markdown

* fix: hide command panel

* fix: publish access control in mobile

* fix: recent docs of invisible and locked docs

* fix: backlink in mobile

* fix: empty Tab

* fix: get locked and forbidden doc assets and files

* fix: refblock & asset

* fix: disable /api/file/sql in publish mode

* fix: publish access of kanban

* fix: adjustment for review

* fix: /api/filetree/setPublishAccess return error meesage

* fix: move the publishAccess button to the More Menu & revert the fix for barCommand/menuCommand

* fix: hotkey of command panel
2026-03-12 18:04:01 +08:00

523 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 <https://www.gnu.org/licenses/>.
package api
import (
"net/http"
"strings"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func listInvalidBlockRefs(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
page := 1
if nil != arg["page"] {
page = int(arg["page"].(float64))
}
if 0 >= page {
page = 1
}
pageSize := 32
if nil != arg["pageSize"] {
pageSize = int(arg["pageSize"].(float64))
}
if 0 >= pageSize {
pageSize = 32
}
blocks, matchedBlockCount, matchedRootCount, pageCount := model.ListInvalidBlockRefs(page, pageSize)
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
blocks = model.FilterBlocksByPublishAccess(c, publishAccess, blocks)
}
ret.Data = map[string]interface{}{
"blocks": blocks,
"matchedBlockCount": matchedBlockCount,
"matchedRootCount": matchedRootCount,
"pageCount": pageCount,
}
}
func getAssetContent(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
query := arg["query"].(string)
queryMethod := int(arg["queryMethod"].(float64))
assetContent := model.GetAssetContent(id, query, queryMethod)
if model.IsReadOnlyRoleContext(c) && assetContent != nil {
publishAccess := model.GetPublishAccess()
filteredAssetContents := model.FilterAssetContentByPublishAccess(c, publishAccess, []*model.AssetContent{assetContent})
if len(filteredAssetContents) > 0 {
assetContent = filteredAssetContents[0]
} else {
assetContent = nil
}
}
ret.Data = map[string]interface{}{
"assetContent": assetContent,
}
return
}
func fullTextSearchAssetContent(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
page, pageSize, query, types, method, orderBy := parseSearchAssetContentArgs(arg)
assetContents, matchedAssetCount, pageCount := model.FullTextSearchAssetContent(query, types, method, orderBy, page, pageSize)
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
assetContents = model.FilterAssetContentByPublishAccess(c, publishAccess, assetContents)
}
ret.Data = map[string]interface{}{
"assetContents": assetContents,
"matchedAssetCount": matchedAssetCount,
"pageCount": pageCount,
}
}
func findReplace(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
_, _, _, paths, boxes, types, method, orderBy, groupBy := parseSearchBlockArgs(arg)
k := arg["k"].(string)
r := arg["r"].(string)
idsArg := arg["ids"].([]interface{})
var ids []string
for _, id := range idsArg {
ids = append(ids, id.(string))
}
replaceTypes := map[string]bool{}
// text, imgText, imgTitle, imgSrc, aText, aTitle, aHref, code, em, strong, inlineMath, inlineMemo, blockRef, fileAnnotationRef kbd, mark, s, sub, sup, tag, u
// docTitle, codeBlock, mathBlock, htmlBlock
if nil != arg["replaceTypes"] {
replaceTypesArg := arg["replaceTypes"].(map[string]interface{})
for t, b := range replaceTypesArg {
replaceTypes[t] = b.(bool)
}
}
err := model.FindReplace(k, r, replaceTypes, ids, paths, boxes, types, method, orderBy, groupBy)
if err != nil {
ret.Code = 1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
return
}
func searchAsset(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
k := arg["k"].(string)
var exts []string
if extsArg := arg["exts"]; nil != extsArg {
for _, ext := range extsArg.([]interface{}) {
exts = append(exts, ext.(string))
}
}
ret.Data = model.SearchAssetsByName(k, exts)
return
}
func searchTag(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
k := arg["k"].(string)
tags := model.SearchTags(k)
if 1 > len(tags) {
tags = []string{}
}
ret.Data = map[string]interface{}{
"tags": tags,
"k": k,
}
}
func searchWidget(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
keyword := arg["k"].(string)
widgets := model.SearchWidget(keyword)
ret.Data = map[string]interface{}{
"widgets": widgets,
"k": keyword,
}
}
func removeTemplate(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
path := arg["path"].(string)
err := model.RemoveTemplate(path)
if err != nil {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func searchTemplate(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
keyword := arg["k"].(string)
templates := model.SearchTemplate(keyword)
ret.Data = map[string]interface{}{
"templates": templates,
"k": keyword,
}
}
func getEmbedBlock(c *gin.Context) {
// Query embed block supports executing JavaScript https://github.com/siyuan-note/siyuan/issues/9648
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
embedBlockID := arg["embedBlockID"].(string)
includeIDsArg := arg["includeIDs"].([]interface{})
var includeIDs []string
for _, includeID := range includeIDsArg {
includeIDs = append(includeIDs, includeID.(string))
}
headingMode := 0 // 0显示标题与下方的块1仅显示标题2仅显示标题下方的块
headingModeArg := arg["headingMode"]
if nil != headingModeArg {
headingMode = int(headingModeArg.(float64))
}
breadcrumb := false
breadcrumbArg := arg["breadcrumb"]
if nil != breadcrumbArg {
breadcrumb = breadcrumbArg.(bool)
}
blocks := model.GetEmbedBlock(embedBlockID, includeIDs, headingMode, breadcrumb)
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
blocks = model.FilterEmbedBlocksByPublishAccess(c, publishAccess, blocks)
}
ret.Data = map[string]interface{}{
"blocks": blocks,
}
}
func updateEmbedBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
content := arg["content"].(string)
err := model.UpdateEmbedBlock(id, content)
if err != nil {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func searchEmbedBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
embedBlockID := arg["embedBlockID"].(string)
stmt := arg["stmt"].(string)
excludeIDsArg := arg["excludeIDs"].([]interface{})
var excludeIDs []string
for _, excludeID := range excludeIDsArg {
if nil == excludeID {
continue
}
excludeIDs = append(excludeIDs, excludeID.(string))
}
headingMode := 0 // 0显示标题与下方的块1仅显示标题2仅显示标题下方的块
headingModeArg := arg["headingMode"]
if nil != headingModeArg {
headingMode = int(headingModeArg.(float64))
}
breadcrumb := false
breadcrumbArg := arg["breadcrumb"]
if nil != breadcrumbArg {
breadcrumb = breadcrumbArg.(bool)
}
blocks := model.SearchEmbedBlock(embedBlockID, stmt, excludeIDs, headingMode, breadcrumb)
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
blocks = model.FilterEmbedBlocksByPublishAccess(c, publishAccess, blocks)
}
ret.Data = map[string]interface{}{
"blocks": blocks,
}
}
func searchRefBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
reqId := arg["reqId"]
ret.Data = map[string]interface{}{"reqId": reqId}
if nil == arg["id"] {
return
}
isSquareBrackets := false
if isSquareBracketsArg := arg["isSquareBrackets"]; nil != isSquareBracketsArg {
isSquareBrackets = isSquareBracketsArg.(bool)
}
isDatabase := false
if isDatabaseArg := arg["isDatabase"]; nil != isDatabaseArg {
isDatabase = isDatabaseArg.(bool)
}
rootID := arg["rootID"].(string)
id := arg["id"].(string)
keyword := arg["k"].(string)
beforeLen := int(arg["beforeLen"].(float64))
blocks, newDoc := model.SearchRefBlock(id, rootID, keyword, beforeLen, isSquareBrackets, isDatabase)
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
blocks = model.FilterBlocksByPublishAccess(c, publishAccess, blocks)
}
ret.Data = map[string]interface{}{
"blocks": blocks,
"newDoc": newDoc,
"k": util.EscapeHTML(keyword),
"reqId": arg["reqId"],
}
}
func fullTextSearchBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
page, pageSize, query, paths, boxes, types, method, orderBy, groupBy := parseSearchBlockArgs(arg)
blocks, matchedBlockCount, matchedRootCount, pageCount, docMode := model.FullTextSearchBlock(query, boxes, paths, types, method, orderBy, groupBy, page, pageSize)
if model.IsReadOnlyRoleContext(c) {
publishAccess := model.GetPublishAccess()
blocks = model.FilterBlocksByPublishAccess(c, publishAccess, blocks)
}
ret.Data = map[string]interface{}{
"blocks": blocks,
"matchedBlockCount": matchedBlockCount,
"matchedRootCount": matchedRootCount,
"pageCount": pageCount,
"docMode": docMode,
}
}
func parseSearchBlockArgs(arg map[string]interface{}) (page, pageSize int, query string, paths, boxes []string, types map[string]bool, method, orderBy, groupBy int) {
page = 1
if nil != arg["page"] {
page = int(arg["page"].(float64))
}
if 0 >= page {
page = 1
}
pageSize = 32
if nil != arg["pageSize"] {
pageSize = int(arg["pageSize"].(float64))
}
if 0 >= pageSize {
pageSize = 32
}
queryArg := arg["query"]
if nil != queryArg {
query = queryArg.(string)
}
pathsArg := arg["paths"]
if nil != pathsArg {
for _, p := range pathsArg.([]interface{}) {
path := p.(string)
box := strings.TrimSpace(strings.Split(path, "/")[0])
if "" != box {
boxes = append(boxes, box)
}
path = strings.TrimSpace(strings.TrimPrefix(path, box))
if "" != path {
paths = append(paths, path)
}
}
paths = gulu.Str.RemoveDuplicatedElem(paths)
boxes = gulu.Str.RemoveDuplicatedElem(boxes)
}
if nil != arg["types"] {
typesArg := arg["types"].(map[string]interface{})
types = map[string]bool{}
for t, b := range typesArg {
types[t] = b.(bool)
}
}
// method0关键字1查询语法2SQL3正则表达式
methodArg := arg["method"]
if nil != methodArg {
method = int(methodArg.(float64))
}
// orderBy0按块类型默认1按创建时间升序2按创建时间降序3按更新时间升序4按更新时间降序5按内容顺序仅在按文档分组时6按相关度升序7按相关度降序
orderByArg := arg["orderBy"]
if nil != orderByArg {
orderBy = int(orderByArg.(float64))
}
// groupBy 0不分组1按文档分组
groupByArg := arg["groupBy"]
if nil != groupByArg {
groupBy = int(groupByArg.(float64))
}
return
}
func parseSearchAssetContentArgs(arg map[string]interface{}) (page, pageSize int, query string, types map[string]bool, method, orderBy int) {
page = 1
if nil != arg["page"] {
page = int(arg["page"].(float64))
}
if 0 >= page {
page = 1
}
pageSize = 32
if nil != arg["pageSize"] {
pageSize = int(arg["pageSize"].(float64))
}
if 0 >= pageSize {
pageSize = 32
}
queryArg := arg["query"]
if nil != queryArg {
query = queryArg.(string)
}
if nil != arg["types"] {
typesArg := arg["types"].(map[string]interface{})
types = map[string]bool{}
for t, b := range typesArg {
types[t] = b.(bool)
}
}
// method0关键字1查询语法2SQL3正则表达式
methodArg := arg["method"]
if nil != methodArg {
method = int(methodArg.(float64))
}
// orderBy0按相关度降序1按相关度升序2按更新时间升序3按更新时间降序
orderByArg := arg["orderBy"]
if nil != orderByArg {
orderBy = int(orderByArg.(float64))
}
return
}