@capgo/cordova-updater 8.1.3 → 8.2.0

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.
Files changed (36) hide show
  1. package/dist/esm/definitions.d.ts +116 -86
  2. package/dist/esm/definitions.js +2 -2
  3. package/dist/esm/definitions.js.map +1 -1
  4. package/dist/esm/exec.js +11 -1
  5. package/dist/esm/exec.js.map +1 -1
  6. package/dist/esm/history.js +7 -7
  7. package/dist/esm/history.js.map +1 -1
  8. package/dist/plugin.cjs.js +13 -3
  9. package/dist/plugin.cjs.js.map +1 -1
  10. package/dist/plugin.js +13 -3
  11. package/dist/plugin.js.map +1 -1
  12. package/package.json +5 -3
  13. package/plugin.xml +9 -9
  14. package/src/android/CordovaUpdaterPlugin.java +173 -5
  15. package/src/android/app/capgo/cordova/updater/CapgoUpdater.java +313 -55
  16. package/src/android/app/capgo/cordova/updater/CryptoCipher.java +51 -18
  17. package/src/android/app/capgo/cordova/updater/DelayUpdateUtils.java +0 -1
  18. package/src/android/app/capgo/cordova/updater/DownloadService.java +404 -91
  19. package/src/android/app/capgo/cordova/updater/Version.java +312 -0
  20. package/src/android/capgo-cordova-updater.gradle +0 -2
  21. package/src/android/test/app/capgo/cordova/updater/BackgroundDownloadSettlementTest.java +29 -0
  22. package/src/android/test/app/capgo/cordova/updater/CapacitorUpdaterUnitTest.java.skip +2 -12
  23. package/src/android/test/app/capgo/cordova/updater/DelayUpdateUtilsTest.java +0 -1
  24. package/src/android/test/app/capgo/cordova/updater/RsaContractTest.java +137 -0
  25. package/src/android/test/app/capgo/cordova/updater/SecurityHardeningTest.java +197 -0
  26. package/src/android/test/app/capgo/cordova/updater/SessionKeyRequiredTest.java +344 -0
  27. package/src/ios/CapgoSemanticVersion.swift +181 -0
  28. package/src/ios/CapgoUpdater.swift +427 -114
  29. package/src/ios/CordovaUpdaterPlugin.swift +171 -40
  30. package/src/ios/CryptoCipher.swift +41 -38
  31. package/src/ios/DelayUpdateUtils.swift +19 -45
  32. package/src/ios/Logger.swift +81 -5
  33. package/src/ios/RedirectPolicyDelegate.swift +53 -0
  34. package/src/ios/WebViewStatsReporter.swift +5 -0
  35. package/src/ios/ZipArchiveReader.swift +263 -0
  36. package/src/ios/ZipCentralDirectory.swift +283 -0
@@ -36,7 +36,6 @@ import com.google.android.play.core.install.InstallStateUpdatedListener;
36
36
  import com.google.android.play.core.install.model.AppUpdateType;
37
37
  import com.google.android.play.core.install.model.InstallStatus;
38
38
  import com.google.android.play.core.install.model.UpdateAvailability;
39
- import io.github.g00fy2.versioncompare.Version;
40
39
  import java.io.ByteArrayOutputStream;
41
40
  import java.io.IOException;
42
41
  import java.io.InputStream;
@@ -171,7 +170,7 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
171
170
  static final int APPLICATION_EXIT_REASON_USER_REQUESTED = 10;
172
171
  static final int APPLICATION_EXIT_REASON_DEPENDENCY_DIED = 12;
173
172
 
174
- private final String pluginVersion = "8.1.3";
173
+ private final String pluginVersion = "8.2.0";
175
174
  private static final String DELAY_CONDITION_PREFERENCES = "";
176
175
 
177
176
  private SharedPreferences.Editor editor;
@@ -245,6 +244,11 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
245
244
  private Boolean isPreviousMainActivity = true;
246
245
 
247
246
  private volatile Thread backgroundDownloadTask;
247
+ private volatile long activeBackgroundDownloadGeneration = 0L;
248
+ private final Object readyGuardLock = new Object();
249
+ private Object readyGenerationScriptHandle;
250
+ private volatile int readyGeneration = 0;
251
+ private volatile boolean readyGuardArmed = false;
248
252
  private volatile Thread appReadyCheck;
249
253
  private volatile long downloadStartTimeMs = 0;
