diff --git a/package-lock.json b/package-lock.json
index 889339b63..dbbd8b82c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -153,6 +153,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1978,6 +1979,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -1990,6 +1992,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz",
"integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
@@ -2026,6 +2029,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
@@ -2052,6 +2056,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
@@ -2279,6 +2284,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@@ -2371,6 +2377,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
"integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
@@ -2392,6 +2399,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz",
"integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
@@ -4204,6 +4212,7 @@
"integrity": "sha512-Iax6UhrfZqJajA778c1d5DBFbSIqPOSrI34kpNIiNpWd8Jq7mFIa+Z60SQb5ZQDZuUxcCZikjz5BxinFjTkg7Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@module-federation/runtime-tools": "0.22.0",
"@rspack/binding": "1.7.6",
@@ -4493,8 +4502,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
@@ -4511,8 +4519,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
@@ -4929,7 +4936,8 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
@@ -5006,6 +5014,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5054,6 +5063,7 @@
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -5570,6 +5580,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5732,6 +5743,7 @@
"integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
@@ -8965,6 +8977,7 @@
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
@@ -10063,6 +10076,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -10183,6 +10197,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -10326,6 +10341,7 @@
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -10596,6 +10612,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -12022,7 +12039,8 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
- "license": "0BSD"
+ "license": "0BSD",
+ "peer": true
},
"node_modules/tuf-js": {
"version": "4.1.0",
@@ -12090,6 +12108,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -12392,6 +12411,7 @@
"integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
diff --git a/src/pages/plugins/plugins.js b/src/pages/plugins/plugins.js
index 92c5e433b..b10d4d19d 100644
--- a/src/pages/plugins/plugins.js
+++ b/src/pages/plugins/plugins.js
@@ -341,17 +341,21 @@ export default function PluginsInclude(updates) {
async function searchRemotely(query) {
if (!query) return [];
+ const encodedQuery = encodeURIComponent(query);
+ const searchUrl = withSupportedEditor(
+ `${constants.API_BASE}/plugins?name=${encodedQuery}`,
+ );
try {
- const response = await fetch(
- withSupportedEditor(`${constants.API_BASE}/plugins?name=${query}`),
- );
- const plugins = await response.json();
+ const plugins = await helpers.requestJson(searchUrl);
// Map the plugins to Item elements and return
return plugins.map((plugin) => );
} catch (error) {
$list.all.setAttribute("empty-msg", strings["error"]);
- window.log("error", "Failed to search remotely:");
- window.log("error", error);
+ window.log(
+ "error",
+ `Remote plugin search failed for query "${query}" at URL "${searchUrl}"`,
+ error,
+ );
return [];
}
}
@@ -421,21 +425,20 @@ export default function PluginsInclude(updates) {
if (filterState.type === "orderBy") {
const page = filterState.nextPage || 1;
try {
- let response;
+ let items;
if (filterState.value === "top_rated") {
- response = await fetch(
+ items = await helpers.requestJson(
withSupportedEditor(
`${constants.API_BASE}/plugins?explore=random&page=${page}&limit=${LIMIT}`,
),
);
} else {
- response = await fetch(
+ items = await helpers.requestJson(
withSupportedEditor(
`${constants.API_BASE}/plugin?orderBy=${filterState.value}&page=${page}&limit=${LIMIT}`,
),
);
}
- const items = await response.json();
if (!Array.isArray(items)) {
return { items: [], hasMore: false };
}
@@ -470,10 +473,9 @@ export default function PluginsInclude(updates) {
try {
const page = filterState.nextPage;
- const response = await fetch(
+ const data = await helpers.requestJson(
withSupportedEditor(`${constants.API_BASE}/plugins?page=${page}&limit=${LIMIT}`),
);
- const data = await response.json();
filterState.nextPage = page + 1;
if (!Array.isArray(data) || !data.length) {
@@ -567,10 +569,9 @@ export default function PluginsInclude(updates) {
$list.all.setAttribute("empty-msg", strings["loading..."]);
- const response = await fetch(
+ const newPlugins = await helpers.requestJson(
withSupportedEditor(`${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`),
);
- const newPlugins = await response.json();
if (newPlugins.length < LIMIT) {
hasMore = false;
diff --git a/src/sidebarApps/extensions/index.js b/src/sidebarApps/extensions/index.js
index 83545cdcf..a027d6efa 100644
--- a/src/sidebarApps/extensions/index.js
+++ b/src/sidebarApps/extensions/index.js
@@ -145,12 +145,11 @@ async function loadMorePlugins() {
isLoading = true;
startLoading($explore);
- const response = await fetch(
+ const newPlugins = await helpers.requestJson(
withSupportedEditor(
`${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`,
),
);
- const newPlugins = await response.json();
if (newPlugins.length < LIMIT) {
hasMore = false;
@@ -218,7 +217,7 @@ async function searchPlugin() {
$searchResult.onscroll = null;
$searchResult.content = "";
- const status = helpers.checkAPIStatus();
+ const status = await helpers.checkAPIStatus();
if (!status) {
$searchResult.content = (
{strings.api_error}
@@ -228,14 +227,15 @@ async function searchPlugin() {
const query = this.value;
if (!query) return;
+ const encodedQuery = encodeURIComponent(query);
try {
$searchResult.classList.add("loading");
- const plugins = await fsOperation(
+ const plugins = await helpers.requestJson(
withSupportedEditor(
- Url.join(constants.API_BASE, `plugins?name=${query}`),
+ Url.join(constants.API_BASE, `plugins?name=${encodedQuery}`),
),
- ).readFile("json");
+ );
installedPlugins = await listInstalledPlugins();
$searchResult.content = plugins.map(ListItem);
@@ -409,7 +409,7 @@ async function loadInstalled() {
async function loadExplore() {
if (this.collapsed) return;
- const status = helpers.checkAPIStatus();
+ const status = await helpers.checkAPIStatus();
if (!status) {
$explore.$ul.content = {strings.api_error};
return;
@@ -420,12 +420,11 @@ async function loadExplore() {
currentPage = 1;
hasMore = true;
- const response = await fetch(
+ const plugins = await helpers.requestJson(
withSupportedEditor(
`${constants.API_BASE}/plugins?page=${currentPage}&limit=${LIMIT}`,
),
);
- const plugins = await response.json();
if (plugins.length < LIMIT) {
hasMore = false;
@@ -463,21 +462,20 @@ async function getFilteredPlugins(filterState) {
if (filterState.type === "orderBy") {
const page = filterState.nextPage || 1;
try {
- let response;
+ let items;
if (filterState.value === "top_rated") {
- response = await fetch(
+ items = await helpers.requestJson(
withSupportedEditor(
`${constants.API_BASE}/plugins?explore=random&page=${page}&limit=${LIMIT}`,
),
);
} else {
- response = await fetch(
+ items = await helpers.requestJson(
withSupportedEditor(
`${constants.API_BASE}/plugin?orderBy=${filterState.value}&page=${page}&limit=${LIMIT}`,
),
);
}
- const items = await response.json();
if (!Array.isArray(items)) {
return { items: [], hasMore: false };
}
@@ -512,12 +510,11 @@ async function getFilteredPlugins(filterState) {
try {
const page = filterState.nextPage;
- const response = await fetch(
+ const data = await helpers.requestJson(
withSupportedEditor(
`${constants.API_BASE}/plugins?page=${page}&limit=${LIMIT}`,
),
);
- const data = await response.json();
filterState.nextPage = page + 1;
if (!Array.isArray(data) || !data.length) {
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index a925bfabf..1368c5859 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -344,9 +344,42 @@ export default {
func(...args, resolve, reject);
});
},
+ // Plugin API requests are all remote HTTPS calls. For this workload, the
+ // JS-to-native bridge cost is negligible compared to network latency, so we
+ // use native HTTP unconditionally instead of keeping a fetch fast path.
+ // This also avoids the WebView CORS mismatch that caused false
+ // "API unavailable" failures against the plugin API.
+ async requestJson(url, headers = {}) {
+ const nativeHttp = window.cordova?.plugin?.http;
+ if (typeof nativeHttp?.get !== "function") {
+ // Do not fall back to fetch here: these callers exist specifically to
+ // bypass the Android WebView CORS mismatch for the plugin API. Missing
+ // native HTTP support is a runtime/configuration error, not a case for
+ // silently re-enabling the broken transport path.
+ throw new Error("Native HTTP plugin is unavailable");
+ }
+
+ const response = await new Promise((resolve, reject) => {
+ nativeHttp.get(url, {}, headers, resolve, reject);
+ });
+
+ if (typeof response?.data === "string") {
+ try {
+ return JSON.parse(response.data);
+ } catch (error) {
+ const bodyPreview = response.data.slice(0, 200);
+ throw new Error(
+ `Failed to parse JSON response from ${url}: ${bodyPreview}`,
+ );
+ }
+ }
+
+ return response?.data;
+ },
async checkAPIStatus() {
+ const statusUrl = Url.join(constants.API_BASE, "status");
try {
- const { status } = await ajax.get(Url.join(constants.API_BASE, "status"));
+ const { status } = await this.requestJson(statusUrl);
return status === "ok";
} catch (error) {
window.log("error", error);