@capgo/capacitor-updater 8.47.2 → 8.47.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +200 -42
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +36 -14
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +21 -2
- package/dist/docs.json +16 -0
- package/dist/esm/definitions.d.ts +9 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +213 -44
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +59 -17
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2200,9 +2200,10 @@ If you don't use backend, you need to provide the URL and version of the bundle.
|
|
|
2200
2200
|
|
|
2201
2201
|
##### StartPreviewSessionOptions
|
|
2202
2202
|
|
|
2203
|
-
| Prop
|
|
2204
|
-
|
|
|
2205
|
-
| **`appId`**
|
|
2203
|
+
| Prop | Type | Description | Default | Since |
|
|
2204
|
+
| ---------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ------ |
|
|
2205
|
+
| **`appId`** | <code>string</code> | App id to use while the preview session is active. The previous app id is restored when leaving the preview session. Requires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`. | <code>undefined</code> | 8.47.0 |
|
|
2206
|
+
| **`payloadUrl`** | <code>string</code> | HTTP(S) URL returning a preview download payload. When provided, the native shake reload action fetches this payload again before reloading so channel previews can move to the latest bundle. Requires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`. | <code>undefined</code> | 8.48.0 |
|
|
2206
2207
|
|
|
2207
2208
|
|
|
2208
2209
|
##### BundleListResult
|
|
@@ -49,9 +49,13 @@ import com.google.android.play.core.install.model.AppUpdateType;
|
|
|
49
49
|
import com.google.android.play.core.install.model.InstallStatus;
|
|
50
50
|
import com.google.android.play.core.install.model.UpdateAvailability;
|
|
51
51
|
import io.github.g00fy2.versioncompare.Version;
|
|
52
|
+
import java.io.ByteArrayOutputStream;
|
|
52
53
|
import java.io.IOException;
|
|
54
|
+
import java.io.InputStream;
|
|
55
|
+
import java.net.HttpURLConnection;
|
|
53
56
|
import java.net.MalformedURLException;
|
|
54
57
|
import java.net.URL;
|
|
58
|
+
import java.nio.charset.StandardCharsets;
|
|
55
59
|
import java.util.ArrayList;
|
|
56
60
|
import java.util.Date;
|
|
57
61
|
import java.util.HashMap;
|
|
@@ -98,7 +102,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
98
102
|
private static final String PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY = "CapacitorUpdater.previewPreviousShakeChannelSelector";
|
|
99
103
|
private static final String PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY = "CapacitorUpdater.previewPreviousNextBundle";
|
|
100
104
|
private static final String PREVIEW_PREVIOUS_APP_ID_PREF_KEY = "CapacitorUpdater.previewPreviousAppId";
|
|
105
|
+
private static final String PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.previewPreviousDefaultChannel";
|
|
106
|
+
private static final String PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY = "CapacitorUpdater.previewPreviousDefaultChannelWasSet";
|
|
101
107
|
private static final String PREVIEW_APP_ID_PREF_KEY = "CapacitorUpdater.previewAppId";
|
|
108
|
+
private static final String PREVIEW_PAYLOAD_URL_PREF_KEY = "CapacitorUpdater.previewPayloadUrl";
|
|
102
109
|
private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
|
|
103
110
|
private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
|
|
104
111
|
private static final String LAST_REPORTED_APP_EXIT_TIMESTAMP_PREF_KEY = "CapacitorUpdater.lastReportedAppExitTimestamp";
|
|
@@ -120,7 +127,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
120
127
|
static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
|
|
121
128
|
static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
|
|
122
129
|
|
|
123
|
-
private final String pluginVersion = "8.47.
|
|
130
|
+
private final String pluginVersion = "8.47.4";
|
|
124
131
|
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
125
132
|
|
|
126
133
|
private SharedPreferences.Editor editor;
|
|
@@ -1930,6 +1937,20 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1930
1937
|
}
|
|
1931
1938
|
}
|
|
1932
1939
|
|
|
1940
|
+
private BundleInfo downloadBundle(
|
|
1941
|
+
final String url,
|
|
1942
|
+
final String version,
|
|
1943
|
+
final String sessionKey,
|
|
1944
|
+
final String checksum,
|
|
1945
|
+
final JSONArray manifest
|
|
1946
|
+
) throws IOException {
|
|
1947
|
+
if (manifest != null) {
|
|
1948
|
+
return this.implementation.downloadManifest(url, version, sessionKey, checksum, manifest);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
return this.implementation.download(url, version, sessionKey, checksum);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1933
1954
|
@PluginMethod
|
|
1934
1955
|
public void download(final PluginCall call) {
|
|
1935
1956
|
final String url = call.getString("url");
|
|
@@ -1951,20 +1972,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
1951
1972
|
logger.info("Downloading " + url);
|
|
1952
1973
|
startNewThread(() -> {
|
|
1953
1974
|
try {
|
|
1954
|
-
final BundleInfo downloaded;
|
|
1955
|
-
if (manifest != null) {
|
|
1956
|
-
// For manifest downloads, we need to handle this asynchronously
|
|
1957
|
-
// to avoid automatically scheduling/applying the downloaded bundle.
|
|
1958
|
-
// Manual download must not schedule/apply the bundle automatically.
|
|
1959
|
-
CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest, false);
|
|
1960
|
-
// Return immediately with a pending status - the actual result will come via listeners
|
|
1961
|
-
final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
|
|
1962
|
-
downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
|
|
1963
|
-
call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
|
|
1964
|
-
return;
|
|
1965
|
-
} else {
|
|
1966
|
-
downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
|
|
1967
|
-
}
|
|
1975
|
+
final BundleInfo downloaded = this.downloadBundle(url, version, sessionKey, checksum, manifest);
|
|
1968
1976
|
if (downloaded.isErrorStatus()) {
|
|
1969
1977
|
throw new RuntimeException("Download failed: " + downloaded.getStatus());
|
|
1970
1978
|
} else {
|
|
@@ -2231,6 +2239,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2231
2239
|
return;
|
|
2232
2240
|
}
|
|
2233
2241
|
final String previewAppId = this.normalizePreviewAppId(call.getString("appId"));
|
|
2242
|
+
final String rawPayloadUrl = call.getString("payloadUrl");
|
|
2243
|
+
final String previewPayloadUrl = this.normalizePreviewPayloadUrl(rawPayloadUrl);
|
|
2244
|
+
if (this.hasPreviewPayloadUrl(rawPayloadUrl) && previewPayloadUrl == null) {
|
|
2245
|
+
logger.error("startPreviewSession called with invalid payloadUrl");
|
|
2246
|
+
call.reject("Invalid preview payloadUrl");
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2234
2249
|
startNewThread(() -> {
|
|
2235
2250
|
try {
|
|
2236
2251
|
if (!Boolean.TRUE.equals(this.previewSessionEnabled)) {
|
|
@@ -2249,6 +2264,16 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2249
2264
|
}
|
|
2250
2265
|
|
|
2251
2266
|
this.editor.putString(PREVIEW_PREVIOUS_APP_ID_PREF_KEY, this.implementation.appId);
|
|
2267
|
+
if (this.prefs.contains(DEFAULT_CHANNEL_PREF_KEY)) {
|
|
2268
|
+
this.editor.putString(
|
|
2269
|
+
PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY,
|
|
2270
|
+
this.prefs.getString(DEFAULT_CHANNEL_PREF_KEY, "")
|
|
2271
|
+
);
|
|
2272
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, true);
|
|
2273
|
+
} else {
|
|
2274
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
|
|
2275
|
+
this.editor.putBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, false);
|
|
2276
|
+
}
|
|
2252
2277
|
this.editor.putBoolean(PREVIEW_PREVIOUS_SHAKE_MENU_PREF_KEY, Boolean.TRUE.equals(this.shakeMenuEnabled));
|
|
2253
2278
|
this.editor.putBoolean(
|
|
2254
2279
|
PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY,
|
|
@@ -2263,6 +2288,13 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2263
2288
|
logger.info("Preview session using appId: " + previewAppId);
|
|
2264
2289
|
}
|
|
2265
2290
|
|
|
2291
|
+
if (previewPayloadUrl != null) {
|
|
2292
|
+
this.editor.putString(PREVIEW_PAYLOAD_URL_PREF_KEY, previewPayloadUrl);
|
|
2293
|
+
logger.info("Preview session using payload URL");
|
|
2294
|
+
} else {
|
|
2295
|
+
this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2266
2298
|
this.previewSessionEnabled = true;
|
|
2267
2299
|
this.previewSessionAlertPending = true;
|
|
2268
2300
|
this.implementation.previewSession = true;
|
|
@@ -2287,9 +2319,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2287
2319
|
return false;
|
|
2288
2320
|
}
|
|
2289
2321
|
|
|
2290
|
-
if (!this.clearPreviewChannelOverride()) {
|
|
2291
|
-
return false;
|
|
2292
|
-
}
|
|
2293
2322
|
final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
|
|
2294
2323
|
this.endPreviewSession();
|
|
2295
2324
|
final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
|
|
@@ -2308,6 +2337,11 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2308
2337
|
}
|
|
2309
2338
|
|
|
2310
2339
|
public boolean reloadPreviewSessionFromShakeMenu() {
|
|
2340
|
+
final String payloadUrl = this.storedPreviewPayloadUrl();
|
|
2341
|
+
if (payloadUrl != null) {
|
|
2342
|
+
return this.refreshPreviewSessionFromPayloadUrl(payloadUrl);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2311
2345
|
return this._reload();
|
|
2312
2346
|
}
|
|
2313
2347
|
|
|
@@ -2350,6 +2384,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2350
2384
|
);
|
|
2351
2385
|
this.restorePreviewPreviousNextBundle();
|
|
2352
2386
|
this.restorePreviewPreviousAppId();
|
|
2387
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
2353
2388
|
|
|
2354
2389
|
this.previewSessionEnabled = false;
|
|
2355
2390
|
this.previewSessionAlertPending = false;
|
|
@@ -2376,6 +2411,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2376
2411
|
|
|
2377
2412
|
this.restorePreviewPreviousNextBundle();
|
|
2378
2413
|
this.restorePreviewPreviousAppId();
|
|
2414
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
2379
2415
|
this.previewSessionEnabled = false;
|
|
2380
2416
|
this.previewSessionAlertPending = false;
|
|
2381
2417
|
this.implementation.previewSession = false;
|
|
@@ -2393,7 +2429,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2393
2429
|
this.editor.remove(PREVIEW_PREVIOUS_SHAKE_CHANNEL_SELECTOR_PREF_KEY);
|
|
2394
2430
|
this.editor.remove(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY);
|
|
2395
2431
|
this.editor.remove(PREVIEW_PREVIOUS_APP_ID_PREF_KEY);
|
|
2432
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY);
|
|
2433
|
+
this.editor.remove(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY);
|
|
2396
2434
|
this.editor.remove(PREVIEW_APP_ID_PREF_KEY);
|
|
2435
|
+
this.editor.remove(PREVIEW_PAYLOAD_URL_PREF_KEY);
|
|
2397
2436
|
this.editor.apply();
|
|
2398
2437
|
}
|
|
2399
2438
|
|
|
@@ -2413,6 +2452,23 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2413
2452
|
logger.info("Restored appId after preview: " + previousAppId);
|
|
2414
2453
|
}
|
|
2415
2454
|
|
|
2455
|
+
private void restorePreviewPreviousDefaultChannel() {
|
|
2456
|
+
final String configDefaultChannel = this.getConfig().getString("defaultChannel", "");
|
|
2457
|
+
if (this.prefs.getBoolean(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_WAS_SET_PREF_KEY, false)) {
|
|
2458
|
+
final String previousDefaultChannel = this.prefs.getString(PREVIEW_PREVIOUS_DEFAULT_CHANNEL_PREF_KEY, "");
|
|
2459
|
+
this.editor.putString(DEFAULT_CHANNEL_PREF_KEY, previousDefaultChannel);
|
|
2460
|
+
this.implementation.defaultChannel = previousDefaultChannel;
|
|
2461
|
+
this.editor.apply();
|
|
2462
|
+
logger.info("Restored defaultChannel after preview");
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
this.editor.remove(DEFAULT_CHANNEL_PREF_KEY);
|
|
2467
|
+
this.implementation.defaultChannel = configDefaultChannel;
|
|
2468
|
+
this.editor.apply();
|
|
2469
|
+
logger.info("Restored defaultChannel after preview to config value");
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2416
2472
|
private String normalizePreviewAppId(final String rawAppId) {
|
|
2417
2473
|
if (rawAppId == null) {
|
|
2418
2474
|
return null;
|
|
@@ -2431,6 +2487,131 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2431
2487
|
return appId;
|
|
2432
2488
|
}
|
|
2433
2489
|
|
|
2490
|
+
private boolean hasPreviewPayloadUrl(final String rawPayloadUrl) {
|
|
2491
|
+
if (rawPayloadUrl == null) {
|
|
2492
|
+
return false;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
final String payloadUrl = rawPayloadUrl.trim();
|
|
2496
|
+
if (payloadUrl.isEmpty()) {
|
|
2497
|
+
return false;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
final String lowercasedPayloadUrl = payloadUrl.toLowerCase(java.util.Locale.ROOT);
|
|
2501
|
+
return !"undefined".equals(lowercasedPayloadUrl) && !"null".equals(lowercasedPayloadUrl);
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
private String normalizePreviewPayloadUrl(final String rawPayloadUrl) {
|
|
2505
|
+
if (!this.hasPreviewPayloadUrl(rawPayloadUrl)) {
|
|
2506
|
+
return null;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
final String payloadUrl = rawPayloadUrl.trim();
|
|
2510
|
+
try {
|
|
2511
|
+
final URL parsedUrl = new URL(payloadUrl);
|
|
2512
|
+
final String protocol = parsedUrl.getProtocol();
|
|
2513
|
+
if (!"https".equals(protocol) && !"http".equals(protocol)) {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
return parsedUrl.toString();
|
|
2517
|
+
} catch (final MalformedURLException ignored) {
|
|
2518
|
+
return null;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
private String storedPreviewPayloadUrl() {
|
|
2523
|
+
return this.normalizePreviewPayloadUrl(this.prefs.getString(PREVIEW_PAYLOAD_URL_PREF_KEY, null));
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
private String readResponseBody(final InputStream stream) throws IOException {
|
|
2527
|
+
if (stream == null) {
|
|
2528
|
+
return "";
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
try (InputStream input = stream; ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
|
2532
|
+
final byte[] buffer = new byte[8192];
|
|
2533
|
+
int read;
|
|
2534
|
+
while ((read = input.read(buffer)) != -1) {
|
|
2535
|
+
output.write(buffer, 0, read);
|
|
2536
|
+
}
|
|
2537
|
+
return output.toString(StandardCharsets.UTF_8.name());
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
private JSONObject fetchPreviewPayload(final String payloadUrl) throws IOException, JSONException {
|
|
2542
|
+
final HttpURLConnection connection = (HttpURLConnection) new URL(payloadUrl).openConnection();
|
|
2543
|
+
connection.setRequestMethod("GET");
|
|
2544
|
+
connection.setRequestProperty("Accept", "application/json");
|
|
2545
|
+
connection.setConnectTimeout(30000);
|
|
2546
|
+
connection.setReadTimeout(60000);
|
|
2547
|
+
|
|
2548
|
+
try {
|
|
2549
|
+
final int statusCode = connection.getResponseCode();
|
|
2550
|
+
final String body = this.readResponseBody(
|
|
2551
|
+
statusCode >= 200 && statusCode < 300 ? connection.getInputStream() : connection.getErrorStream()
|
|
2552
|
+
);
|
|
2553
|
+
final JSONObject payload = new JSONObject(body);
|
|
2554
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
2555
|
+
throw new IOException(
|
|
2556
|
+
payload.optString("message", payload.optString("error", "Preview payload request failed with HTTP " + statusCode))
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
return payload;
|
|
2560
|
+
} finally {
|
|
2561
|
+
connection.disconnect();
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
private BundleInfo downloadPreviewPayloadBundle(final JSONObject payload) throws IOException, JSONException {
|
|
2566
|
+
final String version = payload.optString("version", "").trim();
|
|
2567
|
+
if (version.isEmpty()) {
|
|
2568
|
+
throw new IOException("Preview payload is missing a version");
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
final JSONArray manifest = payload.optJSONArray("manifest");
|
|
2572
|
+
final String url = payload.optString("url", "");
|
|
2573
|
+
if ((url == null || url.isEmpty()) && (manifest == null || manifest.length() == 0)) {
|
|
2574
|
+
throw new IOException("Preview payload is missing download information");
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
return this.downloadBundle(
|
|
2578
|
+
url == null || url.isEmpty() ? "https://404.capgo.app/no.zip" : url,
|
|
2579
|
+
version,
|
|
2580
|
+
payload.optString("sessionKey", ""),
|
|
2581
|
+
payload.optString("checksum", ""),
|
|
2582
|
+
manifest
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
private boolean refreshPreviewSessionFromPayloadUrl(final String payloadUrl) {
|
|
2587
|
+
try {
|
|
2588
|
+
final JSONObject payload = this.fetchPreviewPayload(payloadUrl);
|
|
2589
|
+
final String version = payload.optString("version", "").trim();
|
|
2590
|
+
if (version.isEmpty()) {
|
|
2591
|
+
throw new IOException("Preview payload is missing a version");
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
if (version.equals(this.implementation.getCurrentBundle().getVersionName())) {
|
|
2595
|
+
logger.info("Preview payload unchanged, reloading current bundle");
|
|
2596
|
+
return this._reload();
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
final BundleInfo next = this.downloadPreviewPayloadBundle(payload);
|
|
2600
|
+
if (next.isErrorStatus()) {
|
|
2601
|
+
throw new IOException("Download failed: " + next.getStatus());
|
|
2602
|
+
}
|
|
2603
|
+
if (!this.implementation.set(next.getId())) {
|
|
2604
|
+
throw new IOException("Downloaded preview bundle cannot be applied");
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
this.notifyBundleSet(next);
|
|
2608
|
+
return this._reload();
|
|
2609
|
+
} catch (final Exception err) {
|
|
2610
|
+
logger.error("Could not refresh preview session: " + err.getMessage());
|
|
2611
|
+
return false;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2434
2615
|
private void clearPreviewSessionForNativeBuildChange() {
|
|
2435
2616
|
if (!Boolean.TRUE.equals(this.previewSessionEnabled) && this.implementation.getPreviewFallbackBundle() == null) {
|
|
2436
2617
|
return;
|
|
@@ -2442,35 +2623,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
|
|
|
2442
2623
|
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
2443
2624
|
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
2444
2625
|
this.restorePreviewPreviousAppId();
|
|
2626
|
+
this.restorePreviewPreviousDefaultChannel();
|
|
2445
2627
|
this.implementation.setPreviewFallbackBundle(null);
|
|
2446
2628
|
this.implementation.setNextBundle(null);
|
|
2447
|
-
this.clearPreviewChannelOverride();
|
|
2448
2629
|
this.clearPreviewSessionPreferences();
|
|
2449
2630
|
}
|
|
2450
2631
|
|
|
2451
|
-
private boolean clearPreviewChannelOverride() {
|
|
2452
|
-
final String configDefaultChannel = this.getConfig().getString("defaultChannel", "");
|
|
2453
|
-
final AtomicReference<Map<String, Object>> unsetChannelResult = new AtomicReference<>();
|
|
2454
|
-
try {
|
|
2455
|
-
this.implementation.unsetChannel(this.editor, DEFAULT_CHANNEL_PREF_KEY, configDefaultChannel, unsetChannelResult::set);
|
|
2456
|
-
} catch (final Exception err) {
|
|
2457
|
-
logger.error("Could not clear preview channel override: " + err.getMessage());
|
|
2458
|
-
return false;
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
final Map<String, Object> result = unsetChannelResult.get();
|
|
2462
|
-
if (result == null) {
|
|
2463
|
-
logger.error("Could not clear preview channel override: no result");
|
|
2464
|
-
return false;
|
|
2465
|
-
}
|
|
2466
|
-
if (result.containsKey("error")) {
|
|
2467
|
-
final Object message = result.getOrDefault("message", result.get("error"));
|
|
2468
|
-
logger.error("Could not clear preview channel override: " + message);
|
|
2469
|
-
return false;
|
|
2470
|
-
}
|
|
2471
|
-
return true;
|
|
2472
|
-
}
|
|
2473
|
-
|
|
2474
2632
|
private void restorePreviewPreviousNextBundle() {
|
|
2475
2633
|
final String previousNextBundleId = this.prefs.getString(PREVIEW_PREVIOUS_NEXT_BUNDLE_PREF_KEY, null);
|
|
2476
2634
|
if (previousNextBundleId == null || previousNextBundleId.isEmpty()) {
|
|
@@ -193,6 +193,30 @@ public class CapgoUpdater {
|
|
|
193
193
|
this.cachedKeyId = CryptoCipher.calcKeyId(publicKey);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
static File resolvePathInsideDirectory(final File baseDirectory, final String relativePath) throws IOException {
|
|
197
|
+
if (relativePath == null || relativePath.isEmpty()) {
|
|
198
|
+
throw new IOException("Invalid empty path");
|
|
199
|
+
}
|
|
200
|
+
if (relativePath.contains("\\") || relativePath.indexOf('\0') >= 0) {
|
|
201
|
+
throw new IOException("Invalid path separator");
|
|
202
|
+
}
|
|
203
|
+
if (new File(relativePath).isAbsolute()) {
|
|
204
|
+
throw new IOException("Absolute paths are not allowed");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
final File canonicalBase = baseDirectory.getCanonicalFile();
|
|
208
|
+
final File canonicalTarget = new File(canonicalBase, relativePath).getCanonicalFile();
|
|
209
|
+
final String basePath = canonicalBase.getPath();
|
|
210
|
+
final String targetPath = canonicalTarget.getPath();
|
|
211
|
+
final String normalizedBasePath = basePath.endsWith(File.separator) ? basePath : basePath + File.separator;
|
|
212
|
+
|
|
213
|
+
if (!targetPath.equals(basePath) && !targetPath.startsWith(normalizedBasePath)) {
|
|
214
|
+
throw new IOException("Path escapes base directory: " + relativePath);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return canonicalTarget;
|
|
218
|
+
}
|
|
219
|
+
|
|
196
220
|
public String getKeyId() {
|
|
197
221
|
return this.cachedKeyId;
|
|
198
222
|
}
|
|
@@ -213,23 +237,21 @@ public class CapgoUpdater {
|
|
|
213
237
|
|
|
214
238
|
ZipEntry entry;
|
|
215
239
|
while ((entry = zis.getNextEntry()) != null) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
240
|
+
final File file;
|
|
241
|
+
try {
|
|
242
|
+
file = resolvePathInsideDirectory(targetDirectory, entry.getName());
|
|
243
|
+
} catch (IOException e) {
|
|
244
|
+
if (entry.getName().contains("\\")) {
|
|
245
|
+
logger.error("Unzip failed: Windows path not supported");
|
|
246
|
+
logger.debug("Invalid path: " + entry.getName());
|
|
247
|
+
this.sendStats("windows_path_fail");
|
|
248
|
+
} else {
|
|
249
|
+
this.sendStats("canonical_path_fail");
|
|
250
|
+
}
|
|
251
|
+
throw e;
|
|
220
252
|
}
|
|
221
|
-
final File file = new File(targetDirectory, entry.getName());
|
|
222
|
-
final String canonicalPath = file.getCanonicalPath();
|
|
223
|
-
final String canonicalDir = targetDirectory.getCanonicalPath();
|
|
224
253
|
final File dir = entry.isDirectory() ? file : file.getParentFile();
|
|
225
254
|
|
|
226
|
-
if (!canonicalPath.startsWith(canonicalDir)) {
|
|
227
|
-
this.sendStats("canonical_path_fail");
|
|
228
|
-
throw new FileNotFoundException(
|
|
229
|
-
"SecurityException, Failed to ensure directory is the start path : " + canonicalDir + " of " + canonicalPath
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
255
|
assert dir != null;
|
|
234
256
|
if (!dir.isDirectory() && !dir.mkdirs()) {
|
|
235
257
|
this.sendStats("directory_path_fail");
|
|
@@ -168,6 +168,16 @@ public class DownloadService extends Worker {
|
|
|
168
168
|
return Result.success(output);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
static File resolveManifestTargetFile(final File destFolder, final String fileName) throws IOException {
|
|
172
|
+
final boolean isBrotli = fileName.endsWith(".br");
|
|
173
|
+
final String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
|
|
174
|
+
return CapgoUpdater.resolvePathInsideDirectory(destFolder, targetFileName);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static File resolveManifestBuiltinFile(final File builtinFolder, final String fileName) throws IOException {
|
|
178
|
+
return CapgoUpdater.resolvePathInsideDirectory(builtinFolder, fileName);
|
|
179
|
+
}
|
|
180
|
+
|
|
171
181
|
private String getInputString(String key, String fallback) {
|
|
172
182
|
String value = getInputData().getString(key);
|
|
173
183
|
return value != null ? value : fallback;
|
|
@@ -338,11 +348,20 @@ public class DownloadService extends Worker {
|
|
|
338
348
|
boolean isBrotli = fileName.endsWith(".br");
|
|
339
349
|
String targetFileName = isBrotli ? fileName.substring(0, fileName.length() - 3) : fileName;
|
|
340
350
|
|
|
341
|
-
File targetFile
|
|
351
|
+
File targetFile;
|
|
352
|
+
File builtinFile;
|
|
353
|
+
try {
|
|
354
|
+
targetFile = resolveManifestTargetFile(destFolder, fileName);
|
|
355
|
+
builtinFile = resolveManifestBuiltinFile(builtinFolder, fileName);
|
|
356
|
+
} catch (IOException e) {
|
|
357
|
+
logger.error("Invalid manifest file path: " + fileName);
|
|
358
|
+
sendStatsAsync("manifest_path_fail", version + ":" + fileName);
|
|
359
|
+
hasError.set(true);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
342
362
|
String cacheBaseName = new File(isBrotli ? targetFileName : fileName).getName();
|
|
343
363
|
File cacheFile = new File(cacheFolder, finalFileHash + "_" + cacheBaseName);
|
|
344
364
|
final File legacyCacheFile = isBrotli ? new File(cacheFolder, finalFileHash + "_" + new File(fileName).getName()) : null;
|
|
345
|
-
File builtinFile = new File(builtinFolder, fileName);
|
|
346
365
|
|
|
347
366
|
// Ensure parent directories of the target file exist
|
|
348
367
|
if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
|
package/dist/docs.json
CHANGED
|
@@ -1991,6 +1991,22 @@
|
|
|
1991
1991
|
"docs": "App id to use while the preview session is active.\nThe previous app id is restored when leaving the preview session.\nRequires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`.",
|
|
1992
1992
|
"complexTypes": [],
|
|
1993
1993
|
"type": "string | undefined"
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
"name": "payloadUrl",
|
|
1997
|
+
"tags": [
|
|
1998
|
+
{
|
|
1999
|
+
"text": "8.48.0",
|
|
2000
|
+
"name": "since"
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
"text": "undefined",
|
|
2004
|
+
"name": "default"
|
|
2005
|
+
}
|
|
2006
|
+
],
|
|
2007
|
+
"docs": "HTTP(S) URL returning a preview download payload.\nWhen provided, the native shake reload action fetches this payload again\nbefore reloading so channel previews can move to the latest bundle.\nRequires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`.",
|
|
2008
|
+
"complexTypes": [],
|
|
2009
|
+
"type": "string | undefined"
|
|
1994
2010
|
}
|
|
1995
2011
|
]
|
|
1996
2012
|
},
|
|
@@ -1973,6 +1973,15 @@ export interface StartPreviewSessionOptions {
|
|
|
1973
1973
|
* @default undefined
|
|
1974
1974
|
*/
|
|
1975
1975
|
appId?: string;
|
|
1976
|
+
/**
|
|
1977
|
+
* HTTP(S) URL returning a preview download payload.
|
|
1978
|
+
* When provided, the native shake reload action fetches this payload again
|
|
1979
|
+
* before reloading so channel previews can move to the latest bundle.
|
|
1980
|
+
* Requires {@link PluginsConfig.CapacitorUpdater.allowPreview} to be `true`.
|
|
1981
|
+
* @since 8.48.0
|
|
1982
|
+
* @default undefined
|
|
1983
|
+
*/
|
|
1984
|
+
payloadUrl?: string;
|
|
1976
1985
|
}
|
|
1977
1986
|
export interface AppReadyResult {
|
|
1978
1987
|
bundle: BundleInfo;
|