mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-06-30 07:46:02 +00:00
* feat(kernel-plugin): Prevent storage.put and storage.remove in read-only mode * feat(kernel-plugin): Reload petal plugin on install When installing a Bazaar package, ensure that if the corresponding petal exists and is enabled we call SetPetalEnabled to trigger a kernel plugin reload before scheduling the plugin reload. This ensures the petal's kernel plugin picks up the newly installed package immediately. * feat(kernel-plugin): Use gin-contrib/sse.Event for SSE handling Replace the custom sseEvent type with gin-contrib/sse.Event and update the SSE handling pipeline. The JS handler now accepts an event object (data, event, id, retry), which is validated and converted to sse.Event before being sent on the channel; the server renders events with c.Render(-1, e). Added import for github.com/gin-contrib/sse and bumped gin-contrib/sse to v1.1.1 in go.mod (go.sum updated accordingly). * feat(kernel-plugin): Add global kernel plugin start/stop hooks Introduce OnKernelPluginsStart and OnKernelPluginsStop hooks and replace the previous OnKernelPluginShutdown usage. Update Close to call OnKernelPluginsStop and wire the new hooks in the plugin manager (OnKernelPluginsStart -> GetManager().Start, OnKernelPluginsStop -> GetManager().Stop). Existing per-plugin OnKernelPluginStart/OnKernelPluginStop handlers remain unchanged. This adds global lifecycle hooks to start and stop all valid plugins via the manager. * feat(kernel-plugin): Add plugin manager state and bazaar handling Call kernel plugin start/stop callbacks when bazaar settings change and add lifecycle state to the PluginManager. setting.go now triggers model.OnKernelPluginsStop or model.OnKernelPluginsStart when Bazaar.PetalDisabled or Bazaar.Trust change. plugin/manager.go introduces a PluginManagerState enum, initializes the manager as stopped, adds a State() accessor, and changes lifecycleMu to an RWMutex. Start/Stop now check the current state to avoid redundant operations, skip starting if bazaar config disables plugins, and update the manager state after start/stop, with additional log messages. These changes improve safety around concurrent start/stop and ensure bazaar configuration is honored. * feat(kernel-plugin): Add plugin hot-reload watcher and per-plugin dirs Add fsnotify-based watcher and manager context to detect kernel.js changes and trigger plugin hot-reloads. Introduce pluginsDir, pluginDir and storageDir fields and use storageDir in storage API to prevent path traversal. Replace single plugin mutex map with per-plugin mutex map, serialize start/stop per plugin, and adjust StartPlugin/StopPlugin flow to stop first for hot-reload, honor Bazaar config flags, and propagate start context. Update KernelPlugin to accept a start context and set per-plugin directories. Add helper functions to add/remove plugin dirs from the watcher and simplify GetLoadedPlugin. (Note: go.sum changes are present but not described.) * feat(kernel-plugin): Add storage watcher and plugin reload improvements Introduce per-plugin fsnotify storage watcher and expose a JS API for it (siyuan.storage.watcher.add/remove). KernelPlugin now accepts a parent context, creates an fsnotify.Watcher, starts a storage watch goroutine that publishes standardized runtime events (createEventMessage) for fs changes, and provides addStorageWatch/removeStorageWatch helpers. Manager API renamed to addPluginSourceWatch/removePluginSourceWatch with safer watcher handling and clearer reload logging; StartPlugin now passes the manager context into NewKernelPlugin. Also refactor runtime start/stop event publishing to use createEventMessage and add minor logging/error handling for watcher creation and path watch operations. The JS watcher object is frozen and integrated into siyuan.storage.
787 lines
17 KiB
Go
787 lines
17 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 api
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/88250/gulu"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/siyuan-note/siyuan/kernel/conf"
|
|
"github.com/siyuan-note/siyuan/kernel/model"
|
|
"github.com/siyuan-note/siyuan/kernel/server/proxy"
|
|
"github.com/siyuan-note/siyuan/kernel/sql"
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
)
|
|
|
|
func setEditorReadOnly(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
readOnly := arg["readonly"].(bool)
|
|
|
|
oldReadOnly := model.Conf.Editor.ReadOnly
|
|
model.Conf.Editor.ReadOnly = readOnly
|
|
model.Conf.Save()
|
|
|
|
if oldReadOnly != model.Conf.Editor.ReadOnly {
|
|
util.BroadcastByType("protyle", "readonly", 0, "", model.Conf.Editor.ReadOnly)
|
|
util.BroadcastByType("main", "readonly", 0, "", model.Conf.Editor.ReadOnly)
|
|
}
|
|
}
|
|
|
|
func setConfSnippet(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
snippet := &conf.Snpt{}
|
|
if err = gulu.JSON.UnmarshalJSON(param, snippet); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
model.Conf.Snippet = snippet
|
|
model.Conf.Save()
|
|
|
|
ret.Data = snippet
|
|
model.PushReloadSnippet(snippet)
|
|
}
|
|
|
|
func addVirtualBlockRefExclude(c *gin.Context) {
|
|
// Add internal kernel API `/api/setting/addVirtualBlockRefExclude` https://github.com/siyuan-note/siyuan/issues/9909
|
|
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
keywordsArg := arg["keywords"]
|
|
var keywords []string
|
|
for _, k := range keywordsArg.([]any) {
|
|
keywords = append(keywords, k.(string))
|
|
}
|
|
|
|
model.AddVirtualBlockRefExclude(keywords)
|
|
util.BroadcastByType("main", "setConf", 0, "", model.Conf)
|
|
}
|
|
|
|
func addVirtualBlockRefInclude(c *gin.Context) {
|
|
// Add internal kernel API `/api/setting/addVirtualBlockRefInclude` https://github.com/siyuan-note/siyuan/issues/9909
|
|
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
keywordsArg := arg["keywords"]
|
|
var keywords []string
|
|
for _, k := range keywordsArg.([]any) {
|
|
keywords = append(keywords, k.(string))
|
|
}
|
|
|
|
model.AddVirtualBlockRefInclude(keywords)
|
|
util.BroadcastByType("main", "setConf", 0, "", model.Conf)
|
|
}
|
|
|
|
func refreshVirtualBlockRef(c *gin.Context) {
|
|
// Add internal kernel API `/api/setting/refreshVirtualBlockRef` https://github.com/siyuan-note/siyuan/issues/9829
|
|
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
model.ResetVirtualBlockRefCache()
|
|
util.BroadcastByType("main", "setConf", 0, "", model.Conf)
|
|
}
|
|
|
|
func setBazaar(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
bazaar := &conf.Bazaar{}
|
|
if err = gulu.JSON.UnmarshalJSON(param, bazaar); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
if bazaar.PetalDisabled || !bazaar.Trust {
|
|
// disable all kernel plugins
|
|
if model.OnKernelPluginsStop != nil {
|
|
model.OnKernelPluginsStop()
|
|
}
|
|
} else {
|
|
// enable all kernel plugins
|
|
if model.OnKernelPluginsStart != nil {
|
|
model.OnKernelPluginsStart()
|
|
}
|
|
}
|
|
|
|
model.Conf.Bazaar = bazaar
|
|
model.Conf.Save()
|
|
|
|
ret.Data = bazaar
|
|
}
|
|
|
|
func setAI(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
ai := &conf.AI{}
|
|
if err = gulu.JSON.UnmarshalJSON(param, ai); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
if 5 > ai.OpenAI.APITimeout {
|
|
ai.OpenAI.APITimeout = 5
|
|
}
|
|
if 600 < ai.OpenAI.APITimeout {
|
|
ai.OpenAI.APITimeout = 600
|
|
}
|
|
|
|
if 0 > ai.OpenAI.APIMaxTokens {
|
|
ai.OpenAI.APIMaxTokens = 0
|
|
}
|
|
|
|
if 0 >= ai.OpenAI.APITemperature || 2 < ai.OpenAI.APITemperature {
|
|
ai.OpenAI.APITemperature = 1.0
|
|
}
|
|
|
|
if 1 > ai.OpenAI.APIMaxContexts || 64 < ai.OpenAI.APIMaxContexts {
|
|
ai.OpenAI.APIMaxContexts = 7
|
|
}
|
|
|
|
model.Conf.AI = ai
|
|
model.Conf.Save()
|
|
|
|
ret.Data = ai
|
|
}
|
|
|
|
func setFlashcard(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
flashcard := &conf.Flashcard{}
|
|
if err = gulu.JSON.UnmarshalJSON(param, flashcard); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
if 0 > flashcard.NewCardLimit {
|
|
flashcard.NewCardLimit = 20
|
|
}
|
|
|
|
if 0 > flashcard.ReviewCardLimit {
|
|
flashcard.ReviewCardLimit = 200
|
|
}
|
|
|
|
model.Conf.Flashcard = flashcard
|
|
model.Conf.Save()
|
|
|
|
ret.Data = flashcard
|
|
}
|
|
|
|
func setAccount(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
account := &conf.Account{}
|
|
if err = gulu.JSON.UnmarshalJSON(param, account); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
model.Conf.Account = account
|
|
model.Conf.Save()
|
|
|
|
ret.Data = model.Conf.Account
|
|
}
|
|
|
|
func setEditor(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
oldGenerateHistoryInterval := model.Conf.Editor.GenerateHistoryInterval
|
|
|
|
editor := conf.NewEditor()
|
|
if err = gulu.JSON.UnmarshalJSON(param, editor); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
if "" == editor.PlantUMLServePath {
|
|
editor.PlantUMLServePath = "https://www.plantuml.com/plantuml/svg/~1"
|
|
}
|
|
|
|
if "" == editor.KaTexMacros {
|
|
editor.KaTexMacros = "{}"
|
|
}
|
|
|
|
if 1 > editor.HistoryRetentionDays {
|
|
editor.HistoryRetentionDays = 30
|
|
}
|
|
if 3650 < editor.HistoryRetentionDays {
|
|
editor.HistoryRetentionDays = 3650
|
|
}
|
|
|
|
if nil == editor.FloatWindowDelay {
|
|
v := 620
|
|
editor.FloatWindowDelay = &v
|
|
} else {
|
|
*editor.FloatWindowDelay = max(0, min(2000, *editor.FloatWindowDelay))
|
|
}
|
|
|
|
oldVirtualBlockRef := model.Conf.Editor.VirtualBlockRef
|
|
oldVirtualBlockRefInclude := model.Conf.Editor.VirtualBlockRefInclude
|
|
oldVirtualBlockRefExclude := model.Conf.Editor.VirtualBlockRefExclude
|
|
oldReadOnly := model.Conf.Editor.ReadOnly
|
|
|
|
model.Conf.Editor = editor
|
|
model.Conf.Save()
|
|
|
|
if oldGenerateHistoryInterval != model.Conf.Editor.GenerateHistoryInterval {
|
|
model.GenerateFileHistory()
|
|
model.ChangeHistoryTick(editor.GenerateHistoryInterval)
|
|
}
|
|
|
|
if oldVirtualBlockRef != model.Conf.Editor.VirtualBlockRef ||
|
|
oldVirtualBlockRefInclude != model.Conf.Editor.VirtualBlockRefInclude ||
|
|
oldVirtualBlockRefExclude != model.Conf.Editor.VirtualBlockRefExclude {
|
|
model.ResetVirtualBlockRefCache()
|
|
}
|
|
|
|
if oldReadOnly != model.Conf.Editor.ReadOnly {
|
|
util.BroadcastByType("protyle", "readonly", 0, "", model.Conf.Editor.ReadOnly)
|
|
util.BroadcastByType("main", "readonly", 0, "", model.Conf.Editor.ReadOnly)
|
|
}
|
|
|
|
util.MarkdownSettings = model.Conf.Editor.Markdown
|
|
|
|
ret.Data = model.Conf.Editor
|
|
}
|
|
|
|
func setExport(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
export := &conf.Export{}
|
|
if err = gulu.JSON.UnmarshalJSON(param, export); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
ret.Data = map[string]any{"closeTimeout": 5000}
|
|
return
|
|
}
|
|
|
|
if "" != export.PandocBin {
|
|
if !util.IsValidPandocBin(export.PandocBin) {
|
|
util.PushErrMsg(fmt.Sprintf(model.Conf.Language(117), export.PandocBin), 5000)
|
|
export.PandocBin = util.PandocBinPath
|
|
} else {
|
|
util.PandocBinPath = export.PandocBin
|
|
}
|
|
}
|
|
|
|
model.Conf.Export = export
|
|
model.Conf.Save()
|
|
|
|
ret.Data = model.Conf.Export
|
|
}
|
|
|
|
func setFiletree(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
fileTree := conf.NewFileTree()
|
|
if err = gulu.JSON.UnmarshalJSON(param, fileTree); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
fileTree.RefCreateSavePath = util.TrimSpaceInPath(fileTree.RefCreateSavePath)
|
|
if "" != fileTree.RefCreateSavePath {
|
|
if !strings.HasSuffix(fileTree.RefCreateSavePath, "/") {
|
|
fileTree.RefCreateSavePath += "/"
|
|
}
|
|
}
|
|
|
|
fileTree.DocCreateSavePath = util.TrimSpaceInPath(fileTree.DocCreateSavePath)
|
|
|
|
fileTree.ShorthandSavePath = util.TrimSpaceInPath(fileTree.ShorthandSavePath)
|
|
if "" != fileTree.ShorthandSavePath {
|
|
if !strings.HasPrefix(fileTree.ShorthandSavePath, "/") {
|
|
fileTree.ShorthandSavePath = "/" + fileTree.ShorthandSavePath
|
|
}
|
|
}
|
|
|
|
if 1 > fileTree.MaxOpenTabCount {
|
|
fileTree.MaxOpenTabCount = 8
|
|
}
|
|
if 32 < fileTree.MaxOpenTabCount {
|
|
fileTree.MaxOpenTabCount = 32
|
|
}
|
|
|
|
if conf.MinFileTreeRecentDocsListCount > fileTree.RecentDocsMaxListCount {
|
|
fileTree.RecentDocsMaxListCount = conf.MinFileTreeRecentDocsListCount
|
|
}
|
|
if conf.MaxFileTreeRecentDocsListCount < fileTree.RecentDocsMaxListCount {
|
|
fileTree.RecentDocsMaxListCount = conf.MaxFileTreeRecentDocsListCount
|
|
}
|
|
|
|
model.Conf.FileTree = fileTree
|
|
model.Conf.Save()
|
|
|
|
util.UseSingleLineSave = model.Conf.FileTree.UseSingleLineSave
|
|
util.LargeFileWarningSize = model.Conf.FileTree.LargeFileWarningSize
|
|
|
|
ret.Data = model.Conf.FileTree
|
|
}
|
|
|
|
func setSearch(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
s := &conf.Search{}
|
|
if err = gulu.JSON.UnmarshalJSON(param, s); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
if 32 > s.Limit {
|
|
s.Limit = 32
|
|
}
|
|
|
|
oldCaseSensitive := model.Conf.Search.CaseSensitive
|
|
oldIndexAssetPath := model.Conf.Search.IndexAssetPath
|
|
|
|
oldVirtualRefName := model.Conf.Search.VirtualRefName
|
|
oldVirtualRefAlias := model.Conf.Search.VirtualRefAlias
|
|
oldVirtualRefAnchor := model.Conf.Search.VirtualRefAnchor
|
|
oldVirtualRefDoc := model.Conf.Search.VirtualRefDoc
|
|
|
|
model.Conf.Search = s
|
|
model.Conf.Save()
|
|
|
|
sql.SetCaseSensitive(s.CaseSensitive)
|
|
sql.SetIndexAssetPath(s.IndexAssetPath)
|
|
|
|
if needFullReindex := s.CaseSensitive != oldCaseSensitive || s.IndexAssetPath != oldIndexAssetPath; needFullReindex {
|
|
model.FullReindex(false)
|
|
}
|
|
|
|
if oldVirtualRefName != s.VirtualRefName ||
|
|
oldVirtualRefAlias != s.VirtualRefAlias ||
|
|
oldVirtualRefAnchor != s.VirtualRefAnchor ||
|
|
oldVirtualRefDoc != s.VirtualRefDoc {
|
|
model.ResetVirtualBlockRefCache()
|
|
}
|
|
ret.Data = s
|
|
}
|
|
|
|
func setKeymap(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg["data"])
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
keymap := &conf.Keymap{}
|
|
if err = gulu.JSON.UnmarshalJSON(param, keymap); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
model.Conf.Keymap = keymap
|
|
model.Conf.Save()
|
|
}
|
|
|
|
func setAppearance(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
appearance := &conf.Appearance{}
|
|
if err = gulu.JSON.UnmarshalJSON(param, appearance); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
model.Conf.Appearance = appearance
|
|
util.StatusBarCfg = model.Conf.Appearance.StatusBar
|
|
model.Conf.Lang = appearance.Lang
|
|
util.Lang = model.Conf.Lang
|
|
model.Conf.Save()
|
|
model.InitAppearance()
|
|
|
|
ret.Data = model.Conf.Appearance
|
|
util.BroadcastByType("main", "setAppearance", 0, "", model.Conf.Appearance)
|
|
}
|
|
|
|
func setIcon(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var icon string
|
|
if !util.ParseJsonArgs(arg, ret,
|
|
util.BindJsonArg("icon", &icon, true, true),
|
|
) {
|
|
return
|
|
}
|
|
|
|
if err := model.SetIcon(icon); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
model.InitAppearance()
|
|
util.BroadcastByType("main", "setAppearance", 0, "", model.Conf.Appearance)
|
|
}
|
|
|
|
func setTheme(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var theme, appearanceMode string
|
|
var modesRaw []any
|
|
if !util.ParseJsonArgs(arg, ret,
|
|
util.BindJsonArg("theme", &theme, false, false),
|
|
util.BindJsonArg("modes", &modesRaw, false, false),
|
|
util.BindJsonArg("appearanceMode", &appearanceMode, false, false),
|
|
) {
|
|
return
|
|
}
|
|
|
|
theme, appearanceMode = strings.TrimSpace(theme), strings.TrimSpace(appearanceMode)
|
|
modes := make([]int, 0, 2)
|
|
if theme != "" {
|
|
for _, m := range modesRaw {
|
|
mf, ok := m.(float64)
|
|
if !ok {
|
|
break
|
|
}
|
|
mi := int(mf)
|
|
if mi != 0 && mi != 1 {
|
|
break
|
|
}
|
|
modes = append(modes, mi)
|
|
}
|
|
if len(modes) == 0 {
|
|
ret.Code = -1
|
|
ret.Msg = "[modes] is required ([0] for light, [1] for dark, [0,1] for both)"
|
|
return
|
|
}
|
|
}
|
|
// 没有 theme 时静默忽略 modes
|
|
|
|
if err := model.SetTheme(theme, modes, appearanceMode); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
model.InitAppearance()
|
|
util.BroadcastByType("main", "setAppearance", 0, "", model.Conf.Appearance)
|
|
}
|
|
|
|
func setPublish(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
param, err := gulu.JSON.MarshalJSON(arg)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
publish := &conf.Publish{}
|
|
if err = gulu.JSON.UnmarshalJSON(param, publish); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
model.Conf.Publish = publish
|
|
model.Conf.Save()
|
|
|
|
port, err := proxy.InitPublishService()
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
|
|
ret.Data = map[string]any{
|
|
"port": port,
|
|
"publish": model.Conf.Publish,
|
|
}
|
|
|
|
util.BroadcastByType("main", "setPublish", 0, "", model.Conf.Publish)
|
|
}
|
|
|
|
func getPublish(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
if port, err := proxy.InitPublishService(); err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
} else {
|
|
ret.Data = map[string]any{
|
|
"port": port,
|
|
"publish": model.Conf.Publish,
|
|
}
|
|
}
|
|
}
|
|
|
|
func getCloudUser(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
if !model.IsAdminRoleContext(c) {
|
|
return
|
|
}
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
t := arg["token"]
|
|
var token string
|
|
if nil != t {
|
|
token = t.(string)
|
|
}
|
|
model.RefreshUser(token)
|
|
ret.Data = model.Conf.GetUser()
|
|
}
|
|
|
|
func logoutCloudUser(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
model.LogoutUser()
|
|
}
|
|
|
|
func login2faCloudUser(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
token := arg["token"].(string)
|
|
code := arg["code"].(string)
|
|
data, err := model.Login2fa(token, code)
|
|
if err != nil {
|
|
ret.Code = -1
|
|
ret.Msg = err.Error()
|
|
return
|
|
}
|
|
ret.Data = data
|
|
}
|
|
|
|
func setEmoji(c *gin.Context) {
|
|
ret := gulu.Ret.NewResult()
|
|
defer c.JSON(http.StatusOK, ret)
|
|
|
|
arg, ok := util.JsonArg(c, ret)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
argEmoji := arg["emoji"].([]any)
|
|
var emoji []string
|
|
for _, ae := range argEmoji {
|
|
e := ae.(string)
|
|
if strings.Contains(e, ".") {
|
|
// XSS through emoji name https://github.com/siyuan-note/siyuan/issues/15034
|
|
e = util.FilterUploadEmojiFileName(e)
|
|
}
|
|
emoji = append(emoji, e)
|
|
}
|
|
|
|
model.Conf.Editor.Emoji = emoji
|
|
}
|