A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1// SiYuan - Refactor your thinking
2// Copyright (c) 2020-present, b3log.org
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17package api
18
19import (
20 "fmt"
21 "net/http"
22 "strings"
23
24 "github.com/88250/gulu"
25 "github.com/gin-gonic/gin"
26 "github.com/siyuan-note/siyuan/kernel/bazaar"
27 "github.com/siyuan-note/siyuan/kernel/conf"
28 "github.com/siyuan-note/siyuan/kernel/model"
29 "github.com/siyuan-note/siyuan/kernel/server/proxy"
30 "github.com/siyuan-note/siyuan/kernel/sql"
31 "github.com/siyuan-note/siyuan/kernel/util"
32)
33
34func setEditorReadOnly(c *gin.Context) {
35 ret := gulu.Ret.NewResult()
36 defer c.JSON(http.StatusOK, ret)
37
38 arg, ok := util.JsonArg(c, ret)
39 if !ok {
40 return
41 }
42
43 readOnly := arg["readonly"].(bool)
44
45 oldReadOnly := model.Conf.Editor.ReadOnly
46 model.Conf.Editor.ReadOnly = readOnly
47 model.Conf.Save()
48
49 if oldReadOnly != model.Conf.Editor.ReadOnly {
50 util.BroadcastByType("protyle", "readonly", 0, "", model.Conf.Editor.ReadOnly)
51 util.BroadcastByType("main", "readonly", 0, "", model.Conf.Editor.ReadOnly)
52 }
53}
54
55func setConfSnippet(c *gin.Context) {
56 ret := gulu.Ret.NewResult()
57 defer c.JSON(http.StatusOK, ret)
58
59 arg, ok := util.JsonArg(c, ret)
60 if !ok {
61 return
62 }
63
64 param, err := gulu.JSON.MarshalJSON(arg)
65 if err != nil {
66 ret.Code = -1
67 ret.Msg = err.Error()
68 return
69 }
70
71 snippet := &conf.Snpt{}
72 if err = gulu.JSON.UnmarshalJSON(param, snippet); err != nil {
73 ret.Code = -1
74 ret.Msg = err.Error()
75 return
76 }
77
78 model.Conf.Snippet = snippet
79 model.Conf.Save()
80
81 ret.Data = snippet
82}
83
84func addVirtualBlockRefExclude(c *gin.Context) {
85 // Add internal kernel API `/api/setting/addVirtualBlockRefExclude` https://github.com/siyuan-note/siyuan/issues/9909
86
87 ret := gulu.Ret.NewResult()
88 defer c.JSON(http.StatusOK, ret)
89
90 arg, ok := util.JsonArg(c, ret)
91 if !ok {
92 return
93 }
94
95 keywordsArg := arg["keywords"]
96 var keywords []string
97 for _, k := range keywordsArg.([]interface{}) {
98 keywords = append(keywords, k.(string))
99 }
100
101 model.AddVirtualBlockRefExclude(keywords)
102 util.BroadcastByType("main", "setConf", 0, "", model.Conf)
103}
104
105func addVirtualBlockRefInclude(c *gin.Context) {
106 // Add internal kernel API `/api/setting/addVirtualBlockRefInclude` https://github.com/siyuan-note/siyuan/issues/9909
107
108 ret := gulu.Ret.NewResult()
109 defer c.JSON(http.StatusOK, ret)
110
111 arg, ok := util.JsonArg(c, ret)
112 if !ok {
113 return
114 }
115
116 keywordsArg := arg["keywords"]
117 var keywords []string
118 for _, k := range keywordsArg.([]interface{}) {
119 keywords = append(keywords, k.(string))
120 }
121
122 model.AddVirtualBlockRefInclude(keywords)
123 util.BroadcastByType("main", "setConf", 0, "", model.Conf)
124}
125
126func refreshVirtualBlockRef(c *gin.Context) {
127 // Add internal kernel API `/api/setting/refreshVirtualBlockRef` https://github.com/siyuan-note/siyuan/issues/9829
128
129 ret := gulu.Ret.NewResult()
130 defer c.JSON(http.StatusOK, ret)
131
132 model.ResetVirtualBlockRefCache()
133 util.BroadcastByType("main", "setConf", 0, "", model.Conf)
134}
135
136func setBazaar(c *gin.Context) {
137 ret := gulu.Ret.NewResult()
138 defer c.JSON(http.StatusOK, ret)
139
140 arg, ok := util.JsonArg(c, ret)
141 if !ok {
142 return
143 }
144
145 param, err := gulu.JSON.MarshalJSON(arg)
146 if err != nil {
147 ret.Code = -1
148 ret.Msg = err.Error()
149 return
150 }
151
152 bazaar := &conf.Bazaar{}
153 if err = gulu.JSON.UnmarshalJSON(param, bazaar); err != nil {
154 ret.Code = -1
155 ret.Msg = err.Error()
156 return
157 }
158
159 model.Conf.Bazaar = bazaar
160 model.Conf.Save()
161
162 ret.Data = bazaar
163}
164
165func setAI(c *gin.Context) {
166 ret := gulu.Ret.NewResult()
167 defer c.JSON(http.StatusOK, ret)
168
169 arg, ok := util.JsonArg(c, ret)
170 if !ok {
171 return
172 }
173
174 param, err := gulu.JSON.MarshalJSON(arg)
175 if err != nil {
176 ret.Code = -1
177 ret.Msg = err.Error()
178 return
179 }
180
181 ai := &conf.AI{}
182 if err = gulu.JSON.UnmarshalJSON(param, ai); err != nil {
183 ret.Code = -1
184 ret.Msg = err.Error()
185 return
186 }
187
188 if 5 > ai.OpenAI.APITimeout {
189 ai.OpenAI.APITimeout = 5
190 }
191 if 600 < ai.OpenAI.APITimeout {
192 ai.OpenAI.APITimeout = 600
193 }
194
195 if 0 > ai.OpenAI.APIMaxTokens {
196 ai.OpenAI.APIMaxTokens = 0
197 }
198
199 if 0 >= ai.OpenAI.APITemperature || 2 < ai.OpenAI.APITemperature {
200 ai.OpenAI.APITemperature = 1.0
201 }
202
203 if 1 > ai.OpenAI.APIMaxContexts || 64 < ai.OpenAI.APIMaxContexts {
204 ai.OpenAI.APIMaxContexts = 7
205 }
206
207 model.Conf.AI = ai
208 model.Conf.Save()
209
210 ret.Data = ai
211}
212
213func setFlashcard(c *gin.Context) {
214 ret := gulu.Ret.NewResult()
215 defer c.JSON(http.StatusOK, ret)
216
217 arg, ok := util.JsonArg(c, ret)
218 if !ok {
219 return
220 }
221
222 param, err := gulu.JSON.MarshalJSON(arg)
223 if err != nil {
224 ret.Code = -1
225 ret.Msg = err.Error()
226 return
227 }
228
229 flashcard := &conf.Flashcard{}
230 if err = gulu.JSON.UnmarshalJSON(param, flashcard); err != nil {
231 ret.Code = -1
232 ret.Msg = err.Error()
233 return
234 }
235
236 if 0 > flashcard.NewCardLimit {
237 flashcard.NewCardLimit = 20
238 }
239
240 if 0 > flashcard.ReviewCardLimit {
241 flashcard.ReviewCardLimit = 200
242 }
243
244 model.Conf.Flashcard = flashcard
245 model.Conf.Save()
246
247 ret.Data = flashcard
248}
249
250func setAccount(c *gin.Context) {
251 ret := gulu.Ret.NewResult()
252 defer c.JSON(http.StatusOK, ret)
253
254 arg, ok := util.JsonArg(c, ret)
255 if !ok {
256 return
257 }
258
259 param, err := gulu.JSON.MarshalJSON(arg)
260 if err != nil {
261 ret.Code = -1
262 ret.Msg = err.Error()
263 return
264 }
265
266 account := &conf.Account{}
267 if err = gulu.JSON.UnmarshalJSON(param, account); err != nil {
268 ret.Code = -1
269 ret.Msg = err.Error()
270 return
271 }
272
273 model.Conf.Account = account
274 model.Conf.Save()
275
276 ret.Data = model.Conf.Account
277}
278
279func setEditor(c *gin.Context) {
280 ret := gulu.Ret.NewResult()
281 defer c.JSON(http.StatusOK, ret)
282
283 arg, ok := util.JsonArg(c, ret)
284 if !ok {
285 return
286 }
287
288 param, err := gulu.JSON.MarshalJSON(arg)
289 if err != nil {
290 ret.Code = -1
291 ret.Msg = err.Error()
292 return
293 }
294
295 oldGenerateHistoryInterval := model.Conf.Editor.GenerateHistoryInterval
296
297 editor := conf.NewEditor()
298 if err = gulu.JSON.UnmarshalJSON(param, editor); err != nil {
299 ret.Code = -1
300 ret.Msg = err.Error()
301 return
302 }
303
304 if "" == editor.PlantUMLServePath {
305 editor.PlantUMLServePath = "https://www.plantuml.com/plantuml/svg/~1"
306 }
307
308 if "" == editor.KaTexMacros {
309 editor.KaTexMacros = "{}"
310 }
311
312 if 1 > editor.HistoryRetentionDays {
313 editor.HistoryRetentionDays = 30
314 }
315 if 3650 < editor.HistoryRetentionDays {
316 editor.HistoryRetentionDays = 3650
317 }
318
319 oldVirtualBlockRef := model.Conf.Editor.VirtualBlockRef
320 oldVirtualBlockRefInclude := model.Conf.Editor.VirtualBlockRefInclude
321 oldVirtualBlockRefExclude := model.Conf.Editor.VirtualBlockRefExclude
322 oldReadOnly := model.Conf.Editor.ReadOnly
323
324 model.Conf.Editor = editor
325 model.Conf.Save()
326
327 if oldGenerateHistoryInterval != model.Conf.Editor.GenerateHistoryInterval {
328 model.ChangeHistoryTick(editor.GenerateHistoryInterval)
329 }
330
331 if oldVirtualBlockRef != model.Conf.Editor.VirtualBlockRef ||
332 oldVirtualBlockRefInclude != model.Conf.Editor.VirtualBlockRefInclude ||
333 oldVirtualBlockRefExclude != model.Conf.Editor.VirtualBlockRefExclude {
334 model.ResetVirtualBlockRefCache()
335 }
336
337 if oldReadOnly != model.Conf.Editor.ReadOnly {
338 util.BroadcastByType("protyle", "readonly", 0, "", model.Conf.Editor.ReadOnly)
339 util.BroadcastByType("main", "readonly", 0, "", model.Conf.Editor.ReadOnly)
340 }
341
342 util.MarkdownSettings = model.Conf.Editor.Markdown
343
344 ret.Data = model.Conf.Editor
345}
346
347func setExport(c *gin.Context) {
348 ret := gulu.Ret.NewResult()
349 defer c.JSON(http.StatusOK, ret)
350
351 arg, ok := util.JsonArg(c, ret)
352 if !ok {
353 return
354 }
355
356 param, err := gulu.JSON.MarshalJSON(arg)
357 if err != nil {
358 ret.Code = -1
359 ret.Msg = err.Error()
360 return
361 }
362
363 export := &conf.Export{}
364 if err = gulu.JSON.UnmarshalJSON(param, export); err != nil {
365 ret.Code = -1
366 ret.Msg = err.Error()
367 ret.Data = map[string]interface{}{"closeTimeout": 5000}
368 return
369 }
370
371 if "" != export.PandocBin {
372 if !util.IsValidPandocBin(export.PandocBin) {
373 util.PushErrMsg(fmt.Sprintf(model.Conf.Language(117), export.PandocBin), 5000)
374 export.PandocBin = util.PandocBinPath
375 } else {
376 util.PandocBinPath = export.PandocBin
377 }
378 }
379
380 model.Conf.Export = export
381 model.Conf.Save()
382
383 ret.Data = model.Conf.Export
384}
385
386func setFiletree(c *gin.Context) {
387 ret := gulu.Ret.NewResult()
388 defer c.JSON(http.StatusOK, ret)
389
390 arg, ok := util.JsonArg(c, ret)
391 if !ok {
392 return
393 }
394
395 param, err := gulu.JSON.MarshalJSON(arg)
396 if err != nil {
397 ret.Code = -1
398 ret.Msg = err.Error()
399 return
400 }
401
402 fileTree := conf.NewFileTree()
403 if err = gulu.JSON.UnmarshalJSON(param, fileTree); err != nil {
404 ret.Code = -1
405 ret.Msg = err.Error()
406 return
407 }
408
409 fileTree.RefCreateSavePath = util.TrimSpaceInPath(fileTree.RefCreateSavePath)
410 if "" != fileTree.RefCreateSavePath {
411 if !strings.HasSuffix(fileTree.RefCreateSavePath, "/") {
412 fileTree.RefCreateSavePath += "/"
413 }
414 }
415
416 fileTree.DocCreateSavePath = util.TrimSpaceInPath(fileTree.DocCreateSavePath)
417
418 if 1 > fileTree.MaxOpenTabCount {
419 fileTree.MaxOpenTabCount = 8
420 }
421 if 32 < fileTree.MaxOpenTabCount {
422 fileTree.MaxOpenTabCount = 32
423 }
424 model.Conf.FileTree = fileTree
425 model.Conf.Save()
426
427 util.UseSingleLineSave = model.Conf.FileTree.UseSingleLineSave
428 util.LargeFileWarningSize = model.Conf.FileTree.LargeFileWarningSize
429
430 ret.Data = model.Conf.FileTree
431}
432
433func setSearch(c *gin.Context) {
434 ret := gulu.Ret.NewResult()
435 defer c.JSON(http.StatusOK, ret)
436
437 arg, ok := util.JsonArg(c, ret)
438 if !ok {
439 return
440 }
441
442 param, err := gulu.JSON.MarshalJSON(arg)
443 if err != nil {
444 ret.Code = -1
445 ret.Msg = err.Error()
446 return
447 }
448
449 s := &conf.Search{}
450 if err = gulu.JSON.UnmarshalJSON(param, s); err != nil {
451 ret.Code = -1
452 ret.Msg = err.Error()
453 return
454 }
455
456 if 32 > s.Limit {
457 s.Limit = 32
458 }
459
460 oldCaseSensitive := model.Conf.Search.CaseSensitive
461 oldIndexAssetPath := model.Conf.Search.IndexAssetPath
462
463 oldVirtualRefName := model.Conf.Search.VirtualRefName
464 oldVirtualRefAlias := model.Conf.Search.VirtualRefAlias
465 oldVirtualRefAnchor := model.Conf.Search.VirtualRefAnchor
466 oldVirtualRefDoc := model.Conf.Search.VirtualRefDoc
467
468 model.Conf.Search = s
469 model.Conf.Save()
470
471 sql.SetCaseSensitive(s.CaseSensitive)
472 sql.SetIndexAssetPath(s.IndexAssetPath)
473
474 if needFullReindex := s.CaseSensitive != oldCaseSensitive || s.IndexAssetPath != oldIndexAssetPath; needFullReindex {
475 model.FullReindex()
476 }
477
478 if oldVirtualRefName != s.VirtualRefName ||
479 oldVirtualRefAlias != s.VirtualRefAlias ||
480 oldVirtualRefAnchor != s.VirtualRefAnchor ||
481 oldVirtualRefDoc != s.VirtualRefDoc {
482 model.ResetVirtualBlockRefCache()
483 }
484 ret.Data = s
485}
486
487func setKeymap(c *gin.Context) {
488 ret := gulu.Ret.NewResult()
489 defer c.JSON(http.StatusOK, ret)
490
491 arg, ok := util.JsonArg(c, ret)
492 if !ok {
493 return
494 }
495
496 param, err := gulu.JSON.MarshalJSON(arg["data"])
497 if err != nil {
498 ret.Code = -1
499 ret.Msg = err.Error()
500 return
501 }
502
503 keymap := &conf.Keymap{}
504 if err = gulu.JSON.UnmarshalJSON(param, keymap); err != nil {
505 ret.Code = -1
506 ret.Msg = err.Error()
507 return
508 }
509
510 model.Conf.Keymap = keymap
511 model.Conf.Save()
512}
513
514func setAppearance(c *gin.Context) {
515 ret := gulu.Ret.NewResult()
516 defer c.JSON(http.StatusOK, ret)
517
518 arg, ok := util.JsonArg(c, ret)
519 if !ok {
520 return
521 }
522
523 param, err := gulu.JSON.MarshalJSON(arg)
524 if err != nil {
525 ret.Code = -1
526 ret.Msg = err.Error()
527 return
528 }
529
530 appearance := &conf.Appearance{}
531 if err = gulu.JSON.UnmarshalJSON(param, appearance); err != nil {
532 ret.Code = -1
533 ret.Msg = err.Error()
534 return
535 }
536
537 model.Conf.Appearance = appearance
538 model.Conf.Lang = appearance.Lang
539 oldLang := util.Lang
540 util.Lang = model.Conf.Lang
541 model.Conf.Save()
542 model.InitAppearance()
543
544 if oldLang != util.Lang {
545 // The marketplace language does not change after switching the appearance language https://github.com/siyuan-note/siyuan/issues/12892
546 bazaar.CleanBazaarPackageCache()
547 }
548
549 ret.Data = model.Conf.Appearance
550}
551
552func setPublish(c *gin.Context) {
553 ret := gulu.Ret.NewResult()
554 defer c.JSON(http.StatusOK, ret)
555
556 arg, ok := util.JsonArg(c, ret)
557 if !ok {
558 return
559 }
560
561 param, err := gulu.JSON.MarshalJSON(arg)
562 if err != nil {
563 ret.Code = -1
564 ret.Msg = err.Error()
565 return
566 }
567
568 publish := &conf.Publish{}
569 if err = gulu.JSON.UnmarshalJSON(param, publish); err != nil {
570 ret.Code = -1
571 ret.Msg = err.Error()
572 return
573 }
574
575 model.Conf.Publish = publish
576 model.Conf.Save()
577
578 if port, err := proxy.InitPublishService(); err != nil {
579 ret.Code = -1
580 ret.Msg = err.Error()
581 } else {
582 ret.Data = map[string]any{
583 "port": port,
584 "publish": model.Conf.Publish,
585 }
586 }
587}
588
589func getPublish(c *gin.Context) {
590 ret := gulu.Ret.NewResult()
591 defer c.JSON(http.StatusOK, ret)
592
593 if port, err := proxy.InitPublishService(); err != nil {
594 ret.Code = -1
595 ret.Msg = err.Error()
596 } else {
597 ret.Data = map[string]any{
598 "port": port,
599 "publish": model.Conf.Publish,
600 }
601 }
602}
603
604func getCloudUser(c *gin.Context) {
605 ret := gulu.Ret.NewResult()
606 defer c.JSON(http.StatusOK, ret)
607
608 if !model.IsAdminRoleContext(c) {
609 return
610 }
611
612 arg, ok := util.JsonArg(c, ret)
613 if !ok {
614 return
615 }
616
617 t := arg["token"]
618 var token string
619 if nil != t {
620 token = t.(string)
621 }
622 model.RefreshUser(token)
623 ret.Data = model.Conf.GetUser()
624}
625
626func logoutCloudUser(c *gin.Context) {
627 ret := gulu.Ret.NewResult()
628 defer c.JSON(http.StatusOK, ret)
629
630 model.LogoutUser()
631}
632
633func login2faCloudUser(c *gin.Context) {
634 ret := gulu.Ret.NewResult()
635 defer c.JSON(http.StatusOK, ret)
636
637 arg, ok := util.JsonArg(c, ret)
638 if !ok {
639 return
640 }
641
642 token := arg["token"].(string)
643 code := arg["code"].(string)
644 data, err := model.Login2fa(token, code)
645 if err != nil {
646 ret.Code = -1
647 ret.Msg = err.Error()
648 return
649 }
650 ret.Data = data
651}
652
653func setEmoji(c *gin.Context) {
654 ret := gulu.Ret.NewResult()
655 defer c.JSON(http.StatusOK, ret)
656
657 arg, ok := util.JsonArg(c, ret)
658 if !ok {
659 return
660 }
661
662 argEmoji := arg["emoji"].([]interface{})
663 var emoji []string
664 for _, ae := range argEmoji {
665 e := ae.(string)
666 if strings.Contains(e, ".") {
667 // XSS through emoji name https://github.com/siyuan-note/siyuan/issues/15034
668 e = util.FilterUploadEmojiFileName(e)
669 }
670 emoji = append(emoji, e)
671 }
672
673 model.Conf.Editor.Emoji = emoji
674}