Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/docs/guides/core-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,24 @@ builder.RegisterCustomSchemeHandler("app", (sender, scheme, url, out string? con
- Additional handlers can be added after `Build()` via `window.RegisterCustomSchemeHandler(...)`.
- Scheme names are lowercased automatically.

### CORS and same-origin policy

Custom scheme responses automatically include CORS headers when the request originates from the same origin (same scheme, host, and port). This allows `fetch()` and `XMLHttpRequest` to work without disabling web security.

**Same-origin behavior** (e.g., `app://localhost` page fetching `app://localhost/data.json`):
- `Access-Control-Allow-Origin: app://localhost`
- `Access-Control-Allow-Credentials: true`
- `Vary: Origin`

**Cross-origin behavior** (e.g., `https://example.com` page fetching `app://localhost/data.json`):
- No CORS headers are added
- The browser engine may block the request entirely depending on web security settings

**Platform notes:**
- **Windows (WebView2):** CORS headers are built via `BuildCustomSchemeResponseHeaders` and set on the `ICoreWebView2WebResourceResponse`. The `app` scheme is registered with `TreatAsSecure(TRUE)` and `HasAuthorityComponent(TRUE)`.
- **Linux (WebKitGTK):** The `app` scheme is registered as CORS-enabled via `webkit_security_manager_register_uri_scheme_as_cors_enabled()`. WebKitGTK handles CORS header injection natively.
- **macOS (WKWebView):** CORS headers are built in the `UrlSchemeHandler` delegate using the same `IsSameOrigin` logic as Windows.

## Dialogs

InfiniFrame exposes the native OS dialog system.
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/migration/photino-breaking-changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ InfiniFrameNative_ShowSaveFile(title, defaultPath, filters, count, defaultFileNa
| [photino.native #141](https://github.com/tryphotino/photino.native/issues/141) | Stack overflow in `WaitForExit` on Linux | Per-window independent message loops; no shared global `MessageLoopState` lock |
| [photino.NET #75](https://github.com/tryphotino/photino.NET/issues/75) | `RegisterWindowClosingHandler` does not fire on Linux | Closing handler rewritten using the GTK `delete-event` signal correctly |
| [photino.NET #257](https://github.com/tryphotino/photino.NET/issues/257) | `SetLogVerbosity(0)` still logs a message | Integer verbosity removed entirely and replaced by `ILogger<IInfiniFrameWindow>` |
| [photino.NET #232](https://github.com/tryphotino/photino.NET/issues/232) | Custom scheme handlers break `fetch`/`XHR` (CORS interference) | Scheme handler registration refactored; CORS headers handled correctly per platform |
| [photino.NET #232](https://github.com/tryphotino/photino.NET/issues/232) | Custom scheme handlers break `fetch`/`XHR` (CORS interference) | Scheme handler registration refactored; CORS headers reflected for same-origin requests; integration tests verify fetch/XHR + header correctness across platforms |
| [photino.native #175](https://github.com/tryphotino/photino.native/issues/175) | `SetTopmost` uses wrong Win32 style; `null` crash on Linux | Fixed Win32 `HWND_TOPMOST`/`HWND_NOTOPMOST` usage; `null` guards added on Linux |

## Removed or Replaced Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ internal static InfiniFrameNativeInteropStatus ShowOpenFolder(IntPtr instance, s
internal static InfiniFrameNativeInteropStatus ShowSaveFile(IntPtr instance, string title, string defaultPath, string[] filters, int filtersCount, string? defaultFileName, out string? value) {
InfiniFrameNativeInteropStatus status = ShowSaveFilePtr(instance, title, defaultPath, filters, filtersCount, defaultFileName, out IntPtr ptrValue);
try {
value = PtrToNativeString(ptrValue);
value = MarshalNativeToString(ptrValue);
}
finally {
if (ptrValue != IntPtr.Zero) {
Expand Down Expand Up @@ -161,7 +161,7 @@ internal static partial InfiniFrameNativeInteropStatus CancelDialog(
string?[] values = new string?[count];
Marshal.Copy(valuesPtr, ptrArray, 0, count);
for (int i = 0; i < count; i++) {
values[i] = PtrToNativeString(ptrArray[i]);
values[i] = MarshalNativeToString(ptrArray[i]);
}

return values;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public partial class InfiniFrameNative {
if (status != InfiniFrameNativeInteropStatus.Success || ptr == IntPtr.Zero) return null;

try {
return PtrToNativeString(ptr);
return MarshalNativeToString(ptr);
}
finally {
FreeString(ptr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public partial class InfiniFrameNative {
return null;

try {
return PtrToNativeString(ptr);
return MarshalNativeToString(ptr);
}
finally {
FreeString(ptr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ public partial class InfiniFrameNative {
internal static InfiniFrameNativeInteropStatus GetUserAgent(IntPtr instance, out string? userAgent) {
InfiniFrameNativeInteropStatus status = GetUserAgentPtr(instance, out IntPtr ptr);
try {
userAgent = PtrToNativeString(ptr);
userAgent = MarshalNativeToString(ptr);
}
finally {
if (ptr != IntPtr.Zero) {
Expand All @@ -307,7 +307,7 @@ internal static InfiniFrameNativeInteropStatus GetUserAgent(IntPtr instance, out
internal static InfiniFrameNativeInteropStatus GetTitle(IntPtr instance, out string? title) {
InfiniFrameNativeInteropStatus status = GetTitlePtr(instance, out IntPtr ptr);
try {
title = PtrToNativeString(ptr);
title = MarshalNativeToString(ptr);
}
finally {
if (ptr != IntPtr.Zero) {
Expand All @@ -333,7 +333,7 @@ internal static InfiniFrameNativeInteropStatus GetTitle(IntPtr instance, out str
internal static InfiniFrameNativeInteropStatus GetIconFileName(IntPtr instance, out string iconFileName) {
InfiniFrameNativeInteropStatus status = GetIconFileNamePtr(instance, out IntPtr ptr);
try {
iconFileName = PtrToNativeString(ptr) ?? string.Empty;
iconFileName = MarshalNativeToString(ptr) ?? string.Empty;
}
finally {
if (ptr != IntPtr.Zero) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ IntPtr completionContext
internal static InfiniFrameNativeInteropStatus GetCurrentUrl(IntPtr instance, out string? url) {
InfiniFrameNativeInteropStatus status = GetCurrentUrlPtr(instance, out IntPtr ptr);
try {
url = PtrToNativeString(ptr);
url = MarshalNativeToString(ptr);
}
finally {
if (ptr != IntPtr.Zero) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public partial class InfiniFrameNative {
/// </summary>
/// <param name="ptr">The native string pointer.</param>
/// <returns>The managed string, or <c>null</c> if the pointer is zero.</returns>
public static string? PtrToNativeString(IntPtr ptr) {
public static string? MarshalNativeToString(IntPtr ptr) {
if (ptr == IntPtr.Zero) return null;

return OperatingSystem.IsWindows()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// ---------------------------------------------------------------------------------------------------------------------
// Imports
// ---------------------------------------------------------------------------------------------------------------------
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace InfiniFrame.NativeBridge;
// ---------------------------------------------------------------------------------------------------------------------
// Code
// ---------------------------------------------------------------------------------------------------------------------
public static partial class InfiniFrameNativeTesting {

[LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNativeTests_ParseOrigin", SetLastError = true)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
private static partial InfiniFrameNativeInteropStatus ParseOriginNative(
IntPtr value,
out IntPtr scheme,
out IntPtr host,
out IntPtr port,
out int valid
);

[LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNativeTests_IsSameOrigin", SetLastError = true)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
private static partial InfiniFrameNativeInteropStatus IsSameOriginNative(
IntPtr left,
IntPtr right,
out int result
);

[LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNativeTests_BuildHeaders", SetLastError = true)]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
private static partial InfiniFrameNativeInteropStatus BuildHeadersNative(
IntPtr contentType,
IntPtr resourceUri,
IntPtr requestOrigin,
out IntPtr headers
);

internal static InfiniFrameNativeInteropStatus ParseOrigin(
string value,
out IntPtr scheme,
out IntPtr host,
out IntPtr port,
out int valid
) {
IntPtr valuePtr = MarshalStringToNative(value);
try {
return ParseOriginNative(valuePtr, out scheme, out host, out port, out valid);
}
finally {
MarshalFreeNativeString(valuePtr);
}
}

internal static InfiniFrameNativeInteropStatus IsSameOrigin(
string left,
string right,
out int result
) {
IntPtr leftPtr = MarshalStringToNative(left);
IntPtr rightPtr = MarshalStringToNative(right);
try {
return IsSameOriginNative(leftPtr, rightPtr, out result);
}
finally {
MarshalFreeNativeString(leftPtr);
MarshalFreeNativeString(rightPtr);
}
}

internal static InfiniFrameNativeInteropStatus BuildHeaders(
string contentType,
string resourceUri,
string requestOrigin,
out IntPtr headers
) {
IntPtr contentTypePtr = MarshalStringToNative(contentType);
IntPtr resourceUriPtr = MarshalStringToNative(resourceUri);
IntPtr requestOriginPtr = MarshalStringToNative(requestOrigin);
try {
return BuildHeadersNative(contentTypePtr, resourceUriPtr, requestOriginPtr, out headers);
}
finally {
MarshalFreeNativeString(contentTypePtr);
MarshalFreeNativeString(resourceUriPtr);
MarshalFreeNativeString(requestOriginPtr);
}
}

internal static InfiniFrameNativeInteropStatus FreeTestString(IntPtr value)
=> InfiniFrameNative.FreeString(value);

private static IntPtr MarshalStringToNative(string? value) {
if (value == null) return IntPtr.Zero;
return OperatingSystem.IsWindows()
? Marshal.StringToHGlobalUni(value)
: Marshal.StringToHGlobalAnsi(value);
}

private static void MarshalFreeNativeString(IntPtr ptr) {
if (ptr != IntPtr.Zero) Marshal.FreeHGlobal(ptr);
}
}
1 change: 1 addition & 0 deletions src/InfiniFrame.NativeBridge/Native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ set(COMMON_SOURCES

set(TEST_SOURCES
src/Api/Testing/Exports.Tests.cpp
src/Api/Testing/Exports.CustomSchemeResponseTests.cpp
)

set(WINDOWS_SOURCES
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// ---------------------------------------------------------------------------------------------------------------------
// Imports
// ---------------------------------------------------------------------------------------------------------------------
#include "Runtime/Shared/Window/InfiniFrame.h"
#include "Api/Exports/Exports.h"
#include "Runtime/Shared/WebView/CustomSchemeResponse.h"
// ---------------------------------------------------------------------------------------------------------------------
// Code
// ---------------------------------------------------------------------------------------------------------------------
#if defined(INFINIFRAME_BUILD_TEST_EXPORTS)

namespace {
template <typename CharT>
infiniframe::ParsedOrigin<CharT> CallParseOrigin(const CharT* value) {
return infiniframe::ParseOrigin<CharT>(std::basic_string<CharT>(value));
}

template <typename CharT>
bool CallIsSameOrigin(const CharT* left, const CharT* right) {
return infiniframe::IsSameOrigin<CharT>(
std::basic_string<CharT>(left),
std::basic_string<CharT>(right));
}

template <typename CharT>
AutoString CallBuildHeaders(
const CharT* contentType,
const CharT* resourceUri,
const CharT* requestOrigin
) {
std::basic_string<CharT> result = infiniframe::BuildCustomSchemeResponseHeaders<CharT>(
std::basic_string<CharT>(contentType),
std::basic_string<CharT>(resourceUri),
std::basic_string<CharT>(requestOrigin));
return AllocateStringCopy(result);
}
}

extern "C" {

EXPORTED InteropStatus InfiniFrameNativeTests_ParseOrigin(
AutoStringConst value,
AutoString* scheme,
AutoString* host,
AutoString* port,
int* valid
) {
if (scheme != nullptr) *scheme = nullptr;
if (host != nullptr) *host = nullptr;
if (port != nullptr) *port = nullptr;
if (valid != nullptr) *valid = 0;

return RunExportStatus([&] {
if (!EnsureNotNull(value, "value") ||
!EnsureNotNull(scheme, "scheme", ::InteropStatus::OutParameterSetToInvalidNull) ||
!EnsureNotNull(host, "host", ::InteropStatus::OutParameterSetToInvalidNull) ||
!EnsureNotNull(port, "port", ::InteropStatus::OutParameterSetToInvalidNull) ||
!EnsureNotNull(valid, "valid", ::InteropStatus::OutParameterSetToInvalidNull)) {
return;
}

auto result = CallParseOrigin(value);
*valid = result.Valid ? 1 : 0;
if (result.Valid) {
*scheme = AllocateStringCopy(result.Scheme);
*host = AllocateStringCopy(result.Host);
*port = AllocateStringCopy(result.Port);
}
});
}

EXPORTED InteropStatus InfiniFrameNativeTests_IsSameOrigin(
AutoStringConst left,
AutoStringConst right,
int* result
) {
if (result != nullptr) *result = 0;

return RunExportStatus([&] {
if (!EnsureNotNull(left, "left") ||
!EnsureNotNull(right, "right") ||
!EnsureNotNull(result, "result", ::InteropStatus::OutParameterSetToInvalidNull)) {
return;
}

*result = CallIsSameOrigin(left, right) ? 1 : 0;
});
}

EXPORTED InteropStatus InfiniFrameNativeTests_BuildHeaders(
AutoStringConst contentType,
AutoStringConst resourceUri,
AutoStringConst requestOrigin,
AutoString* headers
) {
if (headers != nullptr) *headers = nullptr;

return RunExportStatus([&] {
if (!EnsureNotNull(contentType, "contentType") ||
!EnsureNotNull(resourceUri, "resourceUri") ||
!EnsureNotNull(requestOrigin, "requestOrigin") ||
!EnsureNotNull(headers, "headers", ::InteropStatus::OutParameterSetToInvalidNull)) {
return;
}

*headers = CallBuildHeaders(contentType, resourceUri, requestOrigin);
});
}

}

#endif
Loading