@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 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 | 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 |
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.2";
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
- if (entry.getName().contains("\\")) {
217
- logger.error("Unzip failed: Windows path not supported");
218
- logger.debug("Invalid path: " + entry.getName());
219
- this.sendStats("windows_path_fail");
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 = new File(destFolder, targetFileName);
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;