Skip to content

Commit bd4bb1a

Browse files
committed
🐛 Show password prompt for protected published documents #18569
1 parent e40c9fe commit bd4bb1a

6 files changed

Lines changed: 256 additions & 33 deletions

File tree

app/src/protyle/util/onGet.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,23 @@ export const onGet = (options: {
138138
return;
139139
}
140140

141+
if (options.data.data.publishAccessRequired) {
142+
setHTML({
143+
content: options.data.data.content,
144+
eof: options.data.data.eof,
145+
expand: options.data.data.isBacklinkExpand,
146+
action: options.action,
147+
scrollAttr: options.scrollAttr,
148+
updateReadonly: options.updateReadonly,
149+
isSyncing: options.data.data.isSyncing,
150+
refreshHeadingNumbers,
151+
afterCB: options.afterCB,
152+
scrollPosition: options.scrollPosition
153+
}, options.protyle);
154+
removeLoading(options.protyle);
155+
return;
156+
}
157+
141158
const docInfoParam: IObject = {
142159
id: options.protyle.block.rootID
143160
};

kernel/api/block.go

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,20 @@ func getBlockInfo(c *gin.Context) {
11381138
if !holdBlockRequest(c, ret, boxID) {
11391139
return
11401140
}
1141-
if !checkBlockPublishAccessInBox(c, id, boxID, ret) {
1141+
blockTree, publishAccessRequired, publishMetadataVisible, publishAccessible := getBlockInfoPublishAccess(c, id, boxID)
1142+
if !publishAccessible {
1143+
ret.Code = -1
1144+
ret.Msg = fmt.Sprintf(model.Conf.Language(15), id)
1145+
return
1146+
}
1147+
if publishAccessRequired && !publishMetadataVisible {
1148+
ret.Data = map[string]any{
1149+
"rootID": blockTree.RootID,
1150+
"rootTitle": "",
1151+
"rootTitleEmpty": true,
1152+
"rootIcon": "",
1153+
"publishAccessRequired": true,
1154+
}
11421155
return
11431156
}
11441157

@@ -1183,6 +1196,26 @@ func getBlockInfo(c *gin.Context) {
11831196
return
11841197
}
11851198

1199+
root, err := model.GetBlock(block.RootID, tree)
1200+
if errors.Is(err, model.ErrIndexing) {
1201+
ret.Code = 3
1202+
ret.Data = model.Conf.Language(56)
1203+
return
1204+
}
1205+
rootTitle := root.IAL["title"]
1206+
rootTitle = html.UnescapeString(rootTitle)
1207+
icon := html.UnescapeString(root.IAL["icon"])
1208+
if publishAccessRequired {
1209+
ret.Data = map[string]any{
1210+
"rootID": block.RootID,
1211+
"rootTitle": rootTitle,
1212+
"rootTitleEmpty": root.IAL[model.NodeAttrTitleEmpty] == "true",
1213+
"rootIcon": icon,
1214+
"publishAccessRequired": true,
1215+
}
1216+
return
1217+
}
1218+
11861219
var rootChildID string
11871220
b := block
11881221
for range 128 {
@@ -1197,15 +1230,6 @@ func getBlockInfo(c *gin.Context) {
11971230
}
11981231
}
11991232

1200-
root, err := model.GetBlock(block.RootID, tree)
1201-
if errors.Is(err, model.ErrIndexing) {
1202-
ret.Code = 3
1203-
ret.Data = model.Conf.Language(56)
1204-
return
1205-
}
1206-
rootTitle := root.IAL["title"]
1207-
rootTitle = html.UnescapeString(rootTitle)
1208-
icon := html.UnescapeString(root.IAL["icon"])
12091233
ret.Data = map[string]any{
12101234
"box": block.Box,
12111235
"path": block.Path,
@@ -1217,6 +1241,25 @@ func getBlockInfo(c *gin.Context) {
12171241
}
12181242
}
12191243

1244+
func getBlockInfoPublishAccess(c *gin.Context, id, boxID string) (blockTree *treenode.BlockTree, passwordRequired, metadataVisible, accessible bool) {
1245+
if !model.IsReadOnlyRoleContext(c) {
1246+
return nil, false, true, true
1247+
}
1248+
1249+
blockTree = treenode.GetBlockTreeInBox(id, boxID)
1250+
publishAccess := model.GetPublishAccess()
1251+
switch model.GetBlockTreePublishAccessStatus(c, publishAccess, blockTree) {
1252+
case model.PublishAccessAllowed:
1253+
return blockTree, false, true, true
1254+
case model.PublishAccessPasswordRequired:
1255+
metadataVisible = model.CheckBlockTreeDiscoverableByPublishAccess(publishAccess, blockTree)
1256+
if metadataVisible || blockTree.ID == blockTree.RootID {
1257+
return blockTree, true, metadataVisible, true
1258+
}
1259+
}
1260+
return blockTree, false, false, false
1261+
}
1262+
12201263
func checkBlockPublishAccess(c *gin.Context, id string, ret *gulu.Result) bool {
12211264
return checkBlockPublishAccessInBox(c, id, "", ret)
12221265
}

kernel/api/block_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,91 @@ func TestFilterBlockAndRefIDsByPublishAccess(t *testing.T) {
139139
}
140140
}
141141

142+
func TestGetBlockInfoPublishAccess(t *testing.T) {
143+
const (
144+
boxID = "20260806000020-box0020"
145+
protectedID = "20260806000021-protect"
146+
privateID = "20260806000022-private"
147+
privateChildID = "20260806000023-child20"
148+
disabledID = "20260806000024-disable"
149+
protectedPassword = "protected-password"
150+
privatePassword = "private-password"
151+
)
152+
153+
previousBlockTreeDBPath := util.BlockTreeDBPath
154+
previousDataDir := util.DataDir
155+
util.DataDir = t.TempDir()
156+
util.BlockTreeDBPath = filepath.Join(util.DataDir, "blocktree.db")
157+
treenode.InitBlockTree(true)
158+
previousPublishAccess := model.GetPublishAccess()
159+
if err := model.SetPublishAccess(model.PublishAccess{
160+
{ID: protectedID, Visible: true, Password: protectedPassword},
161+
{ID: privateID, Visible: false, Password: privatePassword},
162+
{ID: disabledID, Visible: true, Disable: true},
163+
}); err != nil {
164+
t.Fatalf("set publish access failed: %v", err)
165+
}
166+
t.Cleanup(func() {
167+
_ = model.SetPublishAccess(previousPublishAccess)
168+
treenode.CloseDatabase()
169+
util.BlockTreeDBPath = previousBlockTreeDBPath
170+
util.DataDir = previousDataDir
171+
})
172+
173+
for _, id := range []string{protectedID, disabledID} {
174+
treenode.IndexBlockTree(&parse.Tree{
175+
ID: id,
176+
Box: boxID,
177+
Path: "/" + id + ".sy",
178+
Root: &ast.Node{ID: id, Type: ast.NodeDocument},
179+
})
180+
}
181+
privateRoot := &ast.Node{ID: privateID, Type: ast.NodeDocument}
182+
privateRoot.AppendChild(&ast.Node{ID: privateChildID, Type: ast.NodeParagraph})
183+
treenode.IndexBlockTree(&parse.Tree{
184+
ID: privateID,
185+
Box: boxID,
186+
Path: "/" + privateID + ".sy",
187+
Root: privateRoot,
188+
})
189+
190+
c, _ := gin.CreateTestContext(httptest.NewRecorder())
191+
c.Request = httptest.NewRequest(http.MethodPost, "/", nil)
192+
c.Set(model.RoleContextKey, model.RoleReader)
193+
194+
_, passwordRequired, metadataVisible, accessible := getBlockInfoPublishAccess(c, protectedID, "")
195+
if !passwordRequired || !metadataVisible || !accessible {
196+
t.Fatalf("protected document gate = [%v, %v, %v], want password required with visible metadata",
197+
passwordRequired, metadataVisible, accessible)
198+
}
199+
200+
_, passwordRequired, metadataVisible, accessible = getBlockInfoPublishAccess(c, privateID, "")
201+
if !passwordRequired || metadataVisible || !accessible {
202+
t.Fatalf("private document gate = [%v, %v, %v], want password required without visible metadata",
203+
passwordRequired, metadataVisible, accessible)
204+
}
205+
206+
_, _, _, accessible = getBlockInfoPublishAccess(c, privateChildID, "")
207+
if accessible {
208+
t.Fatal("private child block should not open the password gate before authorization")
209+
}
210+
211+
_, _, _, accessible = getBlockInfoPublishAccess(c, disabledID, "")
212+
if accessible {
213+
t.Fatal("publish-disabled document should not open the password gate")
214+
}
215+
216+
c.Request.AddCookie(&http.Cookie{
217+
Name: "publish-auth-" + privateID,
218+
Value: util.SHA256Hash([]byte(privateID + privatePassword)),
219+
})
220+
_, passwordRequired, metadataVisible, accessible = getBlockInfoPublishAccess(c, privateChildID, "")
221+
if passwordRequired || !metadataVisible || !accessible {
222+
t.Fatalf("authorized private child gate = [%v, %v, %v], want normal access",
223+
passwordRequired, metadataVisible, accessible)
224+
}
225+
}
226+
142227
func TestGetDocBlocksOrdersArguments(t *testing.T) {
143228
tests := []struct {
144229
name string

kernel/api/filetree.go

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,9 +1452,11 @@ func getDoc(c *gin.Context) {
14521452
// 判断是否正在同步中 https://github.com/siyuan-note/siyuan/issues/6290
14531453
isSyncing := model.IsSyncingFile(rootID)
14541454

1455+
publishAccessRequired := false
14551456
if model.IsReadOnlyRoleContext(c) {
14561457
publishAccess := model.GetPublishAccess()
1457-
newContent := model.FilterContentByPublishAccess(c, publishAccess, boxID, docPath, content, false)
1458+
newContent, publishAccessStatus := model.FilterContentByPublishAccessWithStatus(c, publishAccess, boxID, docPath, content, false)
1459+
publishAccessRequired = publishAccessStatus == model.PublishAccessPasswordRequired
14581460
if newContent != content {
14591461
content = newContent
14601462
headingNumbers = nil
@@ -1463,23 +1465,24 @@ func getDoc(c *gin.Context) {
14631465
}
14641466

14651467
ret.Data = map[string]any{
1466-
"id": id,
1467-
"mode": mode,
1468-
"parentID": parentID,
1469-
"parent2ID": parent2ID,
1470-
"rootID": rootID,
1471-
"type": typ,
1472-
"content": content,
1473-
"blockCount": blockCount,
1474-
"eof": eof,
1475-
"scroll": scroll,
1476-
"box": boxID,
1477-
"path": docPath,
1478-
"isSyncing": isSyncing,
1479-
"isBacklinkExpand": isBacklinkExpand,
1480-
"keywords": keywords,
1481-
"headingNumbers": headingNumbers,
1482-
"reqId": arg["reqId"],
1468+
"id": id,
1469+
"mode": mode,
1470+
"parentID": parentID,
1471+
"parent2ID": parent2ID,
1472+
"rootID": rootID,
1473+
"type": typ,
1474+
"content": content,
1475+
"blockCount": blockCount,
1476+
"eof": eof,
1477+
"scroll": scroll,
1478+
"box": boxID,
1479+
"path": docPath,
1480+
"isSyncing": isSyncing,
1481+
"isBacklinkExpand": isBacklinkExpand,
1482+
"keywords": keywords,
1483+
"headingNumbers": headingNumbers,
1484+
"publishAccessRequired": publishAccessRequired,
1485+
"reqId": arg["reqId"],
14831486
}
14841487
}
14851488

kernel/model/publish_access.go

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ type PublishAccessItem struct {
5050

5151
type PublishAccess []*PublishAccessItem
5252

53+
type PublishAccessStatus int
54+
55+
const (
56+
PublishAccessAllowed PublishAccessStatus = iota
57+
PublishAccessPasswordRequired
58+
PublishAccessDenied
59+
)
60+
5361
var (
5462
publishAccessLastModified int64
5563
publishAccess PublishAccess
@@ -428,14 +436,25 @@ func CheckBlockTreeDiscoverableByPublishAccess(publishAccess PublishAccess, bt *
428436
CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishDisable)
429437
}
430438

431-
func checkBlockTreeAccessableByPublishAccess(c *gin.Context, publishAccess PublishAccess, bt *treenode.BlockTree) bool {
439+
func GetBlockTreePublishAccessStatus(c *gin.Context, publishAccess PublishAccess, bt *treenode.BlockTree) PublishAccessStatus {
432440
if bt == nil || IsEncryptedBoxDeniedByPublishAccess(bt.BoxID) {
433-
return false
441+
return PublishAccessDenied
442+
}
443+
444+
publishDisable := filterDisablePublishAccess(publishAccess)
445+
if !CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishDisable) {
446+
return PublishAccessDenied
434447
}
435448

436-
publishIgnore := filterDisablePublishAccess(publishAccess)
437449
passwordID, password := GetPathPasswordByPublishAccess(bt.BoxID, bt.Path, publishAccess)
438-
return CheckPathAccessableByPublishIgnore(bt.BoxID, bt.Path, publishIgnore) && (password == "" || CheckPublishAuthCookie(c, passwordID, password))
450+
if password != "" && !CheckPublishAuthCookie(c, passwordID, password) {
451+
return PublishAccessPasswordRequired
452+
}
453+
return PublishAccessAllowed
454+
}
455+
456+
func checkBlockTreeAccessableByPublishAccess(c *gin.Context, publishAccess PublishAccess, bt *treenode.BlockTree) bool {
457+
return GetBlockTreePublishAccessStatus(c, publishAccess, bt) == PublishAccessAllowed
439458
}
440459

441460
func SetPublishAuthCookie(c *gin.Context, ID string, password string) {
@@ -1185,13 +1204,20 @@ func FilterBlockInfoByPublishAccess(c *gin.Context, publishAccess PublishAccess,
11851204
return
11861205
}
11871206

1188-
func FilterContentByPublishAccess(c *gin.Context, publishAccess PublishAccess, box string, docPath string, content string, onlyIcon bool) (ret string) {
1207+
func FilterContentByPublishAccess(c *gin.Context, publishAccess PublishAccess, box string, docPath string, content string, onlyIcon bool) string {
1208+
ret, _ := FilterContentByPublishAccessWithStatus(c, publishAccess, box, docPath, content, onlyIcon)
1209+
return ret
1210+
}
1211+
1212+
func FilterContentByPublishAccessWithStatus(c *gin.Context, publishAccess PublishAccess, box string, docPath string, content string, onlyIcon bool) (ret string, status PublishAccessStatus) {
11891213
ret = content
1214+
status = PublishAccessAllowed
11901215

11911216
// 密码访问
11921217
passwordID, password := GetPathPasswordByPublishAccess(box, docPath, publishAccess)
11931218
if password != "" {
11941219
if !CheckPublishAuthCookie(c, passwordID, password) {
1220+
status = PublishAccessPasswordRequired
11951221
if onlyIcon {
11961222
passwordHTML := `<div class="protyle-password protyle-password--alert" data-node-id="%s">
11971223
<span class="protyle-password__logo">🔒</span>
@@ -1218,6 +1244,7 @@ func FilterContentByPublishAccess(c *gin.Context, publishAccess PublishAccess, b
12181244
}
12191245
publishIgnore := GetDisablePublishAccess(publishAccess)
12201246
if !CheckPathAccessableByPublishIgnore(box, docPath, publishIgnore) {
1247+
status = PublishAccessDenied
12211248
if onlyIcon {
12221249
forbiddenHTML := `<div class="protyle-password protyle-password--alert" data-node-id="%s">
12231250
<span class="protyle-password__logo">🚫</span>

0 commit comments

Comments
 (0)