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 model
18
19import (
20 "fmt"
21 "os"
22 "path/filepath"
23 "sync"
24
25 "github.com/88250/gulu"
26 "github.com/siyuan-note/filelock"
27 "github.com/siyuan-note/logging"
28 "github.com/siyuan-note/siyuan/kernel/bazaar"
29 "github.com/siyuan-note/siyuan/kernel/util"
30)
31
32// Petal represents a plugin's management status.
33type Petal struct {
34 Name string `json:"name"` // Plugin name
35 DisplayName string `json:"displayName"` // Plugin display name
36 Enabled bool `json:"enabled"` // Whether enabled
37 Incompatible bool `json:"incompatible"` // Whether incompatible
38 DisabledInPublish bool `json:"disabledInPublish"` // Whether disabled in publish mode
39
40 JS string `json:"js"` // JS code
41 CSS string `json:"css"` // CSS code
42 I18n map[string]interface{} `json:"i18n"` // i18n text
43}
44
45func SetPetalEnabled(name string, enabled bool, frontend string) (ret *Petal, err error) {
46 petals := getPetals()
47
48 found, displayName, incompatible, disabledInPublish := bazaar.ParseInstalledPlugin(name, frontend)
49 if !found {
50 logging.LogErrorf("plugin [%s] not found", name)
51 return
52 }
53
54 ret = getPetalByName(name, petals)
55 if nil == ret {
56 ret = &Petal{
57 Name: name,
58 }
59 petals = append(petals, ret)
60 }
61 ret.DisplayName = displayName
62 ret.Enabled = enabled
63 ret.Incompatible = incompatible
64 ret.DisabledInPublish = disabledInPublish
65
66 if incompatible {
67 err = fmt.Errorf(Conf.Language(205))
68 logging.LogInfof("plugin [%s] is incompatible [%s]", name, frontend)
69 return
70 }
71
72 savePetals(petals)
73 loadCode(ret)
74 return
75}
76
77func LoadPetals(frontend string, isPublish bool) (ret []*Petal) {
78 ret = []*Petal{}
79
80 if Conf.Bazaar.PetalDisabled {
81 return
82 }
83
84 if !Conf.Bazaar.Trust {
85 // 移动端没有集市模块,所以要默认开启,桌面端和 Docker 容器需要用户手动确认过信任后才能开启
86 if util.ContainerStd == util.Container || util.ContainerDocker == util.Container {
87 return
88 }
89 }
90
91 petals := getPetals()
92 for _, petal := range petals {
93 installPath := filepath.Join(util.DataDir, "plugins", petal.Name)
94 if !filelock.IsExist(installPath) {
95 continue
96 }
97
98 _, petal.DisplayName, petal.Incompatible, petal.DisabledInPublish = bazaar.ParseInstalledPlugin(petal.Name, frontend)
99 if !petal.Enabled || petal.Incompatible || (isPublish && petal.DisabledInPublish) {
100 continue
101 }
102
103 loadCode(petal)
104 ret = append(ret, petal)
105 }
106 return
107}
108
109func loadCode(petal *Petal) {
110 pluginDir := filepath.Join(util.DataDir, "plugins", petal.Name)
111 jsPath := filepath.Join(pluginDir, "index.js")
112 if !filelock.IsExist(jsPath) {
113 logging.LogErrorf("plugin [%s] js not found", petal.Name)
114 return
115 }
116
117 data, err := filelock.ReadFile(jsPath)
118 if err != nil {
119 logging.LogErrorf("read plugin [%s] js failed: %s", petal.Name, err)
120 return
121 }
122 petal.JS = string(data)
123
124 cssPath := filepath.Join(pluginDir, "index.css")
125 if filelock.IsExist(cssPath) {
126 data, err = filelock.ReadFile(cssPath)
127 if err != nil {
128 logging.LogErrorf("read plugin [%s] css failed: %s", petal.Name, err)
129 } else {
130 petal.CSS = string(data)
131 }
132 }
133
134 i18nDir := filepath.Join(pluginDir, "i18n")
135 if gulu.File.IsDir(i18nDir) {
136 langJSONs, readErr := os.ReadDir(i18nDir)
137 if nil != readErr {
138 logging.LogErrorf("read plugin [%s] i18n failed: %s", petal.Name, readErr)
139 } else if 0 < len(langJSONs) {
140 preferredLang := Conf.Lang + ".json"
141 foundPreferredLang := false
142 foundEnUS := false
143 foundZhCN := false
144 for _, langJSON := range langJSONs {
145 if langJSON.Name() == preferredLang {
146 foundPreferredLang = true
147 break
148 }
149 if langJSON.Name() == "en_US.json" {
150 foundEnUS = true
151 }
152 if langJSON.Name() == "zh_CN.json" {
153 foundZhCN = true
154 }
155 }
156
157 if !foundPreferredLang {
158 if foundEnUS {
159 preferredLang = "en_US.json"
160 } else if foundZhCN {
161 preferredLang = "zh_CN.json"
162 } else {
163 preferredLang = langJSONs[0].Name()
164 }
165 }
166
167 if langFilePath := filepath.Join(i18nDir, preferredLang); gulu.File.IsExist(langFilePath) {
168 data, err = filelock.ReadFile(langFilePath)
169 if err != nil {
170 logging.LogErrorf("read plugin [%s] i18n failed: %s", petal.Name, err)
171 } else {
172 petal.I18n = map[string]interface{}{}
173 if err = gulu.JSON.UnmarshalJSON(data, &petal.I18n); err != nil {
174 logging.LogErrorf("unmarshal plugin [%s] i18n failed: %s", petal.Name, err)
175 }
176 }
177 }
178 }
179 }
180}
181
182var petalsStoreLock = sync.Mutex{}
183
184func savePetals(petals []*Petal) {
185 petalsStoreLock.Lock()
186 defer petalsStoreLock.Unlock()
187 savePetals0(petals)
188}
189
190func savePetals0(petals []*Petal) {
191 if 1 > len(petals) {
192 petals = []*Petal{}
193 }
194
195 petalDir := filepath.Join(util.DataDir, "storage", "petal")
196 confPath := filepath.Join(petalDir, "petals.json")
197 data, err := gulu.JSON.MarshalIndentJSON(petals, "", "\t")
198 if err != nil {
199 logging.LogErrorf("marshal petals failed: %s", err)
200 return
201 }
202 if err = filelock.WriteFile(confPath, data); err != nil {
203 logging.LogErrorf("write petals [%s] failed: %s", confPath, err)
204 return
205 }
206}
207
208func getPetals() (ret []*Petal) {
209 petalsStoreLock.Lock()
210 defer petalsStoreLock.Unlock()
211
212 ret = []*Petal{}
213 petalDir := filepath.Join(util.DataDir, "storage", "petal")
214 if err := os.MkdirAll(petalDir, 0755); err != nil {
215 logging.LogErrorf("create petal dir [%s] failed: %s", petalDir, err)
216 return
217 }
218
219 confPath := filepath.Join(petalDir, "petals.json")
220 if !filelock.IsExist(confPath) {
221 data, err := gulu.JSON.MarshalIndentJSON(ret, "", "\t")
222 if err != nil {
223 logging.LogErrorf("marshal petals failed: %s", err)
224 return
225 }
226 if err = filelock.WriteFile(confPath, data); err != nil {
227 logging.LogErrorf("write petals [%s] failed: %s", confPath, err)
228 return
229 }
230 return
231 }
232
233 data, err := filelock.ReadFile(confPath)
234 if err != nil {
235 logging.LogErrorf("read petal file [%s] failed: %s", confPath, err)
236 return
237 }
238
239 if err = gulu.JSON.UnmarshalJSON(data, &ret); err != nil {
240 logging.LogErrorf("unmarshal petals failed: %s", err)
241 return
242 }
243
244 var tmp []*Petal
245 pluginsDir := filepath.Join(util.DataDir, "plugins")
246 for _, petal := range ret {
247 if petal.Enabled && filelock.IsExist(filepath.Join(pluginsDir, petal.Name)) {
248 tmp = append(tmp, petal)
249 }
250 }
251 if len(tmp) != len(ret) {
252 savePetals0(tmp)
253 ret = tmp
254 }
255 if 1 > len(ret) {
256 ret = []*Petal{}
257 }
258 return
259}
260
261func getPetalByName(name string, petals []*Petal) (ret *Petal) {
262 for _, p := range petals {
263 if name == p.Name {
264 ret = p
265 break
266 }
267 }
268 return
269}