mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
ae55a4090c
* refactor(canvas): retire legacy host and commands * refactor(apple): narrow shared Canvas contracts * refactor(macos): keep Canvas as widget presenter * refactor(ios): remove Canvas client * refactor(android): remove Canvas client * refactor(linux): remove Canvas client * fix(ci): isolate native locale artifacts * fix(linux): regenerate companion lockfile * fix(canvas): refresh native tool display metadata * test(canvas): align coverage with presenter surface * test(canvas): remove obsolete asset root seam * test(canvas): stabilize retirement CI coverage * refactor(swift): remove orphaned resource wrapper * test(ios): remove retired canvas layout assertion * fix(macos): reserve retired canvas command namespace * refactor(macos): isolate canvas command policy * fix(canvas): select only eligible macOS panels * fix(canvas): keep panel selection plugin-owned
117 lines
4.3 KiB
Swift
117 lines
4.3 KiB
Swift
import Foundation
|
|
|
|
enum CanvasHostedURLResolver {
|
|
private static let canvasPath = "/__openclaw__/canvas"
|
|
|
|
static func resolve(surfaceURL rawSurfaceURL: String?, target rawTarget: String) -> URL? {
|
|
guard let target = relativeHostedTarget(rawTarget),
|
|
var surface = capabilitySurface(rawSurfaceURL)
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
var surfacePath = surface.percentEncodedPath
|
|
while surfacePath.hasSuffix("/") {
|
|
surfacePath.removeLast()
|
|
}
|
|
surface.percentEncodedPath = surfacePath + target.percentEncodedPath
|
|
surface.percentEncodedQuery = target.percentEncodedQuery
|
|
surface.fragment = target.fragment
|
|
return surface.url
|
|
}
|
|
|
|
static func isHostedTarget(_ rawTarget: String) -> Bool {
|
|
self.relativeHostedTarget(rawTarget) != nil
|
|
}
|
|
|
|
static func isAppLocalTarget(_ rawTarget: String) -> Bool {
|
|
let target = rawTarget.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard let components = URLComponents(string: target),
|
|
components.scheme?.lowercased() == CanvasScheme.scheme,
|
|
components.host?.isEmpty == false,
|
|
components.user == nil,
|
|
components.password == nil,
|
|
components.port == nil,
|
|
components.percentEncodedPath.isEmpty ||
|
|
isCanonicalHostedPath(components.percentEncodedPath)
|
|
else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private static func relativeHostedTarget(_ rawTarget: String) -> URLComponents? {
|
|
let target = rawTarget.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard target.hasPrefix("/"),
|
|
let components = URLComponents(string: target),
|
|
components.scheme == nil,
|
|
components.host == nil,
|
|
components.user == nil,
|
|
components.password == nil,
|
|
isCanonicalHostedPath(components.percentEncodedPath),
|
|
isCanvasPath(components.percentEncodedPath)
|
|
else {
|
|
return nil
|
|
}
|
|
return components
|
|
}
|
|
|
|
private static func capabilitySurface(_ rawSurfaceURL: String?) -> URLComponents? {
|
|
let raw = rawSurfaceURL?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
guard !raw.isEmpty,
|
|
let components = URLComponents(string: raw),
|
|
isWebURL(components),
|
|
components.user == nil,
|
|
components.password == nil,
|
|
components.percentEncodedQuery == nil,
|
|
components.fragment == nil
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
let segments = components.percentEncodedPath.split(separator: "/", omittingEmptySubsequences: true)
|
|
guard segments.count >= 3,
|
|
segments[segments.count - 3] == "__openclaw__",
|
|
segments[segments.count - 2] == "cap",
|
|
let capability = String(segments[segments.count - 1]).removingPercentEncoding,
|
|
!capability.isEmpty
|
|
else {
|
|
return nil
|
|
}
|
|
return components
|
|
}
|
|
|
|
private static func isWebURL(_ components: URLComponents) -> Bool {
|
|
let scheme = components.scheme?.lowercased()
|
|
return (scheme == "http" || scheme == "https") && components.host?.isEmpty == false
|
|
}
|
|
|
|
private static func isCanonicalHostedPath(_ path: String) -> Bool {
|
|
let segments = path.split(separator: "/", omittingEmptySubsequences: false)
|
|
guard segments.first?.isEmpty == true else { return false }
|
|
|
|
for (index, encodedSegment) in segments.enumerated() {
|
|
if index == 0 || (index == segments.count - 1 && encodedSegment.isEmpty) {
|
|
continue
|
|
}
|
|
guard !encodedSegment.isEmpty else { return false }
|
|
var segment = String(encodedSegment)
|
|
while true {
|
|
guard let decoded = segment.removingPercentEncoding else { return false }
|
|
if decoded == segment {
|
|
break
|
|
}
|
|
segment = decoded
|
|
}
|
|
if segment == "." || segment == ".." || segment.contains("/") || segment.contains("\\") {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
private static func isCanvasPath(_ path: String) -> Bool {
|
|
path == self.canvasPath || path.hasPrefix("\(self.canvasPath)/")
|
|
}
|
|
}
|