Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2024 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 : #include "extensionsloader.h"
23 :
24 : #include "extensionstypes.h"
25 : #include "global/io/dir.h"
26 : #include "global/io/file.h"
27 : #include "global/io/fileinfo.h"
28 : #include "global/serialization/json.h"
29 : #include "global/stringutils.h"
30 : #include "global/runtime.h"
31 :
32 : #include "log.h"
33 : #include "types/ret.h"
34 :
35 : using namespace muse;
36 : using namespace muse::extensions;
37 :
38 : const std::string MANIFEST("manifest.json");
39 : const std::string DEV_EXTENSIONS("extensions/dev/");
40 :
41 : namespace {
42 : Val jsonValue(const JsonValue& value);
43 :
44 2 : ValMap jsonMap(const JsonObject& object)
45 : {
46 2 : ValMap result;
47 8 : for (const std::string& key : object.keys()) {
48 6 : result.emplace(key, jsonValue(object.value(key)));
49 2 : }
50 2 : return result;
51 0 : }
52 :
53 1 : ValList jsonList(const JsonArray& array)
54 : {
55 1 : ValList result;
56 1 : result.reserve(array.size());
57 3 : for (size_t index = 0; index < array.size(); ++index) {
58 2 : result.push_back(jsonValue(array.at(index)));
59 : }
60 1 : return result;
61 0 : }
62 :
63 8 : Val jsonValue(const JsonValue& value)
64 : {
65 8 : if (value.isBool()) {
66 1 : return Val(value.toBool());
67 : }
68 7 : if (value.isNumber()) {
69 1 : return Val(value.toDouble());
70 : }
71 6 : if (value.isString()) {
72 4 : return Val(value.toStdString());
73 : }
74 2 : if (value.isArray()) {
75 1 : return Val(jsonList(value.toArray()));
76 : }
77 1 : if (value.isObject()) {
78 1 : return Val(jsonMap(value.toObject()));
79 : }
80 0 : return {};
81 : }
82 : } // namespace
83 :
84 0 : ManifestList ExtensionsLoader::loadManifestList(const io::path_t& defPath, const io::path_t& extPath) const
85 : {
86 0 : TRACEFUNC;
87 :
88 0 : LOGD() << "try load extensions, def: " << defPath << ", user: " << extPath;
89 :
90 0 : ManifestList defaultManifests = manifestList(defPath);
91 0 : ManifestList externalManifests = manifestList(extPath);
92 :
93 0 : ManifestList retList;
94 0 : for (const Manifest& m : defaultManifests) {
95 0 : if (!m.isValid()) {
96 0 : continue;
97 : }
98 :
99 0 : if (!runtime::isDebug() && muse::strings::startsWith(m.uri.path(), DEV_EXTENSIONS)) {
100 0 : continue;
101 : }
102 :
103 0 : retList.push_back(m);
104 : }
105 :
106 0 : for (Manifest& m : externalManifests) {
107 0 : if (!m.isValid()) {
108 0 : continue;
109 : }
110 :
111 0 : if (!runtime::isDebug() && muse::strings::startsWith(m.uri.path(), DEV_EXTENSIONS)) {
112 0 : continue;
113 : }
114 :
115 0 : m.isRemovable = true;
116 0 : retList.push_back(m);
117 : }
118 :
119 0 : return retList;
120 0 : }
121 :
122 0 : ManifestList ExtensionsLoader::manifestList(const io::path_t& rootPath) const
123 : {
124 0 : ManifestList manifests;
125 0 : io::paths_t paths = manifestPaths(rootPath);
126 0 : for (const io::path_t& path : paths) {
127 0 : LOGD() << "parsing manifest: " << path;
128 0 : RetVal<Manifest> rv = parseManifest(path);
129 0 : if (!rv.ret) {
130 0 : LOGE() << "failed parse manifest: " << path << ", err: " << rv.ret.toString();
131 0 : continue;
132 : }
133 0 : Manifest& manifest = rv.val;
134 0 : manifest.path = path;
135 0 : resolvePaths(manifest, io::FileInfo(path).dirPath());
136 0 : manifests.push_back(manifest);
137 0 : }
138 :
139 0 : return manifests;
140 0 : }
141 :
142 0 : io::paths_t ExtensionsLoader::manifestPaths(const io::path_t& rootPath) const
143 : {
144 0 : RetVal<io::paths_t> paths = io::Dir::scanFiles(rootPath, { MANIFEST });
145 0 : if (!paths.ret) {
146 0 : LOGE() << "failed scan files, err: " << paths.ret.toString();
147 : }
148 0 : return paths.val;
149 0 : }
150 :
151 0 : RetVal<Manifest> ExtensionsLoader::parseManifest(const io::path_t& path) const
152 : {
153 0 : ByteArray data;
154 0 : Ret ret = io::File::readFile(path, data);
155 0 : if (!ret) {
156 0 : return RetVal<Manifest>::make_ret(ret);
157 : }
158 :
159 0 : return parseManifest(data);
160 0 : }
161 :
162 1 : RetVal<Manifest> ExtensionsLoader::parseManifest(const ByteArray& data) const
163 : {
164 1 : std::string jsonErr;
165 1 : JsonObject obj = JsonDocument::fromJson(data, &jsonErr).rootObject();
166 1 : if (!jsonErr.empty()) {
167 0 : return RetVal<Manifest>::make_ret(Ret::Code::BadData,
168 0 : "failed parse json, err: " + jsonErr
169 0 : );
170 : }
171 :
172 4 : auto check_required = [](const JsonObject& obj, const std::string& key) {
173 4 : if (!obj.contains(key)) {
174 : return muse::make_ret(Ret::Code::BadData,
175 0 : key + " is required but not specified"
176 0 : );
177 : }
178 4 : return muse::make_ok();
179 : };
180 :
181 1 : Manifest m;
182 : // required
183 1 : Ret ret = check_required(obj, "uri");
184 1 : if (!ret) {
185 0 : return RetVal<Manifest>::make_ret(ret);
186 : }
187 3 : m.uri = Uri(obj.value("uri").toStdString());
188 1 : if (m.uri.scheme() != EXTENSION_SCHEME) {
189 1 : LOGW() << "extension uri must have the scheme - " << EXTENSION_SCHEME << "://, uri: " << m.uri;
190 2 : m.uri.setScheme(std::string(EXTENSION_SCHEME));
191 : }
192 :
193 1 : ret = check_required(obj, "type");
194 1 : if (!ret) {
195 0 : return RetVal<Manifest>::make_ret(ret);
196 : }
197 3 : m.type = typeFromString(obj.value("type").toStdString());
198 :
199 1 : ret = check_required(obj, "title");
200 1 : if (!ret) {
201 0 : return RetVal<Manifest>::make_ret(ret);
202 : }
203 3 : m.title = obj.value("title").toString();
204 :
205 : // optional
206 3 : m.description = obj.value("description").toString();
207 3 : m.category = obj.value("category").toString();
208 3 : m.thumbnail = obj.value("thumbnail").toStdString();
209 3 : m.version = obj.value("version").toStdString();
210 3 : m.apiversion = obj.value("apiversion", DEFAULT_API_VERSION).toInt();
211 3 : const JsonValue contributes = obj.value("contributes");
212 1 : if (contributes.isObject()) {
213 1 : const JsonObject declared = contributes.toObject();
214 3 : for (const std::string& name : declared.keys()) {
215 2 : const JsonValue value = declared.value(name);
216 2 : if (!value.isArray()) {
217 1 : continue;
218 : }
219 1 : std::vector<ValMap> contribution;
220 1 : const JsonArray items = value.toArray();
221 1 : contribution.reserve(items.size());
222 2 : for (size_t index = 0; index < items.size(); ++index) {
223 1 : const JsonValue item = items.at(index);
224 1 : if (item.isObject()) {
225 1 : contribution.push_back(jsonMap(item.toObject()));
226 : }
227 1 : }
228 1 : m.contributes.emplace(name, std::move(contribution));
229 3 : }
230 1 : }
231 :
232 : //! NOTE Default for actions
233 3 : m.context = obj.value("ui_context", std::string(DEFAULT_CONTEXT)).toStdString();
234 3 : const bool modal = obj.value("modal", DEFAULT_MODAL).toBool();
235 :
236 : // actions (required)
237 1 : ret = check_required(obj, "actions");
238 1 : if (!ret) {
239 0 : return RetVal<Manifest>::make_ret(ret);
240 : }
241 :
242 3 : const JsonArray arr = obj.value("actions").toArray();
243 1 : for (size_t i = 0; i < arr.size(); ++i) {
244 0 : const JsonObject ao = arr.at(i).toObject();
245 0 : Action a;
246 : // required
247 0 : a.path = ao.value("path").toStdString();
248 0 : if (a.path.empty()) {
249 0 : return RetVal<Manifest>::make_ret(Ret::Code::BadData,
250 : "action `path` is required but not specified"
251 0 : );
252 : }
253 :
254 : // optional
255 0 : a.code = ao.value("code", String::number(i)).toStdString();
256 0 : a.type = ao.contains("type") ? typeFromString(ao.value("type").toStdString()) : m.type;
257 0 : a.modal = ao.value("modal", modal).toBool();
258 0 : a.title = ao.value("title").toString();
259 0 : std::string icon = ao.value("icon").toStdString();
260 0 : a.icon = ui::IconCode::fromString(icon.c_str());
261 0 : a.context = ao.value("ui_context", m.context).toStdString();
262 0 : a.showOnAppmenu = ao.value("show_on_appmenu", true).toBool();
263 0 : a.showOnToolbar = ao.value("show_on_toolbar", false).toBool();
264 0 : a.func = ao.value("func", "main").toStdString();
265 0 : a.apiversion = m.apiversion;
266 0 : m.actions.push_back(std::move(a));
267 0 : }
268 :
269 1 : return RetVal<Manifest>::make_ok(m);
270 1 : }
271 :
272 0 : void ExtensionsLoader::resolvePaths(Manifest& m, const io::path_t& rootDirPath) const
273 : {
274 0 : if (!m.thumbnail.empty()) {
275 0 : m.thumbnail = rootDirPath + "/" + m.thumbnail;
276 : }
277 :
278 0 : for (Action& a : m.actions) {
279 0 : a.path = rootDirPath + "/" + a.path;
280 : }
281 0 : }
|