diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80a064c68a..ea338db0f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ concurrency: jobs: setup: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 30 steps: - name: Get Latest uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -38,7 +38,7 @@ jobs: key: ${{ runner.OS }}-bun-dependencies-cache-${{ hashFiles('bun.lock') }} lint: runs-on: macos-15 - timeout-minutes: 10 + timeout-minutes: 30 steps: - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: @@ -57,7 +57,7 @@ jobs: - run: bun run lint test-cli: runs-on: macos-15 - timeout-minutes: 10 + timeout-minutes: 30 needs: - setup - lint @@ -81,7 +81,7 @@ jobs: working-directory: ./cli test-core: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 30 needs: - setup - lint @@ -105,7 +105,7 @@ jobs: working-directory: ./core test-ios: runs-on: macos-15 - timeout-minutes: 10 + timeout-minutes: 30 needs: - setup - lint @@ -136,7 +136,7 @@ jobs: run: sh ./scripts/native-podspec.sh lint test-android: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 30 needs: - setup - lint diff --git a/CHANGELOG.md b/CHANGELOG.md index 5064d193a9..5fd3394cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,33 +3,21 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [8.5.2](https://github.com/Cap-go/capacitor-plus/compare/8.5.1...8.5.2) (2026-08-26) - -**Note:** Version bump only for package capacitor - - - - - -## [8.5.1](https://github.com/Cap-go/capacitor-plus/compare/8.3.12...8.5.1) (2026-08-25) - +## [8.5.1](https://github.com/ionic-team/capacitor/compare/8.5.0...8.5.1) (2026-08-31) ### Bug Fixes -* **ci:** fetch upstream objects so plus branch sync works ([#109](https://github.com/Cap-go/capacitor-plus/issues/109)) ([1fa7eee](https://github.com/Cap-go/capacitor-plus/commit/1fa7eeeff9494e9b32afb20a9850389b79d6bcec)) -* **ci:** resolve modify/delete conflicts in sync conflict PRs ([#110](https://github.com/Cap-go/capacitor-plus/issues/110)) ([5c29fb9](https://github.com/Cap-go/capacitor-plus/commit/5c29fb9af001b9aafdcde7b7054585e093f44431)) - - - - +- block navigation to the internal HTTP proxy path ([ee586ae](https://github.com/ionic-team/capacitor/commit/ee586ae680887ba99d066616f976db149542d922)) +- **cli:** use POSIX paths in CapApp-SPM Package.swift ([#8549](https://github.com/ionic-team/capacitor/issues/8549)) ([5e5bb3b](https://github.com/ionic-team/capacitor/commit/5e5bb3befc312477900252ab07e23b596f8cb0d1)) +- **core:** prevent removeListener from removing wrong listener ([#8271](https://github.com/ionic-team/capacitor/issues/8271)) ([5ac4dd6](https://github.com/ionic-team/capacitor/commit/5ac4dd613ae989d8dc8738ea25b77efbd4fa21fe)) # [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31) -## Bug Fixes +### Bug Fixes - **cli:** support TypeScript 7 when loading capacitor.config.ts ([#8534](https://github.com/ionic-team/capacitor/issues/8534)) ([4c1c870](https://github.com/ionic-team/capacitor/commit/4c1c8709413b9c19b008c99122ca330cc3c90e6f)) -## Features +### Features - **cli:** add migrator functionality for adopting UIScene ([#8544](https://github.com/ionic-team/capacitor/issues/8544)) ([984fa85](https://github.com/ionic-team/capacitor/commit/984fa85ba0adab0aacf895aed6323bf4b503dccb)) - **ios:** UIScene Support ([#8536](https://github.com/ionic-team/capacitor/issues/8536)) ([3fa04a3](https://github.com/ionic-team/capacitor/commit/3fa04a357c92af34cd6fccb8124791963804a9dc)) @@ -49,14 +37,14 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline # [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) -## Bug Fixes +### Bug Fixes - **android:** show only the requested system bar ([#8480](https://github.com/ionic-team/capacitor/issues/8480)) ([4c6c321](https://github.com/ionic-team/capacitor/commit/4c6c3219afb5223211e857457e46283c37eb9424)) - **cli:** revert live reload config on failure ([#8485](https://github.com/ionic-team/capacitor/issues/8485)) ([1d031a4](https://github.com/ionic-team/capacitor/commit/1d031a4abec2c793079ba8897ad2e40c4cc6c7f9)) - **SystemBars:** make `safe-area-inset-x` available on API <= 34 ([#8424](https://github.com/ionic-team/capacitor/issues/8424)) ([e456de0](https://github.com/ionic-team/capacitor/commit/e456de083e19644f484bec5a5359cb67960ac8bc)) - **SystemBars:** respect `insetsHandling` disable ([#8481](https://github.com/ionic-team/capacitor/issues/8481)) ([d4ad7ff](https://github.com/ionic-team/capacitor/commit/d4ad7ffe39daf66e0cfc63af9028d5c05543bde7)) -## Features +### Features - add method getDouble to plugin config ([#7638](https://github.com/ionic-team/capacitor/issues/7638)) ([93c72de](https://github.com/ionic-team/capacitor/commit/93c72de40a2ec4c78b33659250cb08340083088e)) - **cli:** add experimental packageOptions ([#8471](https://github.com/ionic-team/capacitor/issues/8471)) ([258867b](https://github.com/ionic-team/capacitor/commit/258867b7bf37b1837b99b02ec9638e5a6df08d97)) diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index a6ec97e3ba..54fa1f0fd1 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -3,21 +3,11 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [8.5.2](https://github.com/Cap-go/capacitor-plus/compare/8.5.1...8.5.2) (2026-08-26) - -**Note:** Version bump only for package @capacitor-plus/android - - - - - -## [8.5.1](https://github.com/Cap-go/capacitor-plus/compare/8.3.12...8.5.1) (2026-08-25) - -**Note:** Version bump only for package @capacitor-plus/android - - +## [8.5.1](https://github.com/ionic-team/capacitor/compare/8.5.0...8.5.1) (2026-08-31) +### Bug Fixes +- block navigation to the internal HTTP proxy path ([ee586ae](https://github.com/ionic-team/capacitor/commit/ee586ae680887ba99d066616f976db149542d922)) # [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31) diff --git a/android/capacitor/src/androidTest/AndroidManifest.xml b/android/capacitor/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000000..33952f2e02 --- /dev/null +++ b/android/capacitor/src/androidTest/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/android/capacitor/src/androidTest/java/com/getcapacitor/android/HttpInterceptorNavigationTest.java b/android/capacitor/src/androidTest/java/com/getcapacitor/android/HttpInterceptorNavigationTest.java new file mode 100644 index 0000000000..1cde86b4ba --- /dev/null +++ b/android/capacitor/src/androidTest/java/com/getcapacitor/android/HttpInterceptorNavigationTest.java @@ -0,0 +1,115 @@ +package com.getcapacitor.android; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.net.Uri; +import android.webkit.WebResourceRequest; +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.getcapacitor.Bridge; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * The proxy path shares the app's host and scheme, so the host/scheme guard alone would let it + * load in the WebView. + */ +@RunWith(AndroidJUnit4.class) +public class HttpInterceptorNavigationTest { + + private static final String INTERCEPTOR_URL = + "https://localhost" + Bridge.CAPACITOR_HTTP_INTERCEPTOR_START + "?u=https://example.com/payload.html"; + private static final String IN_APP_URL = "https://localhost/index.html"; + private static final String EXTERNAL_URL = "https://example.com/"; + + /** A plugin registered by the test host returns "allow" for the proxy path; it must not win. */ + @Test + public void blocksNavigationToInterceptorPath() { + try (ActivityScenario scenario = ActivityScenario.launch(TestHostActivity.class)) { + scenario.onActivity((activity) -> { + Bridge bridge = activity.getBridge(); + assertNotNull(bridge); + assertTrue("interceptor navigation must be blocked", bridge.launchIntent(Uri.parse(INTERCEPTOR_URL))); + assertFalse("in-app navigation must stay in the WebView", bridge.launchIntent(Uri.parse(IN_APP_URL))); + assertTrue("external navigation must leave the WebView", bridge.launchIntent(Uri.parse(EXTERNAL_URL))); + }); + } + } + + /** An iframe looks like a fetch to isForMainFrame(), so subframe documents must be refused too. */ + @Test + public void refusesProxyForDocumentRequests() { + try (ActivityScenario scenario = ActivityScenario.launch(TestHostActivity.class)) { + scenario.onActivity((activity) -> { + Bridge bridge = activity.getBridge(); + assertNotNull(bridge); + // Without this the proxy refuses everything and the assertions below prove nothing. + assertTrue( + "CapacitorHttp must be enabled for this test to mean anything", + bridge.getConfig().getPluginConfiguration("CapacitorHttp").getBoolean("enabled", false) + ); + + Map navHeaders = new HashMap<>(); + navHeaders.put("Accept", "text/html,application/xhtml+xml"); + navHeaders.put("Upgrade-Insecure-Requests", "1"); + + assertNull( + "main frame document must be refused", + bridge.getLocalServer().shouldInterceptRequest(new FakeRequest(INTERCEPTOR_URL, true, navHeaders)) + ); + assertNull( + "iframe document must be refused", + bridge.getLocalServer().shouldInterceptRequest(new FakeRequest(INTERCEPTOR_URL, false, navHeaders)) + ); + }); + } + } + + private static class FakeRequest implements WebResourceRequest { + + private final Uri url; + private final boolean mainFrame; + private final Map headers; + + FakeRequest(String url, boolean mainFrame, Map headers) { + this.url = Uri.parse(url); + this.mainFrame = mainFrame; + this.headers = headers; + } + + @Override + public Uri getUrl() { + return url; + } + + @Override + public boolean isForMainFrame() { + return mainFrame; + } + + @Override + public boolean isRedirect() { + return false; + } + + @Override + public boolean hasGesture() { + return false; + } + + @Override + public String getMethod() { + return "GET"; + } + + @Override + public Map getRequestHeaders() { + return headers; + } + } +} diff --git a/android/capacitor/src/androidTest/java/com/getcapacitor/android/InterceptorAllowingPlugin.java b/android/capacitor/src/androidTest/java/com/getcapacitor/android/InterceptorAllowingPlugin.java new file mode 100644 index 0000000000..49be9f906a --- /dev/null +++ b/android/capacitor/src/androidTest/java/com/getcapacitor/android/InterceptorAllowingPlugin.java @@ -0,0 +1,20 @@ +package com.getcapacitor.android; + +import android.net.Uri; +import com.getcapacitor.Bridge; +import com.getcapacitor.Plugin; +import com.getcapacitor.annotation.CapacitorPlugin; + +/** A plugin that tries to allow the proxy path. The navigation guard must ignore it. */ +@CapacitorPlugin(name = "InterceptorAllowingPlugin") +public class InterceptorAllowingPlugin extends Plugin { + + @Override + public Boolean shouldOverrideLoad(Uri url) { + String path = url.getPath(); + if (path != null && path.startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START)) { + return false; // "allow this navigation" + } + return null; + } +} diff --git a/android/capacitor/src/androidTest/java/com/getcapacitor/android/TestHostActivity.java b/android/capacitor/src/androidTest/java/com/getcapacitor/android/TestHostActivity.java new file mode 100644 index 0000000000..05d7a5d7c5 --- /dev/null +++ b/android/capacitor/src/androidTest/java/com/getcapacitor/android/TestHostActivity.java @@ -0,0 +1,27 @@ +package com.getcapacitor.android; + +import com.getcapacitor.BridgeActivity; +import com.getcapacitor.CapConfig; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Host for instrumented tests. CapacitorHttp is on so the proxy is reachable, and a plugin that + * tries to allow the proxy path is registered so tests can prove the guard still wins. + */ +public class TestHostActivity extends BridgeActivity { + + @Override + protected void load() { + registerPlugin(InterceptorAllowingPlugin.class); + + try { + JSONObject plugins = new JSONObject("{\"CapacitorHttp\":{\"enabled\":true}}"); + config = new CapConfig.Builder(this).setPluginsConfiguration(plugins).create(); + } catch (JSONException e) { + throw new IllegalStateException("bad test plugin config", e); + } + + super.load(); + } +} diff --git a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java index ca6fd9c8fc..65cefa4d39 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/Bridge.java +++ b/android/capacitor/src/main/java/com/getcapacitor/Bridge.java @@ -394,6 +394,12 @@ private int extractWebViewMajorVersion(final PackageManager pm, final String web } public boolean launchIntent(Uri url) { + // The proxy returns a remote body at the app origin, so block it before plugins can allow it. + String path = url.getPath(); + if (path != null && path.startsWith(CAPACITOR_HTTP_INTERCEPTOR_START)) { + return true; + } + /* * Give plugins the chance to handle the url */ diff --git a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java index c2075811a0..bac57209fa 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java +++ b/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java @@ -412,12 +412,6 @@ private boolean showImageCapturePicker(final ValueCallback filePathCallba } takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri); takePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - - // Store in static variables to survive activity recreation - pendingFilePathCallback = filePathCallback; - pendingImageFileUri = imageFileUri; - pendingFileChooserType = FileChooserType.IMAGE_CAPTURE; - activityListener = (activityResult) -> { Uri[] result = null; if (activityResult.getResultCode() == Activity.RESULT_OK) { diff --git a/android/capacitor/src/main/java/com/getcapacitor/Plugin.java b/android/capacitor/src/main/java/com/getcapacitor/Plugin.java index 19c8b8b3a6..e209bab8aa 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/Plugin.java +++ b/android/capacitor/src/main/java/com/getcapacitor/Plugin.java @@ -1003,7 +1003,8 @@ protected void handleOnDestroy() {} * Give the plugins a chance to take control when a URL is about to be loaded in the WebView. * Returning true causes the WebView to abort loading the URL. * Returning false causes the WebView to continue loading the URL. - * Returning null will defer to the default Capacitor policy + * Returning null will defer to the default Capacitor policy. + * Not called for Capacitor's internal HTTP proxy path, which is always blocked. */ @SuppressWarnings("unused") public Boolean shouldOverrideLoad(Uri url) { diff --git a/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java b/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java index 016609ce3a..8974173a23 100755 --- a/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java +++ b/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java @@ -185,6 +185,11 @@ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { Uri loadingUrl = request.getUrl(); if (null != loadingUrl.getPath() && loadingUrl.getPath().startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START)) { + // Only fetch/XHR should reach the proxy; a document would run remote content at the app origin. + boolean httpEnabled = bridge.getConfig().getPluginConfiguration("CapacitorHttp").getBoolean("enabled", false); + if (!httpEnabled || isDocumentRequest(request)) { + return null; + } Logger.debug("Handling CapacitorHttp request: " + loadingUrl); try { return handleCapacitorHttpRequest(request); @@ -210,6 +215,24 @@ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { } } + /** isForMainFrame() is false for an iframe and a fetch alike; only navigations send this header. */ + private boolean isDocumentRequest(WebResourceRequest request) { + if (request.isForMainFrame()) { + return true; + } + Map headers = request.getRequestHeaders(); + if (headers == null) { + // The proxy needs the headers too, so the request fails there anyway. + return false; + } + for (String header : headers.keySet()) { + if ("Upgrade-Insecure-Requests".equalsIgnoreCase(header)) { + return true; + } + } + return false; + } + private boolean isLocalFile(Uri uri) { String path = uri.getPath(); return path.startsWith(capacitorContentStart) || path.startsWith(capacitorFileStart); @@ -333,6 +356,9 @@ private WebResourceResponse handleCapacitorHttpRequest(WebResourceRequest reques int responseCode = connection.getResponseCode(); String reasonPhrase = getReasonPhraseFromResponseCode(responseCode); + // Nothing should render this. If anything does, sandbox keeps it inert and off the app origin. + responseHeaders.put("Content-Security-Policy", "sandbox; frame-ancestors 'none'"); + return new WebResourceResponse(mimeType, encoding, responseCode, reasonPhrase, responseHeaders, inputStream); } @@ -756,48 +782,6 @@ public long skip(long n) throws IOException { } } - /** - * An InputStream wrapper that limits the number of bytes that can be read. - */ - static class BoundedInputStream extends InputStream { - - private final InputStream in; - private long remaining; - - public BoundedInputStream(InputStream in, long limit) { - this.in = in; - this.remaining = limit; - } - - @Override - public int available() throws IOException { - int available = in.available(); - return (int) Math.min(available, remaining); - } - - @Override - public int read() throws IOException { - if (remaining <= 0) return -1; - int result = in.read(); - if (result != -1) remaining--; - return result; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (remaining <= 0) return -1; - int toRead = (int) Math.min(len, remaining); - int result = in.read(b, off, toRead); - if (result > 0) remaining -= result; - return result; - } - - @Override - public void close() throws IOException { - in.close(); - } - } - // For L and above. private static class LollipopLazyInputStream extends LazyInputStream { diff --git a/android/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java b/android/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java index 1115429d7b..020c1457a1 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java +++ b/android/capacitor/src/main/java/com/getcapacitor/cordova/MockCordovaWebViewImpl.java @@ -70,14 +70,12 @@ public CapacitorEvalBridgeMode(WebView webView, CordovaInterface cordova) { @Override public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) { - cordova - .getActivity() - .runOnUiThread(() -> { - String js = queue.popAndEncodeAsJs(); - if (js != null) { - webView.evaluateJavascript(js, null); - } - }); + cordova.getActivity().runOnUiThread(() -> { + String js = queue.popAndEncodeAsJs(); + if (js != null) { + webView.evaluateJavascript(js, null); + } + }); } } diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java index 84a4bfa8a3..9a50858f40 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -44,17 +44,17 @@ public class SystemBars extends Plugin { private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_KEYBOARD_FIX = 144; static final String viewportMetaJSFunction = """ - function capacitorSystemBarsCheckMetaViewport() { - const meta = document.querySelectorAll("meta[name=viewport]"); - if (meta.length == 0) { - return false; - } - // get the last found meta viewport tag - const metaContent = meta[meta.length - 1].content; - return metaContent.includes("viewport-fit=cover"); + function capacitorSystemBarsCheckMetaViewport() { + const meta = document.querySelectorAll("meta[name=viewport]"); + if (meta.length == 0) { + return false; } - capacitorSystemBarsCheckMetaViewport(); - """; + // get the last found meta viewport tag + const metaContent = meta[meta.length - 1].content; + return metaContent.includes("viewport-fit=cover"); + } + capacitorSystemBarsCheckMetaViewport(); + """; private String insetsHandling = INSETS_HANDLING_CSS; private boolean hasViewportCover = false; @@ -272,7 +272,7 @@ private void initWindowInsetsListener() { // We need to correct for a possible shown IME v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0); - Insets safeAreaInsets = calcSafeAreaInsets(safeAreaSource); + Insets safeAreaInsets = calcSafeAreaInsets(insets); injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); return new WindowInsetsCompat.Builder(insets) @@ -297,7 +297,7 @@ private void initWindowInsetsListener() { .setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0)) .build(); - Insets safeAreaInsets = calcSafeAreaInsets(safeAreaSource); + Insets safeAreaInsets = calcSafeAreaInsets(newInsets); injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); return newInsets; @@ -364,7 +364,6 @@ private void setHidden(boolean hide, String bar) { if (hide) { if (bar.isEmpty()) { windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars()); - navBarVisible = false; } else if (bar.equals(BAR_STATUS_BAR)) { windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.statusBars()); } else if (bar.equals(BAR_GESTURE_BAR)) { @@ -376,7 +375,6 @@ private void setHidden(boolean hide, String bar) { if (bar.isEmpty()) { windowInsetsControllerCompat.show(WindowInsetsCompat.Type.systemBars()); - navBarVisible = true; } else if (bar.equals(BAR_STATUS_BAR)) { windowInsetsControllerCompat.show(WindowInsetsCompat.Type.statusBars()); } else if (bar.equals(BAR_GESTURE_BAR)) { diff --git a/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java b/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java index 0ea955ae88..f72373a88e 100644 --- a/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java +++ b/android/capacitor/src/test/java/com/getcapacitor/plugin/SystemBarsTest.java @@ -1,7 +1,5 @@ package com.getcapacitor.plugin; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; @@ -15,7 +13,6 @@ import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsControllerCompat; import com.getcapacitor.Bridge; -import java.lang.reflect.Field; import java.lang.reflect.Method; import org.junit.Test; import org.mockito.MockedStatic; @@ -49,66 +46,8 @@ public void showWithNavigationBarShowsOnlyNavigationBars() throws Exception { verify(controller, never()).show(WindowInsetsCompat.Type.statusBars()); } - @Test - public void hideWithEmptyBarHidesSystemBars() throws Exception { - WindowInsetsControllerCompat controller = invokeSetHidden(new SystemBars(), true, ""); - - verify(controller).hide(WindowInsetsCompat.Type.systemBars()); - verify(controller, never()).hide(WindowInsetsCompat.Type.statusBars()); - verify(controller, never()).hide(WindowInsetsCompat.Type.navigationBars()); - } - - @Test - public void hideWithStatusBarHidesOnlyStatusBars() throws Exception { - WindowInsetsControllerCompat controller = invokeSetHidden(new SystemBars(), true, "StatusBar"); - - verify(controller).hide(WindowInsetsCompat.Type.statusBars()); - verify(controller, never()).hide(WindowInsetsCompat.Type.systemBars()); - verify(controller, never()).hide(WindowInsetsCompat.Type.navigationBars()); - } - - @Test - public void togglingNavigationBarTracksNavBarVisible() throws Exception { - SystemBars plugin = new SystemBars(); - - invokeSetHidden(plugin, true, "NavigationBar"); - assertFalse(navBarVisible(plugin)); - - invokeSetHidden(plugin, false, "NavigationBar"); - assertTrue(navBarVisible(plugin)); - } - - @Test - public void togglingAllBarsTracksNavBarVisible() throws Exception { - SystemBars plugin = new SystemBars(); - - invokeSetHidden(plugin, true, ""); - assertFalse(navBarVisible(plugin)); - - invokeSetHidden(plugin, false, ""); - assertTrue(navBarVisible(plugin)); - } - - @Test - public void hidingOnlyStatusBarLeavesNavBarVisible() throws Exception { - SystemBars plugin = new SystemBars(); - - invokeSetHidden(plugin, true, "StatusBar"); - - assertTrue(navBarVisible(plugin)); - } - - private boolean navBarVisible(SystemBars plugin) throws Exception { - Field field = SystemBars.class.getDeclaredField("navBarVisible"); - field.setAccessible(true); - return field.getBoolean(plugin); - } - private WindowInsetsControllerCompat invokeSetHidden(String bar) throws Exception { - return invokeSetHidden(new SystemBars(), false, bar); - } - - private WindowInsetsControllerCompat invokeSetHidden(SystemBars plugin, boolean hide, String bar) throws Exception { + SystemBars plugin = new SystemBars(); Bridge bridge = mock(Bridge.class); AppCompatActivity activity = mock(AppCompatActivity.class); Window window = mock(Window.class); @@ -126,7 +65,7 @@ private WindowInsetsControllerCompat invokeSetHidden(SystemBars plugin, boolean Method setHidden = SystemBars.class.getDeclaredMethod("setHidden", boolean.class, String.class); setHidden.setAccessible(true); - setHidden.invoke(plugin, hide, bar); + setHidden.invoke(plugin, false, bar); } return controller; diff --git a/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java b/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java index fd8b686ee6..1006721235 100644 --- a/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java +++ b/android/capacitor/src/test/java/com/getcapacitor/plugin/util/HttpRequestHandlerTest.java @@ -11,8 +11,8 @@ public class HttpRequestHandlerTest { static final String BASE_URL = "https://httpbin.org/get"; static final String PARAMS_JSON = """ - {"k": "a&b"} - """; + {"k": "a&b"} + """; @Test public void testHttpURLConnectionBuilderSetUrlParamsEncoded() throws Exception { diff --git a/android/package.json b/android/package.json index f42e65913d..9e1ad80672 100644 --- a/android/package.json +++ b/android/package.json @@ -1,9 +1,9 @@ { - "name": "@capacitor-plus/android", - "version": "8.5.2", - "description": "Capacitor+: Enhanced Capacitor with automated upstream sync - Cross-platform apps with JavaScript and the web", - "homepage": "https://capgo.app/docs/plugins/capacitor-plus/", - "author": "Capgo Team (https://capgo.app)", + "name": "@capacitor/android", + "version": "8.5.1", + "description": "Capacitor: Cross-platform apps with JavaScript and the web", + "homepage": "https://capacitorjs.com", + "author": "Ionic Team (https://ionic.io)", "license": "MIT", "repository": { "type": "git", @@ -23,7 +23,6 @@ "verify": "./gradlew clean lint build test -b capacitor/build.gradle" }, "peerDependencies": { - "@capacitor-plus/core": "^8.5.0", "@capacitor/core": "^8.5.0" }, "publishConfig": { diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 64ffed5668..847eac90ee 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -3,29 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [8.5.2](https://github.com/Cap-go/capacitor-plus/compare/8.5.1...8.5.2) (2026-08-26) - -**Note:** Version bump only for package @capacitor-plus/cli - - - - - -## [8.5.1](https://github.com/Cap-go/capacitor-plus/compare/8.3.12...8.5.1) (2026-08-25) - -**Note:** Version bump only for package @capacitor-plus/cli - - +## [8.5.1](https://github.com/ionic-team/capacitor/compare/8.5.0...8.5.1) (2026-08-31) +### Bug Fixes +- **cli:** use POSIX paths in CapApp-SPM Package.swift ([#8549](https://github.com/ionic-team/capacitor/issues/8549)) ([5e5bb3b](https://github.com/ionic-team/capacitor/commit/5e5bb3befc312477900252ab07e23b596f8cb0d1)) # [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31) -## Bug Fixes +### Bug Fixes - **cli:** support TypeScript 7 when loading capacitor.config.ts ([#8534](https://github.com/ionic-team/capacitor/issues/8534)) ([4c1c870](https://github.com/ionic-team/capacitor/commit/4c1c8709413b9c19b008c99122ca330cc3c90e6f)) -## Features +### Features - **cli:** add migrator functionality for adopting UIScene ([#8544](https://github.com/ionic-team/capacitor/issues/8544)) ([984fa85](https://github.com/ionic-team/capacitor/commit/984fa85ba0adab0aacf895aed6323bf4b503dccb)) @@ -42,11 +32,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline # [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) -## Bug Fixes +### Bug Fixes - **cli:** revert live reload config on failure ([#8485](https://github.com/ionic-team/capacitor/issues/8485)) ([1d031a4](https://github.com/ionic-team/capacitor/commit/1d031a4abec2c793079ba8897ad2e40c4cc6c7f9)) -## Features +### Features - **cli:** add experimental packageOptions ([#8471](https://github.com/ionic-team/capacitor/issues/8471)) ([258867b](https://github.com/ionic-team/capacitor/commit/258867b7bf37b1837b99b02ec9638e5a6df08d97)) - **cli:** capture ios_package_manager in telemetry ([#8482](https://github.com/ionic-team/capacitor/issues/8482)) ([b4b297a](https://github.com/ionic-team/capacitor/commit/b4b297a52f8732659662d5e5aaeff81c0f7d9835)) diff --git a/cli/package.json b/cli/package.json index f7ee82153a..07a14e4f3c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,9 +1,9 @@ { - "name": "@capacitor-plus/cli", - "version": "8.5.2", - "description": "Capacitor+: Enhanced Capacitor with automated upstream sync - Cross-platform apps with JavaScript and the web", - "homepage": "https://capgo.app/docs/plugins/capacitor-plus/", - "author": "Capgo Team (https://capgo.app)", + "name": "@capacitor/cli", + "version": "8.5.1", + "description": "Capacitor: Cross-platform apps with JavaScript and the web", + "homepage": "https://capacitorjs.com", + "author": "Ionic Team (https://ionic.io)", "license": "MIT", "repository": { "type": "git", @@ -60,7 +60,6 @@ "semver": "^7.6.3", "tar": "^7.5.3", "tslib": "^2.8.1", - "typescript": "~5.0.2", "xcode": "^3.0.1", "xml2js": "^0.6.2" }, diff --git a/cli/src/ios/update.ts b/cli/src/ios/update.ts index 8d28888330..8790c9ca35 100644 --- a/cli/src/ios/update.ts +++ b/cli/src/ios/update.ts @@ -1,6 +1,6 @@ import { copy, remove, pathExists, readFile, realpath, writeFile } from 'fs-extra'; import { basename, dirname, join, relative } from 'path'; -import { major, prerelease, valid } from 'semver'; +import { major, prerelease } from 'semver'; import c from '../colors'; import { checkPlatformVersions, getCapacitorPackageVersion, runTask } from '../common'; @@ -59,6 +59,30 @@ async function updatePluginFiles(config: Config, plugins: Plugin[], deployment: await generateCordovaPackageFiles(cordovaPlugins, config); const validSPMPackages = await checkPluginsForPackageSwift(config, plugins); + await Promise.all( + validSPMPackages.map(async (plugin) => { + const iosPlatformVersion = await getCapacitorPackageVersion(config, config.ios.name); + const packageSwiftPath = join(plugin.rootPath, 'Package.swift'); + let content = await readFile(packageSwiftPath, { encoding: 'utf-8' }); + const regex = new RegExp( + 'url:\\s*"https://github.com/ionic-team/capacitor-swift-pm\\.git",\\s*from:\\s*"([^"]+)"', + ); + const version = content.match(regex)?.[1]; + const majorCapVersion = major(iosPlatformVersion); + if (version && major(version) != majorCapVersion) { + const preCapVersion = prerelease(iosPlatformVersion); + const forceVersion = preCapVersion ? iosPlatformVersion : `${majorCapVersion}.0.0`; + content = setAllStringIn( + content, + `url: "https://github.com/ionic-team/capacitor-swift-pm.git",`, + `)`, + ` from: "${forceVersion}"`, + ); + await writeFile(packageSwiftPath, content); + logger.warn(`${plugin.id} is built for Capacitor ${major(version)}, it might cause issues`); + } + }), + ); await generatePackageFile(config, validSPMPackages.concat(cordovaPlugins)); diff --git a/cli/src/tasks/migrate-uiscene.ts b/cli/src/tasks/migrate-uiscene.ts index 40b0bc4200..9f6dfa2789 100644 --- a/cli/src/tasks/migrate-uiscene.ts +++ b/cli/src/tasks/migrate-uiscene.ts @@ -134,125 +134,21 @@ async function scanAndWarn(config: Config): Promise { } } -function findMatchingBrace(source: string, openIdx: number): number | null { +function hasCustomDelegateBody(source: string, sigRegex: RegExp): boolean { + const match = source.match(sigRegex); + if (!match || match.index === undefined) return false; + const openIdx = source.indexOf('{', match.index); + if (openIdx === -1) return false; let depth = 1; let i = openIdx + 1; - let inLineComment = false; - let blockCommentDepth = 0; - let inString: '"' | '"""' | null = null; - let stringHashes = 0; - while (i < source.length && depth > 0) { const ch = source[i]; - const next = source[i + 1]; - - if (inLineComment) { - if (ch === '\n') inLineComment = false; - i++; - continue; - } - - if (blockCommentDepth > 0) { - if (ch === '*' && next === '/') { - blockCommentDepth--; - i += 2; - continue; - } - if (ch === '/' && next === '*') { - blockCommentDepth++; - i += 2; - continue; - } - i++; - continue; - } - - if (inString === '"') { - if (stringHashes === 0 && ch === '\\') { - i += 2; - continue; - } - if (ch === '"') { - let closingHashes = 0; - while (source[i + 1 + closingHashes] === '#') { - closingHashes++; - } - if (closingHashes === stringHashes) { - i += 1 + closingHashes; - inString = null; - stringHashes = 0; - continue; - } - } - i++; - continue; - } - - if (inString === '"""') { - if (ch === '"' && source[i + 1] === '"' && source[i + 2] === '"') { - let closingHashes = 0; - while (source[i + 3 + closingHashes] === '#') { - closingHashes++; - } - if (closingHashes === stringHashes) { - i += 3 + closingHashes; - inString = null; - stringHashes = 0; - continue; - } - } - i++; - continue; - } - - if (ch === '/' && next === '/') { - inLineComment = true; - i += 2; - continue; - } - - if (ch === '/' && next === '*') { - blockCommentDepth++; - i += 2; - continue; - } - - if (ch === '#' || ch === '"') { - let hashes = 0; - while (source[i + hashes] === '#') { - hashes++; - } - const quoteIdx = i + hashes; - if (source[quoteIdx] === '"') { - if (source[quoteIdx + 1] === '"' && source[quoteIdx + 2] === '"') { - inString = '"""'; - stringHashes = hashes; - i = quoteIdx + 3; - continue; - } - inString = '"'; - stringHashes = hashes; - i = quoteIdx + 1; - continue; - } - } - if (ch === '{') depth++; else if (ch === '}') depth--; i++; } - - return depth === 0 ? i - 1 : null; -} - -function hasCustomDelegateBody(source: string, sigRegex: RegExp): boolean { - const match = source.match(sigRegex); - if (!match || match.index === undefined) return false; - const openIdx = source.indexOf('{', match.index); - if (openIdx === -1) return false; - const closeIdx = findMatchingBrace(source, openIdx); - if (closeIdx === null) return false; - const body = source.slice(openIdx + 1, closeIdx); + if (depth !== 0) return false; + const body = source.slice(openIdx + 1, i - 1); const codeLines = body .split('\n') .map((l) => l.trim()) @@ -329,11 +225,18 @@ function extractConfigurationForConnecting(appDelegateSource: string): string | if (openIdx === -1) { return null; } - const closeIdx = findMatchingBrace(appDelegateSource, openIdx); - if (closeIdx === null) { + let depth = 1; + let i = openIdx + 1; + while (i < appDelegateSource.length && depth > 0) { + const ch = appDelegateSource[i]; + if (ch === '{') depth++; + else if (ch === '}') depth--; + i++; + } + if (depth !== 0) { return null; } - return '\n' + appDelegateSource.slice(sigMatch.index, closeIdx + 1) + '\n'; + return '\n' + appDelegateSource.slice(sigMatch.index, i) + '\n'; } function insertBeforeAppDelegateClassEnd(source: string, snippet: string): string | null { @@ -343,10 +246,18 @@ function insertBeforeAppDelegateClassEnd(source: string, snippet: string): strin return null; } const openIdx = source.indexOf('{', match.index); - const closeIdx = findMatchingBrace(source, openIdx); - if (closeIdx === null) { + let depth = 1; + let i = openIdx + 1; + while (i < source.length && depth > 0) { + const ch = source[i]; + if (ch === '{') depth++; + else if (ch === '}') depth--; + i++; + } + if (depth !== 0) { return null; } + const closeIdx = i - 1; return source.slice(0, closeIdx) + snippet + source.slice(closeIdx); } @@ -387,7 +298,6 @@ function describeSignals({ export const __testables = { classify, describeSignals, - findMatchingBrace, insertBeforeAppDelegateClassEnd, extractConfigurationForConnecting, hasCustomDelegateBody, diff --git a/cli/src/tasks/migrate.ts b/cli/src/tasks/migrate.ts index ca6f1e7f0c..8631897294 100644 --- a/cli/src/tasks/migrate.ts +++ b/cli/src/tasks/migrate.ts @@ -446,7 +446,7 @@ async function writeBreakingChanges() { )}.`, ); } - if (allDependencies['@capacitor/ios'] || allDependencies['@capacitor-plus/ios']) { + if (allDependencies['@capacitor/ios']) { logger.info( 'IMPORTANT: Capacitor 8.5 adopts UIScene on iOS. ' + 'See https://capacitorjs.com/docs/updating/8-5 for the full 8.4 → 8.5 migration guide.', diff --git a/cli/src/tasks/run.ts b/cli/src/tasks/run.ts index 780c8130e0..2fef2989f7 100644 --- a/cli/src/tasks/run.ts +++ b/cli/src/tasks/run.ts @@ -121,9 +121,6 @@ export async function runCommand( } catch (e: any) { if (options.liveReload) { await CapLiveReloadHelper.revertCapConfigForLiveReload(); - if (liveReloadManifestUpdated && platformName === config.android.name && cordovaPlugins) { - await writeCordovaAndroidManifest(cordovaPlugins, config, platformName, false); - } } if (!isFatal(e)) { fatal(e.stack ?? e); diff --git a/cli/src/util/node.ts b/cli/src/util/node.ts index 5600b5cad5..2967816113 100644 --- a/cli/src/util/node.ts +++ b/cli/src/util/node.ts @@ -30,7 +30,27 @@ const dynamicImport: (specifier: string) => Promise = new Function('specifi /** * @see https://github.com/ionic-team/stencil/blob/HEAD/src/compiler/sys/node-require.ts */ -function loadWithClassicCompiler(ts: typeof typescript, id: string): unknown { +export const requireTS = async (ts: typeof typescript, p: string): Promise => { + const id = resolve(p); + + if (!hasClassicCompilerAPI(ts)) { + // Node has its own built-in TypeScript syntax stripping (stable since Node 23.6, and + // available behind --experimental-strip-types since Node 22.6), so we can load the file + // directly via the native ESM loader instead of transpiling it ourselves. + try { + return await dynamicImport(pathToFileURL(id).href); + } catch (e: any) { + if (e?.code === 'ERR_UNKNOWN_FILE_EXTENSION') { + throw new Error( + `Your installed version of TypeScript (${ts.version}) no longer provides the compiler API Capacitor previously used to load .ts config files, ` + + `and your Node.js runtime (${process.version}) doesn't support loading them natively either.\n` + + 'Upgrade to Node.js 22.6+ (running with --experimental-strip-types), or Node.js 23.6+, to continue using capacitor.config.ts.', + ); + } + throw e; + } + } + delete require.cache[id]; require.extensions['.ts'] = (module: NodeModuleWithCompile, fileName: string) => { diff --git a/cli/src/util/spm.ts b/cli/src/util/spm.ts index 8c0ee028cd..9f623b7e0f 100644 --- a/cli/src/util/spm.ts +++ b/cli/src/util/spm.ts @@ -137,9 +137,9 @@ let package = Package( const options = packageOptions[plugin.id]; const symlink = options?.symlink; const symlinkFolder = join('symlinks', plugin.name); - const relPath = convertToUnixPath( - symlink ? symlinkFolder : relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath), - ); + const relPath = symlink + ? symlinkFolder + : convertToUnixPath(relative(config.ios.nativeXcodeProjDirAbs, plugin.rootPath)); if (symlink) { await ensureSymlink(plugin.rootPath, resolve(config.ios.nativeProjectDirAbs, 'CapApp-SPM', symlinkFolder)); } @@ -275,6 +275,7 @@ export async function addSceneManifestIfNeeded(config: Config): Promise { { UISceneConfigurationName: 'Default Configuration', UISceneDelegateClassName: '$(PRODUCT_MODULE_NAME).SceneDelegate', + UISceneStoryboardFile: 'Main', }, ], }, diff --git a/cli/src/util/xcode.ts b/cli/src/util/xcode.ts index 86a9707e9e..64ab836a34 100644 --- a/cli/src/util/xcode.ts +++ b/cli/src/util/xcode.ts @@ -1,5 +1,4 @@ import { writeFileSync } from 'fs-extra'; -import { basename } from 'path'; import { project as loadXcodeProject } from 'xcode'; import type { XcodeProject } from 'xcode'; @@ -21,8 +20,7 @@ export function addSwiftFileToAppTarget( const project = loadXcodeProject(pbxprojPath); project.parseSync(); - const targetUuid = project.getFirstTarget().uuid; - if (project.hasFile(fileRelPath) && isSwiftFileInTargetSources(project, fileRelPath, targetUuid)) { + if (project.hasFile(fileRelPath)) { return { added: false }; } @@ -31,31 +29,7 @@ export function addSwiftFileToAppTarget( throw new Error(`Could not find PBXGroup with comment "${groupName}" in ${pbxprojPath}`); } - if (project.hasFile(fileRelPath) && !isSwiftFileInTargetSources(project, fileRelPath, targetUuid)) { - const fileRefUuid = findFileRefUuid(project, fileRelPath); - if (!fileRefUuid) { - throw new Error(`Could not find PBXFileReference for ${fileRelPath} in ${pbxprojPath}`); - } - - const group = project.getPBXGroupByKey(groupUuid); - if (group && !group.children.some((child: { comment?: string }) => child.comment === fileRelPath)) { - group.children.push({ value: fileRefUuid, comment: fileRelPath }); - } - - const file = { - fileRef: fileRefUuid, - path: fileRelPath, - basename: basename(fileRelPath), - group: 'Sources', - target: targetUuid, - uuid: project.generateUuid(), - }; - (project as XcodeProject & { addToPbxBuildFileSection: (file: unknown) => void }).addToPbxBuildFileSection(file); - (project as XcodeProject & { addToPbxSourcesBuildPhase: (file: unknown) => void }).addToPbxSourcesBuildPhase(file); - writeFileSync(pbxprojPath, project.writeSync(), 'utf-8'); - return { added: true }; - } - + const targetUuid = project.getFirstTarget().uuid; const result = project.addSourceFile(fileRelPath, { target: targetUuid }, groupUuid); if (!result) { throw new Error(`Failed to register ${fileRelPath} in ${pbxprojPath}`); @@ -65,38 +39,6 @@ export function addSwiftFileToAppTarget( return { added: true }; } -function fileReferenceMatchesPath(ref: { path?: string }, fileRelPath: string): boolean { - const path = ref.path; - return path === fileRelPath || path === `"${fileRelPath}"`; -} - -function findFileRefUuid(project: XcodeProject, fileRelPath: string): string | null { - const objects = project.hash.project.objects; - const fileRefs = Object.entries(objects.PBXFileReference).filter(([k]) => !k.endsWith('_comment')); - const fileRefEntry = fileRefs.find( - ([, ref]) => typeof ref === 'object' && fileReferenceMatchesPath(ref as { path?: string }, fileRelPath), - ); - return fileRefEntry?.[0] ?? null; -} - -function isSwiftFileInTargetSources(project: XcodeProject, fileRelPath: string, targetUuid: string): boolean { - const fileRefUuid = findFileRefUuid(project, fileRelPath); - if (!fileRefUuid) { - return false; - } - - const objects = project.hash.project.objects; - const sourcesPhase = project.pbxSourcesBuildPhaseObj(targetUuid); - if (!sourcesPhase?.files) { - return false; - } - - return sourcesPhase.files.some((file: { value?: string }) => { - const buildFile = objects.PBXBuildFile[file.value ?? '']; - return typeof buildFile === 'object' && buildFile?.fileRef === fileRefUuid; - }); -} - // Exported for tests. export function findGroupUuidByComment(project: XcodeProject, comment: string): string | null { const groups = project.hash.project.objects.PBXGroup; diff --git a/cli/test/migrate-uiscene-plist.spec.ts b/cli/test/migrate-uiscene-plist.spec.ts index f07dded8c0..dc3bd25135 100644 --- a/cli/test/migrate-uiscene-plist.spec.ts +++ b/cli/test/migrate-uiscene-plist.spec.ts @@ -81,7 +81,7 @@ describe('addSceneManifestIfNeeded', () => { expect(roleArray).toHaveLength(1); expect(roleArray[0]['UISceneConfigurationName']).toBe('Default Configuration'); expect(roleArray[0]['UISceneDelegateClassName']).toBe('$(PRODUCT_MODULE_NAME).SceneDelegate'); - expect(roleArray[0]['UISceneStoryboardFile']).toBeUndefined(); + expect(roleArray[0]['UISceneStoryboardFile']).toBe('Main'); }); it('preserves sibling keys', async () => { diff --git a/cli/test/migrate-uiscene-scan.spec.ts b/cli/test/migrate-uiscene-scan.spec.ts index 7fa3f84e34..c0d7964ddf 100644 --- a/cli/test/migrate-uiscene-scan.spec.ts +++ b/cli/test/migrate-uiscene-scan.spec.ts @@ -7,7 +7,7 @@ import { __testables } from '../src/tasks/migrate-uiscene'; import { mktmp } from './util'; -const { hasCustomDelegateBody, scanAndWarn, findMatchingBrace } = __testables; +const { hasCustomDelegateBody, scanAndWarn } = __testables; const OPEN_URL_SIG = /func application\([^)]*\bopen url:/; const CONTINUE_SIG = /func application\([^)]*\bcontinue userActivity:/; @@ -82,26 +82,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { }); }); -describe('findMatchingBrace', () => { - it('ignores braces inside ordinary string literals', () => { - const source = 'class AppDelegate {\n let payload = "{\\"a\\":1}"\n}'; - const openIdx = source.indexOf('{'); - expect(findMatchingBrace(source, openIdx)).toBe(source.length - 1); - }); - - it('ignores braces inside raw string literals', () => { - const source = 'class AppDelegate {\n let payload = #"}"#\n}'; - const openIdx = source.indexOf('{'); - expect(findMatchingBrace(source, openIdx)).toBe(source.length - 1); - }); - - it('ignores braces inside multiline string literals', () => { - const source = 'class AppDelegate {\n let payload = """\n }\n """\n}'; - const openIdx = source.indexOf('{'); - expect(findMatchingBrace(source, openIdx)).toBe(source.length - 1); - }); -}); - describe('scanAndWarn', () => { let tmpDir: any; let iosDir: string; @@ -172,19 +152,13 @@ describe('scanAndWarn', () => { expect(warnSpy).not.toHaveBeenCalled(); }); - it('skips Pods/, build/, DerivedData/, and .build/ directories', async () => { + it('skips Pods/, build/, and DerivedData/ directories', async () => { const podsDir = join(iosDir, 'App', 'Pods'); const buildDir = join(iosDir, 'App', 'build'); - const derivedDataDir = join(iosDir, 'App', 'DerivedData'); - const dotBuildDir = join(iosDir, 'App', '.build'); await mkdirp(podsDir); await mkdirp(buildDir); - await mkdirp(derivedDataDir); - await mkdirp(dotBuildDir); writeFileSync(join(podsDir, 'ThirdParty.swift'), `let x = UIApplication.shared.applicationState\n`); writeFileSync(join(buildDir, 'Generated.swift'), `class TmpViewController {}\n`); - writeFileSync(join(derivedDataDir, 'BuildOutput.swift'), `class TmpViewController {}\n`); - writeFileSync(join(dotBuildDir, 'Artifact.swift'), `class TmpViewController {}\n`); await scanAndWarn(makeConfig()); diff --git a/cli/test/xcode.spec.ts b/cli/test/xcode.spec.ts index 6071945ab4..e6aa083570 100644 --- a/cli/test/xcode.spec.ts +++ b/cli/test/xcode.spec.ts @@ -23,11 +23,8 @@ describe('findGroupUuidByComment', () => { const uuid = findGroupUuidByComment(project, 'App'); - if (!uuid) { - throw new Error('Expected App group UUID'); - } expect(uuid).toMatch(/^[A-F0-9]{24}$/); - const group = project.getPBXGroupByKey(uuid); + const group = project.getPBXGroupByKey(uuid!); expect(group).toBeDefined(); expect(group?.path).toBe('App'); }); @@ -41,7 +38,7 @@ describe('findGroupUuidByComment', () => { }); describe('addSwiftFileToAppTarget', () => { - let tmpDir: Awaited> | undefined; + let tmpDir: any; let pbxprojPath: string; beforeEach(async () => { @@ -50,8 +47,7 @@ describe('addSwiftFileToAppTarget', () => { }); afterEach(() => { - const cleanup = tmpDir?.cleanupCallback as unknown as (() => void) | undefined; - cleanup?.(); + tmpDir.cleanupCallback(); }); it('registers a new Swift file in all four pbxproj sections', () => { @@ -78,14 +74,11 @@ describe('addSwiftFileToAppTarget', () => { buildFiles.some(([k]) => (objects.PBXBuildFile as any)[`${k}_comment`]?.includes('SceneDelegate.swift')), ).toBe(true); - const appGroupUuid = findGroupUuidByComment(project, 'App'); - if (!appGroupUuid) { - throw new Error('Expected App group UUID'); - } - const appGroup = project.getPBXGroupByKey(appGroupUuid); - expect(appGroup?.children.some((c: any) => c.comment === 'SceneDelegate.swift')).toBe(true); + const appGroupUuid = findGroupUuidByComment(project, 'App')!; + const appGroup = project.getPBXGroupByKey(appGroupUuid)!; + expect(appGroup.children.some((c: any) => c.comment === 'SceneDelegate.swift')).toBe(true); - const sourcesPhase = objects.PBXSourcesBuildPhase ?? {}; + const sourcesPhase = objects.PBXSourcesBuildPhase!; const sourcesEntries = Object.entries(sourcesPhase).filter(([k]) => !k.endsWith('_comment')); const [, sourcesObj] = sourcesEntries[0]; expect((sourcesObj as any).files.some((f: any) => f.comment?.includes('SceneDelegate.swift'))).toBe(true); diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 067d0aa2ee..5d03916378 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,21 +3,11 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [8.5.2](https://github.com/Cap-go/capacitor-plus/compare/8.5.1...8.5.2) (2026-08-26) - -**Note:** Version bump only for package @capacitor-plus/core - - - - - -## [8.5.1](https://github.com/Cap-go/capacitor-plus/compare/8.3.12...8.5.1) (2026-08-25) - -**Note:** Version bump only for package @capacitor-plus/core - - +## [8.5.1](https://github.com/ionic-team/capacitor/compare/8.5.0...8.5.1) (2026-08-31) +### Bug Fixes +- **core:** prevent removeListener from removing wrong listener ([#8271](https://github.com/ionic-team/capacitor/issues/8271)) ([5ac4dd6](https://github.com/ionic-team/capacitor/commit/5ac4dd613ae989d8dc8738ea25b77efbd4fa21fe)) # [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31) diff --git a/core/package.json b/core/package.json index 48d3934ba6..019c5f9a9e 100644 --- a/core/package.json +++ b/core/package.json @@ -1,9 +1,9 @@ { - "name": "@capacitor-plus/core", - "version": "8.5.2", - "description": "Capacitor+: Enhanced Capacitor with automated upstream sync - Cross-platform apps with JavaScript and the web", - "homepage": "https://capgo.app/docs/plugins/capacitor-plus/", - "author": "Capgo Team (https://capgo.app)", + "name": "@capacitor/core", + "version": "8.5.1", + "description": "Capacitor: Cross-platform apps with JavaScript and the web", + "homepage": "https://capacitorjs.com", + "author": "Ionic Team (https://ionic.io)", "license": "MIT", "repository": { "type": "git", diff --git a/ios-pods-template/App/App/Info.plist b/ios-pods-template/App/App/Info.plist index 485f40e314..f8c8a57659 100644 --- a/ios-pods-template/App/App/Info.plist +++ b/ios-pods-template/App/App/Info.plist @@ -25,19 +25,21 @@ UIApplicationSceneManifest UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + UILaunchStoryboardName LaunchScreen diff --git a/ios-spm-template/App/App/Info.plist b/ios-spm-template/App/App/Info.plist index bdf2461d53..fc41ce69bc 100644 --- a/ios-spm-template/App/App/Info.plist +++ b/ios-spm-template/App/App/Info.plist @@ -27,19 +27,21 @@ UIApplicationSceneManifest UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + UILaunchStoryboardName LaunchScreen diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 7886796ba7..e8f405421e 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -3,25 +3,15 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [8.5.2](https://github.com/Cap-go/capacitor-plus/compare/8.5.1...8.5.2) (2026-08-26) - -**Note:** Version bump only for package @capacitor-plus/ios - - - - - -## [8.5.1](https://github.com/Cap-go/capacitor-plus/compare/8.3.12...8.5.1) (2026-08-25) - -**Note:** Version bump only for package @capacitor-plus/ios - - +## [8.5.1](https://github.com/ionic-team/capacitor/compare/8.5.0...8.5.1) (2026-08-31) +### Bug Fixes +- block navigation to the internal HTTP proxy path ([ee586ae](https://github.com/ionic-team/capacitor/commit/ee586ae680887ba99d066616f976db149542d922)) # [8.5.0](https://github.com/ionic-team/capacitor/compare/8.4.2...8.5.0) (2026-07-31) -## Features +### Features - **ios:** UIScene Support ([#8536](https://github.com/ionic-team/capacitor/issues/8536)) ([3fa04a3](https://github.com/ionic-team/capacitor/commit/3fa04a357c92af34cd6fccb8124791963804a9dc)) @@ -35,7 +25,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline # [8.4.0](https://github.com/ionic-team/capacitor/compare/8.3.4...8.4.0) (2026-06-02) -## Features +### Features - add method getDouble to plugin config ([#7638](https://github.com/ionic-team/capacitor/issues/7638)) ([93c72de](https://github.com/ionic-team/capacitor/commit/93c72de40a2ec4c78b33659250cb08340083088e)) diff --git a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj index 573e90cd9a..1bfc57d944 100644 --- a/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj +++ b/ios/Capacitor/Capacitor.xcodeproj/project.pbxproj @@ -89,6 +89,7 @@ A38C3D7B2848BE6F004B3680 /* CapacitorCookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A38C3D7A2848BE6F004B3680 /* CapacitorCookieManager.swift */; }; A71289E627F380A500DADDF3 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71289E527F380A500DADDF3 /* Router.swift */; }; A71289EB27F380FD00DADDF3 /* RouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71289EA27F380FD00DADDF3 /* RouterTests.swift */; }; + AA01F00D0000000000000002 /* HttpInterceptorNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA01F00D0000000000000001 /* HttpInterceptorNavigationTests.swift */; }; A7187FD22BD1CB7D00093C45 /* CAPPluginMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7187FD12BD1CB7D00093C45 /* CAPPluginMethod.swift */; }; A76739792B98E09700795F7B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A76739782B98E09700795F7B /* PrivacyInfo.xcprivacy */; }; A771ADEE2C8B845000AF234D /* DateCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A771ADED2C8B845000AF234D /* DateCodableTests.swift */; }; @@ -248,6 +249,7 @@ A38C3D7A2848BE6F004B3680 /* CapacitorCookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapacitorCookieManager.swift; sourceTree = ""; }; A71289E527F380A500DADDF3 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; A71289EA27F380FD00DADDF3 /* RouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterTests.swift; sourceTree = ""; }; + AA01F00D0000000000000001 /* HttpInterceptorNavigationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpInterceptorNavigationTests.swift; sourceTree = ""; }; A7187FD12BD1CB7D00093C45 /* CAPPluginMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CAPPluginMethod.swift; sourceTree = ""; }; A76739782B98E09700795F7B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; A771ADED2C8B845000AF234D /* DateCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateCodableTests.swift; sourceTree = ""; }; @@ -350,6 +352,7 @@ 62E79C712638B23300414164 /* JSExportTests.swift */, 62959BBD2526510200A3D7F1 /* Info.plist */, A71289EA27F380FD00DADDF3 /* RouterTests.swift */, + AA01F00D0000000000000001 /* HttpInterceptorNavigationTests.swift */, ); path = CapacitorTests; sourceTree = ""; @@ -777,6 +780,7 @@ 62FABD2325AE60BA007B3814 /* BridgedTypesTests.m in Sources */, 621ECCC3254204B700D3D615 /* BridgedTypesTests.swift in Sources */, A71289EB27F380FD00DADDF3 /* RouterTests.swift in Sources */, + AA01F00D0000000000000002 /* HttpInterceptorNavigationTests.swift in Sources */, 6263686025F6EC0100576C1C /* PluginCallAccessorTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift b/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift index 6ef6235e92..782efe4cff 100644 --- a/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift +++ b/ios/Capacitor/Capacitor/CAPSceneDelegateProxy.swift @@ -7,7 +7,6 @@ // import Foundation -import UIKit @objc(CAPSceneDelegateProxy) public class SceneDelegateProxy: NSObject, UISceneDelegate { @@ -22,12 +21,10 @@ public class SceneDelegateProxy: NSObject, UISceneDelegate { // missed. Deliver them on the first capacitorViewDidAppear, once plugins are // registered. var token: NSObjectProtocol? - token = NotificationCenter.default.addObserver(forName: .capacitorViewDidAppear, object: nil, queue: .main) { [weak self] _ in - guard let self, Self.isBridgeReady(for: scene) else { return } + token = NotificationCenter.default.addObserver(forName: .capacitorViewDidAppear, object: nil, queue: .main) { _ in if let token { NotificationCenter.default.removeObserver(token) } - token = nil if !connectionOptions.urlContexts.isEmpty { self.scene(scene, openURLContexts: connectionOptions.urlContexts) } @@ -37,41 +34,6 @@ public class SceneDelegateProxy: NSObject, UISceneDelegate { } } - private static func isBridgeReady(for scene: UIScene) -> Bool { - guard let windowScene = scene as? UIWindowScene else { - return false - } - - for window in windowScene.windows { - guard let bridge = findBridge(in: window.rootViewController) else { - continue - } - if bridge.isViewLoaded && bridge.view.window != nil { - return true - } - } - - return false - } - - private static func findBridge(in viewController: UIViewController?) -> CAPBridgeViewController? { - if let bridge = viewController as? CAPBridgeViewController { - return bridge - } - if let navigationController = viewController as? UINavigationController { - return findBridge(in: navigationController.visibleViewController) - } - if let tabBarController = viewController as? UITabBarController { - return findBridge(in: tabBarController.selectedViewController) - } - for child in viewController?.children ?? [] { - if let bridge = findBridge(in: child) { - return bridge - } - } - return nil - } - public func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { for context in URLContexts { lastURL = context.url diff --git a/ios/Capacitor/Capacitor/CapacitorBridge.swift b/ios/Capacitor/Capacitor/CapacitorBridge.swift index 4251102e4b..657e983357 100644 --- a/ios/Capacitor/Capacitor/CapacitorBridge.swift +++ b/ios/Capacitor/Capacitor/CapacitorBridge.swift @@ -213,6 +213,7 @@ open class CapacitorBridge: NSObject, CAPBridgeProtocol { super.init() self.webViewDelegationHandler.bridge = self + self.webViewAssetHandler.setConfiguration(configuration) exportCoreJS(localUrl: configuration.localURL.absoluteString) registerPlugins() @@ -263,13 +264,16 @@ open class CapacitorBridge: NSObject, CAPBridgeProtocol { exportCordovaJS() registerCordovaPlugins() } else { - observers.append(NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in - guard self?.viewController?.view.window?.windowScene == nil else { return } - self?.triggerDocumentJSEvent(eventName: "resume") + observers.append(NotificationCenter.default.addObserver(forName: UIScene.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] notification in + if let scene = notification.object as? UIWindowScene, scene === self?.viewController?.view.window?.windowScene { + self?.triggerDocumentJSEvent(eventName: "resume") + } + }) - observers.append(NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in - guard self?.viewController?.view.window?.windowScene == nil else { return } - self?.triggerDocumentJSEvent(eventName: "pause") + observers.append(NotificationCenter.default.addObserver(forName: UIScene.didEnterBackgroundNotification, object: nil, queue: OperationQueue.main) { [weak self] notification in + if let scene = notification.object as? UIWindowScene, scene === self?.viewController?.view.window?.windowScene { + self?.triggerDocumentJSEvent(eventName: "pause") + } }) observers.append(NotificationCenter.default.addObserver(forName: UIScene.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] notification in if let scene = notification.object as? UIWindowScene, scene === self?.viewController?.view.window?.windowScene { diff --git a/ios/Capacitor/Capacitor/WebViewAssetHandler.swift b/ios/Capacitor/Capacitor/WebViewAssetHandler.swift index acc8effb74..2e5961dc2c 100644 --- a/ios/Capacitor/Capacitor/WebViewAssetHandler.swift +++ b/ios/Capacitor/Capacitor/WebViewAssetHandler.swift @@ -6,6 +6,7 @@ import UniformTypeIdentifiers open class WebViewAssetHandler: NSObject, WKURLSchemeHandler { private var router: Router private var serverUrl: URL? + private var configuration: InstanceConfiguration? public init(router: Router) { self.router = router @@ -20,6 +21,10 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler { self.serverUrl = serverUrl } + open func setConfiguration(_ configuration: InstanceConfiguration?) { + self.configuration = configuration + } + private func isUsingLiveReload(_ localUrl: URL) -> Bool { return self.serverUrl != nil && self.serverUrl?.scheme != localUrl.scheme } @@ -31,7 +36,13 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler { let localUrl = URL.init(string: url.absoluteString)! if url.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) { - handleCapacitorHttpRequest(urlSchemeTask, localUrl, false) + // Only serve the proxy when CapacitorHttp is on. A scheme task can't tell a document from + // a subresource, so keeping documents out relies on the check in decidePolicyFor. + if configuration?.getPluginConfig("CapacitorHttp").getBoolean("enabled", false) == true { + handleCapacitorHttpRequest(urlSchemeTask, localUrl, false) + } else { + urlSchemeTask.didFailWithError(URLError(.unsupportedURL)) + } return } @@ -152,12 +163,13 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler { let existingHeaders = response.allHeaderFields var newHeaders: [AnyHashable: Any] = [:] + // Nothing should render this. If anything does, sandbox keeps it inert. + newHeaders["Content-Security-Policy"] = "sandbox; frame-ancestors 'none'" + // if using live reload, then set CORS headers if self.isUsingLiveReload(url) { - newHeaders = [ - "Access-Control-Allow-Origin": self.serverUrl?.absoluteString ?? "", - "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS, TRACE" - ] + newHeaders["Access-Control-Allow-Origin"] = self.serverUrl?.absoluteString ?? "" + newHeaders["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS, TRACE" } if let mergedHeaders = existingHeaders.merging(newHeaders, uniquingKeysWith: { (_, newHeaders) in newHeaders }) as? [String: String] { diff --git a/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift b/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift index 866c7e8c5d..935a4c4117 100644 --- a/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift +++ b/ios/Capacitor/Capacitor/WebViewDelegationHandler.swift @@ -74,6 +74,12 @@ open class WebViewDelegationHandler: NSObject, WKNavigationDelegate, WKUIDelegat return } + // The proxy returns a remote body at the app origin, so block it before plugins can allow it. + if navURL.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) { + decisionHandler(.cancel) + return + } + // first, give plugins the chance to handle the decision for pluginObject in bridge.plugins { let plugin = pluginObject.value diff --git a/ios/Capacitor/CapacitorTests/HttpInterceptorNavigationTests.swift b/ios/Capacitor/CapacitorTests/HttpInterceptorNavigationTests.swift new file mode 100644 index 0000000000..eeb2f8c4a6 --- /dev/null +++ b/ios/Capacitor/CapacitorTests/HttpInterceptorNavigationTests.swift @@ -0,0 +1,63 @@ +import WebKit +import XCTest + +@testable import Capacitor + +private class StubFrameInfo: WKFrameInfo { + override var isMainFrame: Bool { false } +} + +private class StubNavigationAction: WKNavigationAction { + private let stubbedRequest: URLRequest + private let stubbedTargetFrame: WKFrameInfo? + init(url: String, subframe: Bool = false) { + self.stubbedRequest = URLRequest(url: URL(string: url)!) + self.stubbedTargetFrame = subframe ? StubFrameInfo() : nil + super.init() + } + override var request: URLRequest { stubbedRequest } + override var targetFrame: WKFrameInfo? { stubbedTargetFrame } +} + +// The proxy path shares the app's origin, so the origin guard alone would allow it. +class HttpInterceptorNavigationTests: XCTestCase { + private var bridge: MockBridge! + private var handler: WebViewDelegationHandler! + private let webView = WKWebView() + + override func setUp() { + super.setUp() + let descriptor = InstanceDescriptor() + handler = WebViewDelegationHandler() + bridge = MockBridge( + with: InstanceConfiguration(with: descriptor, isDebug: true), + delegate: MockBridgeViewController(), + cordovaConfiguration: descriptor.cordovaConfiguration, + assetHandler: MockAssetHandler(router: CapacitorRouter()), + delegationHandler: handler + ) + } + + private func policy(for url: String) -> WKNavigationActionPolicy { + var decision: WKNavigationActionPolicy? + handler.webView(webView, decidePolicyFor: StubNavigationAction(url: url)) { decision = $0 } + return decision! + } + + func testBlocksNavigationToInterceptorPath() { + let interceptorURL = "capacitor://localhost\(CapacitorBridge.httpInterceptorStartIdentifier)?u=https://example.com/payload.html" + XCTAssertEqual(policy(for: interceptorURL), .cancel) + } + + // decidePolicyFor also fires for subframes, and a same-origin iframe gets the bridge too. + func testBlocksSubframeNavigationToInterceptorPath() { + let interceptorURL = "capacitor://localhost\(CapacitorBridge.httpInterceptorStartIdentifier)?u=https://example.com/payload.html" + var decision: WKNavigationActionPolicy? + handler.webView(webView, decidePolicyFor: StubNavigationAction(url: interceptorURL, subframe: true)) { decision = $0 } + XCTAssertEqual(decision, .cancel) + } + + func testAllowsInAppNavigation() { + XCTAssertEqual(policy(for: "capacitor://localhost/index.html"), .allow) + } +} diff --git a/ios/package.json b/ios/package.json index 0925b653eb..df820652c7 100644 --- a/ios/package.json +++ b/ios/package.json @@ -1,9 +1,9 @@ { - "name": "@capacitor-plus/ios", - "version": "8.5.2", - "description": "Capacitor+: Enhanced Capacitor with automated upstream sync - Cross-platform apps with JavaScript and the web", - "homepage": "https://capgo.app/docs/plugins/capacitor-plus/", - "author": "Capgo Team (https://capgo.app)", + "name": "@capacitor/ios", + "version": "8.5.1", + "description": "Capacitor: Cross-platform apps with JavaScript and the web", + "homepage": "https://capacitorjs.com", + "author": "Ionic Team (https://ionic.io)", "license": "MIT", "repository": { "type": "git", @@ -25,7 +25,6 @@ "xc:build:CapacitorCordova": "cd CapacitorCordova && xcodebuild && cd .." }, "peerDependencies": { - "@capacitor-plus/core": "^8.5.0", "@capacitor/core": "^8.5.0" }, "publishConfig": { diff --git a/lerna.json b/lerna.json index 06ff51191c..7ea0a73133 100644 --- a/lerna.json +++ b/lerna.json @@ -13,6 +13,6 @@ "tagVersionPrefix": "" } }, - "version": "8.5.2", + "version": "8.5.1", "$schema": "node_modules/lerna/schemas/lerna-schema.json" }