250
254
  private static final long DOWNLOAD_TIMEOUT_MS = 600000; // 10 minute timeout
@@ -711,6 +715,11 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
711
715
  CordovaUpdaterPlugin.this.scheduleDirectUpdateFinish(latest);
712
716
  }
713
717
 
718
+ @Override
719
+ public void backgroundDownloadSettled(final BundleInfo bundle, final String status, final long settlementToken) {
720
+ CordovaUpdaterPlugin.this.emitLaunchDownloadReady(bundle, status, settlementToken);
721
+ }
722
+
714
723
  @Override
715
724
  public void notifyListeners(final String id, final Map<String, Object> res) {
716
725
  if (activity != null) {
@@ -753,6 +762,7 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
753
762
  CryptoCipher.setLogger(logger);
754
763
  DownloadService.setLogger(logger);
755
764
  DownloadWorkerManager.setLogger(logger);
765
+ DownloadService.setAllowHttpsToHttpRedirect(this.updaterConfig.getBoolean("allowHttpsToHttpRedirect", false));
756
766
 
757
767
  this.implementation.appId = InternalUtils.getPackageName(getContext().getPackageManager(), getContext().getPackageName());
758
768
  this.implementation.appId = updaterConfig.getString("appId", this.implementation.appId);
@@ -2002,6 +2012,32 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
2002
2012
  this.logger = logger;
2003
2013
  }
2004
2014
 
2015
+ static boolean shouldAcceptBackgroundDownloadSettlement(final long settlementToken, final long activeGeneration) {
2016
+ return settlementToken > 0L && settlementToken == activeGeneration;
2017
+ }
2018
+
2019
+ void emitLaunchDownloadReady(final BundleInfo bundle, final String status, final long settlementToken) {
2020
+ if (!shouldAcceptBackgroundDownloadSettlement(settlementToken, this.activeBackgroundDownloadGeneration)) {
2021
+ logger.info("Ignoring stale background download settlement");
2022
+ return;
2023
+ }
2024
+ final BundleInfo readyBundle = bundle != null ? bundle : this.implementation.getCurrentBundle();
2025
+ if (readyBundle == null) {
2026
+ return;
2027
+ }
2028
+ this.endBackGroundTaskWithNotif(
2029
+ status,
2030
+ readyBundle.getVersionName(),
2031
+ readyBundle,
2032
+ false,
2033
+ false,
2034
+ "download_fail",
2035
+ "downloadFailed",
2036
+ false,
2037
+ false
2038
+ );
2039
+ }
2040
+
2005
2041
  void completeBackgroundTaskForTesting(final BundleInfo current, final boolean plannedDirectUpdate) {
2006
2042
  this.endBackGroundTaskWithNotif("test", current.getVersionName(), current, false, plannedDirectUpdate);
2007
2043
  }
@@ -2373,6 +2409,7 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
2373
2409
  CordovaUpdaterPlugin.this.editor,
2374
2410
  DEFAULT_CHANNEL_PREF_KEY,
2375
2411
  configDefaultChannel,
2412
+ CordovaUpdaterPlugin.this.allowSetDefaultChannel,
2376
2413
  (res) -> {
2377
2414
  JSObject jsRes = InternalUtils.mapToJSObject(res);
2378
2415
  if (jsRes.has("error")) {
@@ -2546,6 +2583,20 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
2546
2583
  final String checksum,
2547
2584
  final JSONArray manifest
2548
2585
  ) throws IOException {
2586
+ if (!this.implementation.publicKey.isEmpty() && !CryptoCipher.isValidSessionKey(sessionKey)) {
2587
+ logger.error("Public key present but no valid session key provided");
2588
+ this.implementation.sendStats("session_key_required");
2589
+ throw new IOException("Session key required when public key is present");
2590
+ }
2591
+ if (manifest != null && manifest.length() == 0) {
2592
+ logger.error("Empty manifest provided");
2593
+ throw new IOException("Manifest cannot be empty");
2594
+ }
2595
+ if (manifest == null && (checksum == null || checksum.isEmpty())) {
2596
+ logger.error("No checksum provided");
2597
+ this.implementation.sendStats("checksum_required");
2598
+ throw new IOException("Checksum required");
2599
+ }
2549
2600
  if (manifest != null) {
2550
2601
  return this.implementation.downloadManifest(url, version, sessionKey, checksum, manifest);
2551
2602
  }
@@ -2621,6 +2672,7 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
2621
2672
  this.syncKeepUrlPathFlag(true);
2622
2673
  }
2623
2674
  logger.info("Reloading: " + path);
2675
+ this.stampReadyGenerationBeforeReload();
2624
2676
 
2625
2677
  if (pathHandler != null) {
2626
2678
  if (usingBuiltin) {
@@ -4063,9 +4115,103 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
4063
4115
  this.periodCheckDelay
4064
4116
  );
4065
4117
  }
4118
+ static boolean shouldAcceptReadyCall(
4119
+ final boolean guardArmed,
4120
+ final int expectedGeneration,
4121
+ final boolean hasGeneration,
4122
+ final int reportedGeneration
4123
+ ) {
4124
+ if (!guardArmed) {
4125
+ return true;
4126
+ }
4127
+ return hasGeneration && reportedGeneration == expectedGeneration;
4128
+ }
4129
+
4130
+ static String readyGenerationScript(final int generation) {
4131
+ return "(function(){window.__CAPGO_READY_GEN=" + generation + ";})();";
4132
+ }
4133
+
4134
+ private int armReadyGuard() {
4135
+ synchronized (this.readyGuardLock) {
4136
+ this.readyGeneration = this.readyGeneration + 1;
4137
+ this.readyGuardArmed = true;
4138
+ return this.readyGeneration;
4139
+ }
4140
+ }
4141
+
4142
+ private void disarmReadyGuard(final int generation) {
4143
+ synchronized (this.readyGuardLock) {
4144
+ if (this.readyGeneration == generation) {
4145
+ this.readyGuardArmed = false;
4146
+ logger.warn("Could not stamp notifyAppReady for the next page. Readiness guard disabled for this reload.");
4147
+ }
4148
+ }
4149
+ }
4150
+
4151
+ private void stampReadyGenerationBeforeReload() {
4152
+ final int generation = this.armReadyGuard();
4153
+ final android.webkit.WebView webView = getUpdaterWebView();
4154
+ if (webView == null) {
4155
+ this.disarmReadyGuard(generation);
4156
+ return;
4157
+ }
4158
+ cordova.getActivity().runOnUiThread(() -> {
4159
+ if (!this.installReadyGenerationScript(webView, generation)) {
4160
+ this.disarmReadyGuard(generation);
4161
+ }
4162
+ });
4163
+ }
4164
+
4165
+ private boolean installReadyGenerationScript(final android.webkit.WebView webView, final int generation) {
4166
+ try {
4167
+ final Class<?> webViewFeature = Class.forName("androidx.webkit.WebViewFeature");
4168
+ final String feature = (String) webViewFeature.getField("DOCUMENT_START_SCRIPT").get(null);
4169
+ final Boolean supported = (Boolean) webViewFeature.getMethod("isFeatureSupported", String.class).invoke(null, feature);
4170
+ if (!Boolean.TRUE.equals(supported)) {
4171
+ return false;
4172
+ }
4173
+ final Class<?> webViewCompat = Class.forName("androidx.webkit.WebViewCompat");
4174
+ if (this.readyGenerationScriptHandle != null) {
4175
+ try {
4176
+ this.readyGenerationScriptHandle.getClass().getMethod("remove").invoke(this.readyGenerationScriptHandle);
4177
+ } catch (final Exception removeError) {
4178
+ logger.warn("Unable to remove previous notifyAppReady generation script: " + removeError.getMessage());
4179
+ }
4180
+ this.readyGenerationScriptHandle = null;
4181
+ }
4182
+ this.readyGenerationScriptHandle = webViewCompat
4183
+ .getMethod("addDocumentStartJavaScript", android.webkit.WebView.class, String.class, Set.class)
4184
+ .invoke(null, webView, readyGenerationScript(generation), java.util.Collections.singleton("*"));
4185
+ return true;
4186
+ } catch (final Exception e) {
4187
+ logger.warn("Unable to stamp notifyAppReady generation: " + e.getMessage());
4188
+ return false;
4189
+ }
4190
+ }
4191
+
4192
+ private boolean acceptsReadyCall(final PluginCall call) {
4193
+ if (!this.readyGuardArmed) {
4194
+ return true;
4195
+ }
4196
+ final JSONObject data = call.getData();
4197
+ final boolean hasGeneration = data != null && data.has("loadGeneration");
4198
+ final int reported = hasGeneration ? data.optInt("loadGeneration", -1) : -1;
4199
+ return shouldAcceptReadyCall(this.readyGuardArmed, this.readyGeneration, hasGeneration, reported);
4200
+ }
4201
+
4066
4202
  public void notifyAppReady(final PluginCall call) {
4067
4203
  ensureBridgeSet();
4068
4204
  try {
4205
+ if (!this.acceptsReadyCall(call)) {
4206
+ logger.info("Ignoring notifyAppReady from a page that is no longer current");
4207
+ final BundleInfo current = this.implementation.getCurrentBundle();
4208
+ final app.capgo.cordova.updater.compat.JSObject ignored = new app.capgo.cordova.updater.compat.JSObject();
4209
+ if (current != null) {
4210
+ ignored.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
4211
+ }
4212
+ call.resolve(ignored);
4213
+ return;
4214
+ }
4069
4215
  final BundleInfo bundle = this.implementation.getCurrentBundle();
4070
4216
  this.implementation.setSuccess(bundle, this.autoDeletePrevious);
4071
4217
  logger.info("Current bundle loaded successfully. ['notifyAppReady()' was called] " + bundle);
@@ -4352,6 +4498,7 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
4352
4498
  if (this.shouldBlockAutoUpdateForPreviewSession()) {
4353
4499
  return null;
4354
4500
  }
4501
+ final long generation = ++this.activeBackgroundDownloadGeneration;
4355
4502
  final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
4356
4503
  final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
4357
4504
  final String messageUpdate = initialDirectUpdateAllowed
@@ -4487,6 +4634,22 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
4487
4634
  if (
4488
4635
  latestVersionName != null && !latestVersionName.isEmpty() && !current.getVersionName().equals(latestVersionName)
4489
4636
  ) {
4637
+ final String latestSessionKey = jsRes.has("sessionKey") ? jsRes.getString("sessionKey") : "";
4638
+ if (
4639
+ !CordovaUpdaterPlugin.this.implementation.publicKey.isEmpty() &&
4640
+ !CryptoCipher.isValidSessionKey(latestSessionKey)
4641
+ ) {
4642
+ logger.error("Public key present but no valid session key provided");
4643
+ CordovaUpdaterPlugin.this.implementation.sendStats("session_key_required");
4644
+ CordovaUpdaterPlugin.this.endBackGroundTaskWithNotif(
4645
+ "Session key required when public key is present",
4646
+ latestVersionName,
4647
+ current,
4648
+ true,
4649
+ plannedDirectUpdate
4650
+ );
4651
+ return;
4652
+ }
4490
4653
  final BundleInfo latest = CordovaUpdaterPlugin.this.implementation.getBundleInfoByName(latestVersionName);
4491
4654
  if (latest != null) {
4492
4655
  final app.capgo.cordova.updater.compat.JSObject ret = new app.capgo.cordova.updater.compat.JSObject();
@@ -4502,7 +4665,10 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
4502
4665
  );
4503
4666
  return;
4504
4667
  }
4505
- if (latest.isDownloaded() && BundleStatus.DOWNLOADING != latest.getStatus()) {
4668
+ if (
4669
+ latest.isDownloaded() &&
4670
+ BundleStatus.DOWNLOADING != latest.getStatus()
4671
+ ) {
4506
4672
  logger.info("Latest bundle already exists and download is NOT required. " + messageUpdate);
4507
4673
  final boolean directUpdateAllowedNow = CordovaUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
4508
4674
  plannedDirectUpdate
@@ -4623,7 +4789,8 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
4623
4789
  sessionKey,
4624
4790
  checksum,
4625
4791
  manifest,
4626
- CordovaUpdaterPlugin.this.shouldAutoSetNextBundle()
4792
+ CordovaUpdaterPlugin.this.shouldAutoSetNextBundle(),
4793
+ generation
4627
4794
  );
4628
4795
  } else {
4629
4796
  // Handle single file download (existing code)
@@ -4633,7 +4800,8 @@ public class CordovaUpdaterPlugin extends org.apache.cordova.CordovaPlugin imple
4633
4800
  sessionKey,
4634
4801
  checksum,
4635
4802
  null,
4636
- CordovaUpdaterPlugin.this.shouldAutoSetNextBundle()
4803
+ CordovaUpdaterPlugin.this.shouldAutoSetNextBundle(),
4804
+ generation
4637
4805
  );
4638
4806
  }
4639
4807
  } catch (final Exception e) {