mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-07-03 14:09:06 +02:00
b549020e1d
Signed-off-by: Daniel <845765@qq.com>
935 lines
27 KiB
Go
935 lines
27 KiB
Go
// 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 model
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/88250/gulu"
|
|
"github.com/88250/lute/ast"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/siyuan-note/filelock"
|
|
"github.com/siyuan-note/logging"
|
|
"github.com/siyuan-note/siyuan/kernel/av"
|
|
"github.com/siyuan-note/siyuan/kernel/conf"
|
|
"github.com/siyuan-note/siyuan/kernel/sql"
|
|
"github.com/siyuan-note/siyuan/kernel/treenode"
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
)
|
|
|
|
type PublishAccessItem struct {
|
|
ID string `json:"id"`
|
|
Visible bool `json:"visible"` // 是否发布可见
|
|
Password string `json:"password"` // 密码,为空字符串时表示无密码
|
|
Disable bool `json:"disable"` // 是否禁止发布
|
|
}
|
|
|
|
type PublishAccess []*PublishAccessItem
|
|
|
|
var (
|
|
publishAccessLastModified int64
|
|
publishAccess PublishAccess
|
|
publishAccessLock = sync.Mutex{}
|
|
)
|
|
|
|
func GetPublishAccess() (ret PublishAccess) {
|
|
ret = PublishAccess{}
|
|
now := time.Now().UnixMilli()
|
|
if now-publishAccessLastModified < 30*1000 {
|
|
return publishAccess
|
|
}
|
|
|
|
publishAccessLock.Lock()
|
|
defer publishAccessLock.Unlock()
|
|
|
|
publishAccessLastModified = now
|
|
|
|
publishAccessPath := filepath.Join(util.DataDir, ".siyuan", "publishAccess.json")
|
|
err := os.MkdirAll(filepath.Dir(publishAccessPath), 0755)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if !filelock.IsExist(publishAccessPath) {
|
|
if err = filelock.WriteFile(publishAccessPath, []byte("[]")); err != nil {
|
|
logging.LogErrorf("create publishAccess.json [%s] failed: %s", publishAccessPath, err)
|
|
return
|
|
}
|
|
}
|
|
data, err := os.ReadFile(publishAccessPath)
|
|
if err != nil {
|
|
logging.LogErrorf("read publishAccess.json [%s] failed: %s", publishAccessPath, err)
|
|
return
|
|
}
|
|
if err = gulu.JSON.UnmarshalJSON(data, &publishAccess); err != nil {
|
|
logging.LogWarnf("unmarshal publishAccess.json failed: %s", err)
|
|
return
|
|
}
|
|
ret = publishAccess
|
|
return
|
|
}
|
|
|
|
func SetPublishAccess(inputPublishAccess PublishAccess) (err error) {
|
|
now := time.Now().UnixMilli()
|
|
publishAccessLock.Lock()
|
|
defer publishAccessLock.Unlock()
|
|
publishAccessLastModified = now
|
|
publishAccess = inputPublishAccess
|
|
|
|
publishAccessPath := filepath.Join(util.DataDir, ".siyuan", "publishAccess.json")
|
|
err = os.MkdirAll(filepath.Dir(publishAccessPath), 0755)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("create dir for publishAccess.json [%s] failed: %s", publishAccessPath, err)
|
|
logging.LogErrorf(msg)
|
|
err = errors.New(msg)
|
|
return
|
|
}
|
|
|
|
data, err := gulu.JSON.MarshalJSON(inputPublishAccess)
|
|
if err != nil {
|
|
logging.LogErrorf("marshal publishAccess.json [%s] failed: %s", publishAccessPath, err)
|
|
return
|
|
}
|
|
|
|
err = filelock.WriteFile(publishAccessPath, data)
|
|
if err != nil {
|
|
msg := fmt.Sprintf("write publishAccess.json [%s] failed: %s", publishAccessPath, err)
|
|
logging.LogErrorf(msg)
|
|
err = errors.New(msg)
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetInvisiblePublishAccess(inputPublishAccess PublishAccess) (outputPublishAccess PublishAccess) {
|
|
outputPublishAccess = PublishAccess{}
|
|
for _, item := range inputPublishAccess {
|
|
if !item.Visible {
|
|
outputPublishAccess = append(outputPublishAccess, item)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetDisablePublishAccess(inputPublishAccess PublishAccess) (outputPublishAccess PublishAccess) {
|
|
outputPublishAccess = PublishAccess{}
|
|
for _, item := range inputPublishAccess {
|
|
if item.Disable {
|
|
outputPublishAccess = append(outputPublishAccess, item)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func PurgePublishAccess() {
|
|
publishAccess := GetPublishAccess()
|
|
IDs := []string{}
|
|
for _, item := range publishAccess {
|
|
IDs = append(IDs, item.ID)
|
|
}
|
|
|
|
boxes, err := ListNotebooks()
|
|
if err != nil {
|
|
return
|
|
}
|
|
// 必须在所有笔记本都打开的情况下才能执行清除工作,否则会把关闭的笔记本里文档的发布访问控制状态清除
|
|
for _, box := range boxes {
|
|
if box.Closed {
|
|
return
|
|
}
|
|
}
|
|
|
|
checkResult := treenode.ExistBlockTrees(IDs)
|
|
tempPublishAccess := PublishAccess{}
|
|
for i, ID := range IDs {
|
|
if exists, ok := checkResult[ID]; ok && exists {
|
|
tempPublishAccess = append(tempPublishAccess, publishAccess[i])
|
|
} else {
|
|
for _, box := range boxes {
|
|
if box.ID == ID {
|
|
tempPublishAccess = append(tempPublishAccess, publishAccess[i])
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
SetPublishAccess(tempPublishAccess)
|
|
return
|
|
}
|
|
|
|
func CheckPathAccessableByPublishIgnore(box string, path string, publishIgnore PublishAccess) bool {
|
|
for _, item := range publishIgnore {
|
|
if item.ID == box || strings.Contains(path, item.ID) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func GetPathPasswordByPublishAccess(box string, blockPath string, publishAccess PublishAccess) (passwordID string, password string) {
|
|
currentPath := blockPath
|
|
password = ""
|
|
passwordID = ""
|
|
for currentPath != "/" && password == "" {
|
|
currentID := strings.TrimSuffix(path.Base(currentPath), ".sy")
|
|
for _, accessItem := range publishAccess {
|
|
if accessItem.ID == currentID {
|
|
password = accessItem.Password
|
|
passwordID = accessItem.ID
|
|
break
|
|
}
|
|
}
|
|
currentPath = path.Dir(currentPath)
|
|
}
|
|
if password == "" {
|
|
for _, accessItem := range publishAccess {
|
|
if accessItem.ID == box {
|
|
password = accessItem.Password
|
|
passwordID = accessItem.ID
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func CheckBlockIdAccessableByPublishAccess(c *gin.Context, publishAccess PublishAccess, blockID string) bool {
|
|
publishIgnore := GetDisablePublishAccess(publishAccess)
|
|
bt := treenode.GetBlockTree(blockID)
|
|
if bt == nil {
|
|
return false
|
|
}
|
|
passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
|
|
return CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) && (password == "" || CheckPublishAuthCookie(c, passwordID, password))
|
|
}
|
|
|
|
func SetPublishAuthCookie(c *gin.Context, ID string, password string) {
|
|
authCookie := util.SHA256Hash([]byte(ID + password))
|
|
http.SetCookie(c.Writer, &http.Cookie{
|
|
Name: "publish-auth-" + ID,
|
|
Value: authCookie,
|
|
MaxAge: 24 * 60 * 60,
|
|
Path: "/",
|
|
Secure: util.SSL,
|
|
HttpOnly: true,
|
|
})
|
|
}
|
|
|
|
func CheckPublishAuthCookie(c *gin.Context, ID string, password string) bool {
|
|
authCookie, err := c.Request.Cookie("publish-auth-" + ID)
|
|
return err == nil && authCookie.Value == util.SHA256Hash([]byte(ID+password))
|
|
}
|
|
|
|
func CheckAbsPathAccessableByPublishAccess(c *gin.Context, absPath string, publishAccess PublishAccess) bool {
|
|
absPath = filepath.Clean(absPath)
|
|
|
|
if gulu.File.IsSubPath(util.HistoryDir, absPath) {
|
|
return false
|
|
}
|
|
|
|
if gulu.File.IsSubPath(util.DataDir, absPath) {
|
|
relPath, err := filepath.Rel(util.DataDir, absPath)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
|
|
relPath = strings.ReplaceAll(relPath, "\\", "/")
|
|
|
|
pathParts := strings.Split(relPath, "/")
|
|
if len(pathParts) <= 1 {
|
|
return true
|
|
}
|
|
|
|
if ast.IsNodeIDPattern(pathParts[0]) {
|
|
box := pathParts[0]
|
|
blockPath := "/" + strings.Join(pathParts[1:], "/")
|
|
passwordID, password := GetPathPasswordByPublishAccess(box, blockPath, publishAccess)
|
|
publishIgnore := GetDisablePublishAccess(publishAccess)
|
|
return CheckPathAccessableByPublishIgnore(box, blockPath, publishIgnore) && (password == "" || CheckPublishAuthCookie(c, passwordID, password))
|
|
} else if pathParts[0] == "assets" {
|
|
publishIgnore := GetDisablePublishAccess(publishAccess)
|
|
bts := treenode.GetBlockTreesByType("d")
|
|
for _, bt := range bts {
|
|
passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
|
|
if CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) && (password == "" || CheckPublishAuthCookie(c, passwordID, password)) {
|
|
assets, _ := DocAssets(bt.ID, false)
|
|
for _, assetPath := range assets {
|
|
if assetPath == relPath {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func FilterViewByPublishAccess(c *gin.Context, publishAccess PublishAccess, viewable av.Viewable) (ret av.Viewable) {
|
|
ret = viewable
|
|
publishIgnore := GetDisablePublishAccess(publishAccess)
|
|
|
|
switch ret.GetType() {
|
|
case av.LayoutTypeTable:
|
|
table := ret.(*av.Table)
|
|
filteredRows := []*av.TableRow{}
|
|
for _, row := range table.Rows {
|
|
// 默认第一个属性是文档块
|
|
var bt *treenode.BlockTree
|
|
if len(row.Cells) > 0 {
|
|
if row.Cells[0].Value.Block != nil {
|
|
id := row.Cells[0].Value.Block.ID
|
|
if id != "" {
|
|
bt = treenode.GetBlockTree(id)
|
|
}
|
|
}
|
|
}
|
|
if bt != nil {
|
|
// 不显示禁止文档
|
|
if !CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {
|
|
row = nil
|
|
}
|
|
}
|
|
if row != nil {
|
|
filteredRows = append(filteredRows, row)
|
|
}
|
|
}
|
|
table.Rows = filteredRows
|
|
if table.Groups != nil {
|
|
for i, viewable := range table.Groups {
|
|
table.Groups[i] = FilterViewByPublishAccess(c, publishAccess, viewable)
|
|
}
|
|
}
|
|
case av.LayoutTypeGallery:
|
|
gallery := ret.(*av.Gallery)
|
|
filteredCards := []*av.GalleryCard{}
|
|
for _, card := range gallery.Cards {
|
|
// 默认第一个属性是文档块
|
|
var bt *treenode.BlockTree
|
|
if len(card.Values) > 0 {
|
|
if card.Values[0].Value.Block != nil {
|
|
id := card.Values[0].Value.Block.ID
|
|
if id != "" {
|
|
bt = treenode.GetBlockTree(id)
|
|
}
|
|
}
|
|
}
|
|
if bt != nil {
|
|
// 替换封面
|
|
newCoverContent := FilterContentByPublishAccess(c, publishAccess, bt.BoxID, bt.Path, card.CoverContent, true)
|
|
if card.CoverContent != newCoverContent {
|
|
card.CoverContent = newCoverContent
|
|
card.CoverURL = ""
|
|
}
|
|
|
|
// 不显示禁止文档
|
|
if !CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {
|
|
card = nil
|
|
}
|
|
}
|
|
if card != nil {
|
|
filteredCards = append(filteredCards, card)
|
|
}
|
|
}
|
|
gallery.Cards = filteredCards
|
|
if gallery.Groups != nil {
|
|
for i, viewable := range gallery.Groups {
|
|
gallery.Groups[i] = FilterViewByPublishAccess(c, publishAccess, viewable)
|
|
}
|
|
}
|
|
case av.LayoutTypeKanban:
|
|
kanban := ret.(*av.Kanban)
|
|
filteredCards := []*av.KanbanCard{}
|
|
for _, card := range kanban.Cards {
|
|
// 默认第一个属性是文档块
|
|
var bt *treenode.BlockTree
|
|
if len(card.Values) > 0 {
|
|
if card.Values[0].Value.Block != nil {
|
|
id := card.Values[0].Value.Block.ID
|
|
if id != "" {
|
|
bt = treenode.GetBlockTree(id)
|
|
}
|
|
}
|
|
}
|
|
if bt != nil {
|
|
// 替换封面
|
|
newCoverContent := FilterContentByPublishAccess(c, publishAccess, bt.BoxID, bt.Path, card.CoverContent, true)
|
|
if card.CoverContent != newCoverContent {
|
|
card.CoverContent = newCoverContent
|
|
card.CoverURL = ""
|
|
}
|
|
|
|
// 不显示禁止文档
|
|
if !CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {
|
|
card = nil
|
|
}
|
|
}
|
|
if card != nil {
|
|
filteredCards = append(filteredCards, card)
|
|
}
|
|
}
|
|
kanban.Cards = filteredCards
|
|
kanban.CardCount = len(kanban.Cards)
|
|
if kanban.Groups != nil {
|
|
for i, viewable := range kanban.Groups {
|
|
kanban.Groups[i] = FilterViewByPublishAccess(c, publishAccess, viewable)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterBlockAttributeViewKeysByPublishAccess(c *gin.Context, publishAccess PublishAccess, blockAttributeViewKeys []*BlockAttributeViewKeys) (ret []*BlockAttributeViewKeys) {
|
|
publishIgnore := GetDisablePublishAccess(publishAccess)
|
|
ret = []*BlockAttributeViewKeys{}
|
|
for _, blockAttributeViewKey := range blockAttributeViewKeys {
|
|
accessable := false
|
|
bts := treenode.GetBlockTrees(blockAttributeViewKey.BlockIDs)
|
|
for _, bt := range bts {
|
|
passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
|
|
if (password == "" || CheckPublishAuthCookie(c, passwordID, password)) && CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {
|
|
accessable = true
|
|
break
|
|
}
|
|
}
|
|
if accessable {
|
|
ret = append(ret, blockAttributeViewKey)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterBlockInfoByPublishAccess(c *gin.Context, publishAccess PublishAccess, info *BlockInfo) (ret *BlockInfo) {
|
|
ret = info
|
|
if info == nil {
|
|
return
|
|
}
|
|
|
|
publishIgnore := GetDisablePublishAccess(publishAccess)
|
|
filteredAttrViews := []*AttrView{}
|
|
avIDs := []string{}
|
|
for _, attrView := range info.AttrViews {
|
|
avBlocksAccessable := false
|
|
if attrView.ID != "" {
|
|
avBlockIDs := treenode.GetMirrorAttrViewBlockIDs(attrView.ID)
|
|
avBlocks := treenode.GetBlockTrees(avBlockIDs)
|
|
for _, avBlock := range avBlocks {
|
|
passwordID, password := GetPathPasswordByPublishAccess(avBlock.BoxID, avBlock.Path, publishAccess)
|
|
if (password == "" || CheckPublishAuthCookie(c, passwordID, password)) && CheckPathAccessableByPublishIgnore(avBlock.BoxID, avBlock.Path, publishIgnore) {
|
|
avBlocksAccessable = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if avBlocksAccessable {
|
|
filteredAttrViews = append(filteredAttrViews, attrView)
|
|
avIDs = append(avIDs, attrView.ID)
|
|
}
|
|
}
|
|
ret.AttrViews = filteredAttrViews
|
|
ret.IAL[av.NodeAttrNameAvs] = strings.Join(avIDs, ",")
|
|
|
|
bt := treenode.GetBlockTree(info.RootID)
|
|
if bt != nil {
|
|
passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
|
|
if (password != "" && !CheckPublishAuthCookie(c, passwordID, password)) || !CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {
|
|
ret.IAL["name"] = ""
|
|
ret.IAL["alias"] = ""
|
|
ret.IAL["memo"] = ""
|
|
ret.IAL["bookmark"] = ""
|
|
ret.IAL["tags"] = ""
|
|
ret.RefCount = 0
|
|
ret.RefIDs = []string{}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterContentByPublishAccess(c *gin.Context, publishAccess PublishAccess, box string, docPath string, content string, onlyIcon bool) (ret string) {
|
|
ret = content
|
|
|
|
// 密码访问
|
|
passwordID, password := GetPathPasswordByPublishAccess(box, docPath, publishAccess)
|
|
if password != "" {
|
|
if !CheckPublishAuthCookie(c, passwordID, password) {
|
|
if onlyIcon {
|
|
passwordHTML := `<div class="protyle-password protyle-password--alert" data-node-id="%s">
|
|
<span class="protyle-password__logo">🔒</span>
|
|
</div>`
|
|
ret = fmt.Sprintf(passwordHTML, passwordID)
|
|
} else {
|
|
passwordHTML := `<div class="protyle-password" data-node-id="%s">
|
|
<span class="protyle-password__logo">🔒</span>
|
|
<label class="b3-form__icon protyle-password__content">
|
|
<svg class="b3-form__icon-icon"><use xlink:href="#iconKey"></use></svg>
|
|
<input type="text" class="b3-form__icon-input b3-text-field b3-form__icona-input" placeholder="%s"/>
|
|
<svg class="protyle-password__button b3-form__icona-icon"><use xlink:href="#iconForward"></use></svg>
|
|
</label>
|
|
</div>`
|
|
ret = fmt.Sprintf(passwordHTML, passwordID, Conf.Language(283))
|
|
}
|
|
}
|
|
}
|
|
|
|
// 禁止访问
|
|
ID := box
|
|
if docPath != "/" {
|
|
ID = strings.TrimSuffix(path.Base(docPath), ".sy")
|
|
}
|
|
publishIgnore := GetDisablePublishAccess(publishAccess)
|
|
if !CheckPathAccessableByPublishIgnore(box, docPath, publishIgnore) {
|
|
if onlyIcon {
|
|
forbiddenHTML := `<div class="protyle-password protyle-password--alert" data-node-id="%s">
|
|
<span class="protyle-password__logo">🚫</span>
|
|
</div>`
|
|
ret = fmt.Sprintf(forbiddenHTML, ID)
|
|
} else {
|
|
forbiddenHTML := `<div class="protyle-password protyle-password--forbidden" data-node-id="%s">
|
|
<span class="protyle-password__logo">🚫</span>
|
|
<div class="protyle-password__tip">%s</div>
|
|
</div>`
|
|
ret = fmt.Sprintf(forbiddenHTML, ID, Conf.Language(284))
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterEmbedBlocksByPublishAccess(c *gin.Context, publishAccess PublishAccess, embedBlocks []*EmbedBlock) (ret []*EmbedBlock) {
|
|
ret = []*EmbedBlock{}
|
|
for _, embedBlock := range embedBlocks {
|
|
embedBlock.Block.Content = FilterContentByPublishAccess(c, publishAccess, embedBlock.Block.Box, embedBlock.Block.Path, embedBlock.Block.Content, false)
|
|
ret = append(ret, embedBlock)
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterPathsByPublishAccess(c *gin.Context, publishAccess PublishAccess, paths []*Path) (ret []*Path) {
|
|
ret = []*Path{}
|
|
IDs := []string{}
|
|
|
|
publishIgnore := GetInvisiblePublishAccess(publishAccess)
|
|
|
|
IDtoPathIndexMap := make(map[string]int)
|
|
for i, path := range paths {
|
|
IDs = append(IDs, path.ID)
|
|
IDtoPathIndexMap[path.ID] = i
|
|
}
|
|
bts := treenode.GetBlockTrees(IDs)
|
|
for _, bt := range bts {
|
|
if bt == nil {
|
|
continue
|
|
}
|
|
pathIndex := IDtoPathIndexMap[bt.ID]
|
|
path := paths[pathIndex]
|
|
passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
|
|
if CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) && (password == "" || CheckPublishAuthCookie(c, passwordID, password)) {
|
|
ret = append(ret, path)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterBlocksByPublishAccess(c *gin.Context, publishAccess PublishAccess, blocks []*Block) (ret []*Block) {
|
|
ret = []*Block{}
|
|
|
|
publishIgnore := GetInvisiblePublishAccess(publishAccess)
|
|
|
|
for _, block := range blocks {
|
|
passwordID, password := GetPathPasswordByPublishAccess(block.Box, block.Path, publishAccess)
|
|
if CheckPathAccessableByPublishIgnore(block.Box, block.Path, publishIgnore) && (c == nil || password == "" || CheckPublishAuthCookie(c, passwordID, password)) {
|
|
ret = append(ret, block)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterBlockTreesByPublishIgnore(publishIgnore PublishAccess, bts map[string]*treenode.BlockTree) (ret map[string]*treenode.BlockTree) {
|
|
ret = map[string]*treenode.BlockTree{}
|
|
for id, bt := range bts {
|
|
if CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {
|
|
ret[id] = bt
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterRefDefsByPublishIgnore(publishIgnore PublishAccess, refDefs []*RefDefs) (retRefDefs []*RefDefs, originalRefBlockIDs map[string]string) {
|
|
retRefDefs = []*RefDefs{}
|
|
IDs := []string{}
|
|
for _, refDef := range refDefs {
|
|
IDs = append(IDs, refDef.RefID)
|
|
IDs = append(IDs, refDef.DefIDs...)
|
|
}
|
|
IDs = gulu.Str.RemoveDuplicatedElem(IDs)
|
|
bts := treenode.GetBlockTrees(IDs)
|
|
bts = FilterBlockTreesByPublishIgnore(publishIgnore, bts)
|
|
visibles := make(map[string]bool)
|
|
for _, ID := range IDs {
|
|
visibles[ID] = false
|
|
}
|
|
for _, bt := range bts {
|
|
visibles[bt.ID] = true
|
|
}
|
|
for _, refDef := range refDefs {
|
|
if !visibles[refDef.RefID] {
|
|
continue
|
|
}
|
|
newDefIDs := []string{}
|
|
for i, defID := range refDef.DefIDs {
|
|
if visibles[defID] {
|
|
newDefIDs = append(newDefIDs, refDef.DefIDs[i])
|
|
}
|
|
}
|
|
refDef.DefIDs = newDefIDs
|
|
if len(refDef.DefIDs) > 0 {
|
|
retRefDefs = append(retRefDefs, refDef)
|
|
}
|
|
}
|
|
originalRefBlockIDs = buildBacklinkListItemRefs(retRefDefs)
|
|
return
|
|
}
|
|
|
|
func FilterConfByPublishIgnore(publishIgnore PublishAccess, appConf *AppConf) (ret *AppConf) {
|
|
ret = appConf
|
|
if appConf == nil {
|
|
return
|
|
}
|
|
|
|
appConf.UILayout = FilterUILayoutByPublishIgnore(publishIgnore, appConf.UILayout)
|
|
return
|
|
}
|
|
|
|
func FilterUILayoutByPublishIgnore(publishIgnore PublishAccess, uiLayout *conf.UILayout) (ret *conf.UILayout) {
|
|
ret = uiLayout
|
|
if uiLayout == nil {
|
|
return
|
|
}
|
|
|
|
layout, ok := (*uiLayout)["layout"].(map[string]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
layout = filterLayoutItemByPublishIgnore(publishIgnore, layout)
|
|
(*ret)["layout"] = layout
|
|
return
|
|
}
|
|
|
|
func filterLayoutItemByPublishIgnore(publishIgnore PublishAccess, item map[string]any) (ret map[string]any) {
|
|
ret = item
|
|
if item == nil {
|
|
return
|
|
}
|
|
|
|
instanceItem, exists := item["instance"]
|
|
if !exists {
|
|
return
|
|
}
|
|
instance := instanceItem.(string)
|
|
if instance == "Tab" {
|
|
childrenItem, exists := item["children"]
|
|
if !exists {
|
|
return
|
|
}
|
|
children := childrenItem.(map[string]any)
|
|
if children == nil {
|
|
return
|
|
}
|
|
rootIdItem, exists := children["rootId"]
|
|
if rootIdItem == nil {
|
|
return
|
|
}
|
|
rootId := children["rootId"].(string)
|
|
bt := treenode.GetBlockTree(rootId)
|
|
if bt == nil {
|
|
return
|
|
}
|
|
if !CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {
|
|
ret = nil
|
|
}
|
|
} else {
|
|
childrenItem, exists := item["children"]
|
|
if !exists {
|
|
return
|
|
}
|
|
children := childrenItem.([]any)
|
|
if children == nil {
|
|
return
|
|
}
|
|
newChildren := []any{}
|
|
updateTabs := false
|
|
for _, childItem := range children {
|
|
child := childItem.(map[string]any)
|
|
if child == nil {
|
|
return
|
|
}
|
|
child = filterLayoutItemByPublishIgnore(publishIgnore, child)
|
|
if child != nil {
|
|
newChildren = append(newChildren, child)
|
|
} else {
|
|
updateTabs = true
|
|
}
|
|
}
|
|
if updateTabs {
|
|
hasActive := false
|
|
activeTimes := []int64{}
|
|
for _, childItem := range newChildren {
|
|
child := childItem.(map[string]any)
|
|
activeTimeStr := child["activeTime"].(string)
|
|
var activeTime int64
|
|
if len(activeTimeStr) > 0 {
|
|
activeTime, _ = strconv.ParseInt(activeTimeStr, 10, 64)
|
|
}
|
|
activeTimes = append(activeTimes, activeTime)
|
|
if active, exists := child["active"]; exists && active.(bool) {
|
|
hasActive = true
|
|
}
|
|
}
|
|
if !hasActive && len(activeTimes) > 0 {
|
|
// 如果原本激活的tab刚好被去掉了,就选择日期最新的一个tab激活
|
|
maxIndex := 0
|
|
for i, activeTime := range activeTimes {
|
|
if activeTime > activeTimes[maxIndex] {
|
|
maxIndex = i
|
|
}
|
|
}
|
|
newChildren[maxIndex].(map[string]any)["active"] = true
|
|
}
|
|
if len(newChildren) == 0 {
|
|
child := make(map[string]any)
|
|
child["instance"] = "Tab"
|
|
child["children"] = make(map[string]any)
|
|
newChildren = append(newChildren, child)
|
|
}
|
|
}
|
|
ret["children"] = newChildren
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterGraphByPublishIgnore(publishIgnore PublishAccess, nodes []*GraphNode, links []*GraphLink) (retNodes []*GraphNode, retLinks []*GraphLink) {
|
|
retNodes = []*GraphNode{}
|
|
retLinks = []*GraphLink{}
|
|
ignoreNodeIDs := []string{}
|
|
for _, node := range nodes {
|
|
if CheckPathAccessableByPublishIgnore(node.Box, node.Path, publishIgnore) {
|
|
retNodes = append(retNodes, node)
|
|
} else {
|
|
ignoreNodeIDs = append(ignoreNodeIDs, node.ID)
|
|
}
|
|
}
|
|
for _, link := range links {
|
|
ignore := false
|
|
for _, ignoreNodeID := range ignoreNodeIDs {
|
|
if ignoreNodeID == link.From || ignoreNodeID == link.To {
|
|
ignore = true
|
|
break
|
|
}
|
|
}
|
|
if !ignore {
|
|
retLinks = append(retLinks, link)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterTagsByPublishIgnore(publishIgnore PublishAccess, tags *Tags) (ret *Tags) {
|
|
spans := sql.QueryTagSpans("")
|
|
labelCounts := make(map[string]int)
|
|
for _, span := range spans {
|
|
if CheckPathAccessableByPublishIgnore(span.Box, span.Path, publishIgnore) {
|
|
label := util.UnescapeHTML(span.Content)
|
|
labelCounts[label] += 1
|
|
}
|
|
}
|
|
|
|
ret = &Tags{}
|
|
for _, tag := range *tags {
|
|
tag := reassignTagCounts(tag, labelCounts)
|
|
if tag != nil {
|
|
*ret = append(*ret, tag)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func reassignTagCounts(tag *Tag, counts map[string]int) (ret *Tag) {
|
|
var newChildren Tags
|
|
for _, child := range tag.Children {
|
|
child = reassignTagCounts(child, counts)
|
|
if child != nil {
|
|
newChildren = append(newChildren, child)
|
|
}
|
|
}
|
|
tag.Children = newChildren
|
|
tag.Count = counts[tag.Label]
|
|
if tag.Children == nil && tag.Count == 0 {
|
|
return nil
|
|
}
|
|
return tag
|
|
}
|
|
|
|
func FilterLocalStorageByPublishAccess(publishAccess PublishAccess, localStorage map[string]any) (ret map[string]any) {
|
|
ret = localStorage
|
|
publishIgnore := GetInvisiblePublishAccess(publishAccess)
|
|
// 清空搜索历史记录
|
|
searchKeysItem := ret["local-searchkeys"]
|
|
if searchKeysItem != nil {
|
|
searchKeys := searchKeysItem.(map[string]any)
|
|
if searchKeys != nil {
|
|
searchKeys["keys"] = []string{}
|
|
}
|
|
}
|
|
searchAssetItem := ret["local-searchasset"]
|
|
if searchAssetItem != nil {
|
|
searchAsset := searchAssetItem.(map[string]any)
|
|
if searchAsset != nil {
|
|
searchAsset["k"] = ""
|
|
searchAsset["keys"] = []string{}
|
|
}
|
|
}
|
|
docInfoItem := ret["local-docinfo"]
|
|
if docInfoItem != nil {
|
|
docInfo := docInfoItem.(map[string]any)
|
|
if docInfo != nil {
|
|
idItem := docInfo["id"]
|
|
if idItem != nil {
|
|
id := idItem.(string)
|
|
bt := treenode.GetBlockTree(id)
|
|
if bt != nil {
|
|
if !CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) {
|
|
docInfo["id"] = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterAssetContentByPublishAccess(c *gin.Context, publishAccess PublishAccess, assetContent []*AssetContent) (ret []*AssetContent) {
|
|
publishIgnore := GetInvisiblePublishAccess(publishAccess)
|
|
validAssets := []string{}
|
|
bts := treenode.GetBlockTreesByType("d")
|
|
for _, bt := range bts {
|
|
passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
|
|
if CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) && (password == "" || CheckPublishAuthCookie(c, passwordID, password)) {
|
|
assets, err := DocAssets(bt.ID, false)
|
|
if err == nil {
|
|
validAssets = append(validAssets, assets...)
|
|
}
|
|
}
|
|
}
|
|
|
|
ret = []*AssetContent{}
|
|
for _, asset := range assetContent {
|
|
if asset == nil {
|
|
continue
|
|
}
|
|
for _, validAsset := range validAssets {
|
|
if validAsset == asset.Path {
|
|
ret = append(ret, asset)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterRecentDocsByPublishAccess(c *gin.Context, publishAccess PublishAccess, recentDocs []*RecentDoc) (ret []*RecentDoc) {
|
|
ret = []*RecentDoc{}
|
|
publishIgnore := GetInvisiblePublishAccess(publishAccess)
|
|
for _, recentDoc := range recentDocs {
|
|
bt := treenode.GetBlockTree(recentDoc.RootID)
|
|
if bt != nil {
|
|
passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
|
|
if CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) && (passwordID == "" || CheckPublishAuthCookie(c, passwordID, password)) {
|
|
ret = append(ret, recentDoc)
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func FilterCriteriaByPublishAccess(c *gin.Context, publishAccess PublishAccess, criteria []*Criterion) (ret []*Criterion) {
|
|
ret = []*Criterion{}
|
|
publishIgnore := GetInvisiblePublishAccess(publishAccess)
|
|
// IDPath 元素可能是笔记本 ID、文档 ID,或 "笔记本ID/文档ID[.sy]" 路径串,这里统一解析出文档 ID
|
|
blockIDs := map[string]struct{}{}
|
|
for _, criterion := range criteria {
|
|
for _, p := range criterion.IDPath {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
// 路径形式取末段并去掉 .sy 后缀
|
|
id := strings.TrimSuffix(path.Base(p), ".sy")
|
|
if id != "" && id != "." && id != "/" {
|
|
blockIDs[id] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
blockIDsSlice := make([]string, 0, len(blockIDs))
|
|
for id := range blockIDs {
|
|
blockIDsSlice = append(blockIDsSlice, id)
|
|
}
|
|
blockTrees := treenode.GetBlockTrees(blockIDsSlice)
|
|
for _, criterion := range criteria {
|
|
accessible := false
|
|
for _, p := range criterion.IDPath {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
id := strings.TrimSuffix(path.Base(p), ".sy")
|
|
if id == "" || id == "." || id == "/" {
|
|
continue
|
|
}
|
|
bt := blockTrees[id]
|
|
if bt == nil {
|
|
// 关联的文档不存在,视为不可访问
|
|
accessible = false
|
|
break
|
|
}
|
|
passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
|
|
if !CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) || (passwordID != "" && !CheckPublishAuthCookie(c, passwordID, password)) {
|
|
accessible = false
|
|
break
|
|
}
|
|
accessible = true
|
|
}
|
|
if !accessible {
|
|
// 若 IDPath 全部不可访问(或引用了不可见文档),整条丢弃,避免泄露 HPath
|
|
continue
|
|
}
|
|
|
|
// 复制一份后再清空搜索/替换关键字,避免污染缓存
|
|
cloned := *criterion
|
|
cloned.K = ""
|
|
cloned.R = ""
|
|
ret = append(ret, &cloned)
|
|
}
|
|
return
|
|
}
|