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) 2026 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 :
23 : #include "fetchapi.h"
24 :
25 : #include <QBuffer>
26 : #include <QJSEngine>
27 : #include <QJSValueIterator>
28 : #include <QUrl>
29 : #include <QVariantMap>
30 :
31 : #include "global/progress.h"
32 :
33 : #include "log.h"
34 :
35 : using namespace muse;
36 : using namespace muse::network;
37 : using namespace muse::network::api;
38 :
39 0 : FetchApi::FetchApi(muse::api::IApiEngine* e)
40 0 : : muse::api::ApiObject(e)
41 : {
42 0 : m_self = engine()->newQObject(this);
43 0 : }
44 :
45 0 : QJSValue FetchApi::fetch(QJSValue request, QJSValue options)
46 : {
47 0 : QUrl url;
48 0 : if (request.isString()) {
49 0 : url = QUrl(request.toString());
50 : } else {
51 0 : NOT_SUPPORTED;
52 : }
53 :
54 0 : muse::api::JsPromise promise = engine()->newPromise();
55 :
56 0 : if (!url.isValid() || (url.scheme() != QStringLiteral("http") && url.scheme() != QStringLiteral("https"))) {
57 0 : promise.reject.call({ QJSValue(QStringLiteral("Invalid url")) });
58 0 : return promise.promise;
59 : }
60 :
61 0 : const QString method = stringOption(options, "method", "GET").toUpper();
62 0 : LOGDA() << "fetch:" << method << " " << url;
63 :
64 0 : const RequestHeaders headers = requestHeaders(options);
65 0 : const QByteArray body = byteArrayOption(options, "body", QByteArray());
66 :
67 0 : auto outgoing = std::make_shared<QBuffer>();
68 0 : outgoing->setData(body);
69 :
70 0 : auto incoming = std::make_shared<QBuffer>();
71 0 : RetVal<Progress> progress = execRequest(method, url, outgoing, incoming, headers);
72 0 : if (!progress.ret) {
73 0 : promise.reject.call({ QJSValue(QString::fromStdString(progress.ret.text())) });
74 0 : return promise.promise;
75 : }
76 :
77 0 : progress.val.finished().onReceive(this, [this, incoming, resolve = promise.resolve, reject = promise.reject]
78 : (const ProgressResult& res)
79 : {
80 0 : if (!res.ret) {
81 0 : reject.call({ QJSValue(QString::fromStdString(res.ret.text())) });
82 0 : return;
83 : }
84 :
85 0 : const int status = res.ret.data<int>("status", 0);
86 0 : const QString statusText = QString::fromStdString(res.ret.data<std::string>("statusText", std::string()));
87 0 : const QVariantMap headers = res.ret.data<QVariantMap>("headers", QVariantMap());
88 0 : resolve.call({ makeResponse(status, statusText, incoming->data(), headers) });
89 0 : });
90 :
91 0 : return promise.promise;
92 0 : }
93 :
94 0 : QJSValue FetchApi::toUtf8(const QJSValue& bytes) const
95 : {
96 0 : if (bytes.isUndefined() || bytes.isNull()) {
97 0 : return QJSValue(QString());
98 : }
99 :
100 0 : QByteArray data = qjsvalue_cast<QByteArray>(bytes);
101 0 : return QJSValue(QString::fromUtf8(data));
102 0 : }
103 :
104 0 : QString FetchApi::stringOption(const QJSValue& options, const QString& key, const QString& def) const
105 : {
106 0 : if (!options.isObject()) {
107 0 : return def;
108 : }
109 :
110 0 : const QJSValue value = options.property(key);
111 0 : if (!value.isString()) {
112 0 : return def;
113 : }
114 :
115 0 : return value.toString();
116 0 : }
117 :
118 0 : QByteArray FetchApi::byteArrayOption(const QJSValue& options, const QString& key, const QByteArray& def) const
119 : {
120 0 : if (!options.isObject()) {
121 0 : return def;
122 : }
123 :
124 0 : const QJSValue value = options.property(key);
125 0 : if (value.isUndefined() || value.isNull()) {
126 0 : return def;
127 : }
128 :
129 0 : if (value.isString()) {
130 0 : return value.toString().toUtf8();
131 : }
132 :
133 0 : return qjsvalue_cast<QByteArray>(value);
134 0 : }
135 :
136 0 : RequestHeaders FetchApi::requestHeaders(const QJSValue& options) const
137 : {
138 0 : RequestHeaders headers = configuration()->defaultHeaders();
139 0 : if (!options.isObject()) {
140 0 : return headers;
141 : }
142 :
143 0 : const QJSValue headersValue = options.property("headers");
144 0 : if (!headersValue.isObject()) {
145 0 : return headers;
146 : }
147 :
148 0 : QJSValueIterator it(headersValue);
149 0 : while (it.hasNext()) {
150 0 : it.next();
151 0 : if (!it.value().isString()) {
152 0 : continue;
153 : }
154 :
155 0 : const QString name = it.name();
156 0 : const QString value = it.value().toString();
157 0 : headers.rawHeaders.insert(name.toUtf8(), value.toUtf8());
158 0 : }
159 :
160 0 : return headers;
161 0 : }
162 :
163 0 : RetVal<Progress> FetchApi::execRequest(const QString& method, const QUrl& url,
164 : QIODevicePtr outgoing, QIODevicePtr incoming,
165 : const RequestHeaders& headers)
166 : {
167 0 : if (!m_networkManager) {
168 0 : m_networkManager = networkManagerCreator()->makeNetworkManager();
169 : }
170 :
171 0 : if (method == QStringLiteral("GET")) {
172 0 : return m_networkManager->get(url, incoming, headers);
173 : }
174 0 : if (method == QStringLiteral("HEAD")) {
175 0 : return m_networkManager->head(url, headers);
176 : }
177 0 : if (method == QStringLiteral("DELETE")) {
178 0 : return m_networkManager->del(url, incoming, headers);
179 : }
180 :
181 0 : if (method == QStringLiteral("POST")) {
182 0 : return m_networkManager->post(url, outgoing, incoming, headers);
183 : }
184 0 : if (method == QStringLiteral("PUT")) {
185 0 : return m_networkManager->put(url, outgoing, incoming, headers);
186 : }
187 0 : if (method == QStringLiteral("PATCH")) {
188 0 : return m_networkManager->patch(url, outgoing, incoming, headers);
189 : }
190 :
191 0 : return RetVal<Progress>::make_ret(Ret::Code::NotSupported, "Unsupported method: " + method.toStdString());
192 : }
193 :
194 0 : QJSValue FetchApi::responseFactory()
195 : {
196 0 : if (m_responseFactory.isCallable()) {
197 0 : return m_responseFactory;
198 : }
199 :
200 0 : m_responseFactory = engine()->evaluate(QStringLiteral(
201 : R"(
202 : (function (self, status, statusText, body, headers) {
203 :
204 : return {
205 : status: status,
206 : statusText: statusText,
207 : ok: status >= 200 && status < 300,
208 : headers: {
209 : get: function (name) {
210 : var key = String(name).toLowerCase()
211 : return Object.prototype.hasOwnProperty.call(headers, key) ? headers[key] : null
212 : },
213 : has: function (name) {
214 : return Object.prototype.hasOwnProperty.call(headers, String(name).toLowerCase())
215 : }
216 : },
217 : text: function () {
218 : return Promise.resolve(self.toUtf8(body))
219 : },
220 : json: function () {
221 : return new Promise(function (resolve, reject) {
222 : try {
223 : resolve(JSON.parse(self.toUtf8(body)))
224 : } catch (e) {
225 : reject(e)
226 : }
227 : })
228 : },
229 : arrayBuffer: function () {
230 : return Promise.resolve(body)
231 : }
232 : }
233 : })
234 0 : )"));
235 :
236 0 : if (m_responseFactory.isError()) {
237 0 : LOGE() << "failed to create response factory: " << m_responseFactory.toString();
238 : }
239 :
240 0 : return m_responseFactory;
241 : }
242 :
243 0 : QJSValue FetchApi::makeResponse(int status, const QString& statusText, const QByteArray& body, const QVariantMap& headers)
244 : {
245 0 : QJSValue headersObj = engine()->newObject();
246 0 : for (auto it = headers.cbegin(); it != headers.cend(); ++it) {
247 0 : headersObj.setProperty(it.key(), it.value().toString());
248 : }
249 :
250 0 : return responseFactory().call({
251 0 : m_self,
252 : QJSValue(status),
253 : QJSValue(statusText),
254 0 : engine()->newArrayBuffer(body),
255 : headersObj
256 0 : });
257 0 : }
|