feat(ios): improve share preview (#15538)

#### PR Dependency Tree

* **PR #15538** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added rich link previews to mobile and iOS sharing, including images,
metadata, transcripts, and selected text.
* Share imports can now create structured content blocks, embeds,
bookmarks, and transcript callouts.
* Added workspace-aware preview handling for cloud, self-hosted, and
signed-out modes.
* **Accessibility**
* Improved collapse/expand controls with semantic buttons and ARIA
relationships.
* **Bug Fixes**
  * Enhanced URL and error sanitization in server logs.
* Improved link-preview CORS support, validation, and request handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-08-28 03:32:26 +08:00
committed by GitHub
parent 329839e467
commit b6de0ad51b
36 changed files with 3152 additions and 372 deletions
@@ -1,6 +1,91 @@
import XCTest
private final class SharePreviewURLProtocol: URLProtocol {
static var onStart: ((URLProtocol, URLRequest) -> Void)?
static var onStop: (() -> Void)?
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() { Self.onStart?(self, request) }
override func stopLoading() { Self.onStop?() }
}
final class ShareInboxSafetyTests: XCTestCase {
func testManifestTitleIgnoresPreviewAndOnlyAcceptsExplicitEdits() {
let originalTitle = "Original Safari title"
let serverPreviewTitle = "Untrusted server preview title"
XCTAssertEqual(
ShareInboxSafety.manifestTitle(original: originalTitle, userEdited: nil),
originalTitle
)
XCTAssertNotEqual(
ShareInboxSafety.manifestTitle(original: originalTitle, userEdited: nil),
serverPreviewTitle
)
XCTAssertEqual(
ShareInboxSafety.manifestTitle(
original: originalTitle,
userEdited: " My explicit title "
),
"My explicit title"
)
XCTAssertEqual(
ShareInboxSafety.previewTitle(
original: originalTitle, userEdited: nil, serverTitle: serverPreviewTitle),
serverPreviewTitle
)
XCTAssertEqual(
ShareInboxSafety.previewTitle(
original: originalTitle, userEdited: "My explicit title", serverTitle: serverPreviewTitle),
"My explicit title"
)
XCTAssertEqual(
ShareInboxSafety.manifestTitle(original: originalTitle, userEdited: nil),
originalTitle
)
}
func testShareExtensionActivationAcceptsSupportedRepresentationsAmongExtraAttachments() throws {
let plistURL = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("ShareExtension/Info.plist")
let plist = try PropertyListSerialization.propertyList(
from: Data(contentsOf: plistURL),
format: nil
) as? [String: Any]
let extensionDictionary = plist?["NSExtension"] as? [String: Any]
let attributes = extensionDictionary?["NSExtensionAttributes"] as? [String: Any]
let rule = try XCTUnwrap(attributes?["NSExtensionActivationRule"] as? String)
XCTAssertTrue(rule.contains("public.url"))
XCTAssertTrue(rule.contains("public.text"))
XCTAssertTrue(rule.contains("public.image"))
XCTAssertTrue(rule.contains("com.apple.property-list"))
XCTAssertTrue(rule.contains(".@count > 0"))
XCTAssertFalse(rule.contains("TRUEPREDICATE"))
let predicate = NSPredicate(format: rule)
let youtubePayload: [String: Any] = [
"extensionItems": [[
"attachments": [
["registeredTypeIdentifiers": ["public.url", "public.data"]],
["registeredTypeIdentifiers": ["com.google.youtube.extra"]],
]
]]
]
let unsupportedPayload: [String: Any] = [
"extensionItems": [[
"attachments": [[
"registeredTypeIdentifiers": ["com.adobe.pdf", "public.movie"]
]]
]]
]
XCTAssertTrue(predicate.evaluate(with: youtubePayload))
XCTAssertFalse(predicate.evaluate(with: unsupportedPayload))
}
func testManifestIDsMustBeUUIDs() {
let id = UUID().uuidString
XCTAssertEqual(ShareInboxSafety.normalizedManifestID(id.lowercased()), id)
@@ -24,4 +109,201 @@ final class ShareInboxSafetyTests: XCTestCase {
)
XCTAssertNil(ShareInboxSafety.detectRasterImageMimeType(Data("<svg/>".utf8)))
}
func testPreviewRouteMatrixAndAllowlistBypasses() {
let publicURLs = [
"https://x.com/affine/status/123",
"https://www.twitter.com/affine/status/123",
"https://youtu.be/video-id",
"https://www.youtube.com/watch?v=video-id",
"https://m.youtube.com/shorts/video-id",
]
for mode in [ShareWorkspaceMode.selfHostedPresent, .cloudOnly, .signedOut, .unknown] {
for url in publicURLs {
XCTAssertEqual(ShareInboxSafety.previewRoute(mode: mode, url: url), .official)
}
}
let genericURL = "https://example.com/private"
XCTAssertEqual(ShareInboxSafety.previewRoute(mode: .selfHostedPresent, url: genericURL), .deferred)
XCTAssertEqual(ShareInboxSafety.previewRoute(mode: .unknown, url: genericURL), .deferred)
XCTAssertEqual(ShareInboxSafety.previewRoute(mode: .cloudOnly, url: genericURL), .official)
XCTAssertEqual(ShareInboxSafety.previewRoute(mode: .signedOut, url: genericURL), .official)
for bypass in [
"https://evil.x.com/affine/status/123",
"https://x.com/affine/status/not-a-number",
"https://x.com/affine/status/123/extra",
"https://youtube.com.evil.example/watch?v=video-id",
"https://www.youtube.com/channel/video-id",
"https://youtu.be/video-id/extra",
] {
XCTAssertFalse(ShareInboxSafety.isOfficialPreviewURL(bypass), bypass)
}
}
func testWorkspaceModeSnapshotFailsClosed() throws {
XCTAssertEqual(ShareInboxSafety.workspaceMode(from: nil), .unknown)
XCTAssertEqual(ShareInboxSafety.workspaceMode(from: Data("invalid".utf8)), .unknown)
let incompatible = Data(
"{\"mode\":\"cloudOnly\",\"schemaVersion\":2,\"updatedAt\":\"2026-08-27T00:00:00Z\"}".utf8
)
XCTAssertEqual(ShareInboxSafety.workspaceMode(from: incompatible), .unknown)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let now = Date(timeIntervalSince1970: 1_800_000_000)
let current = try encoder.encode(
ShareWorkspaceModeSnapshot(mode: .selfHostedPresent, updatedAt: now)
)
XCTAssertEqual(ShareInboxSafety.workspaceMode(from: current, now: now), .selfHostedPresent)
XCTAssertEqual(
ShareInboxSafety.workspaceMode(from: current, now: now.addingTimeInterval(24 * 60 * 60 + 1)),
.unknown
)
}
func testOldManifestDefaultsToConservativeRouteAndOriginalURLSurvives() throws {
let id = UUID().uuidString
let oldManifest = """
{
"id":"\(id)",
"documentId":"\(UUID().uuidString)",
"createdAt":"2026-08-27T00:00:00Z",
"title":"Original",
"content":{"kind":"url","url":"https://example.com/original?token=value"},
"attachments":[]
}
"""
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let item = try decoder.decode(ShareInboxItem.self, from: Data(oldManifest.utf8))
XCTAssertNil(item.previewRoute)
XCTAssertEqual(item.previewRoute ?? .deferred, .deferred)
XCTAssertEqual(item.content.url, "https://example.com/original?token=value")
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let encoded = try encoder.encode(item)
XCTAssertEqual(try decoder.decode(ShareInboxItem.self, from: encoded).content.url, item.content.url)
}
func testPreviewAndImageRequestsCarryHeadersAndCanBeCancelled() async throws {
let family = "👨‍👩‍👧"
let transcript = ShareLinkPreview.Transcript(
language: nil,
segments: [
.init(text: " Hello\n\tworld ", startSeconds: nil, durationSeconds: nil, speaker: nil),
.init(text: "again", startSeconds: nil, durationSeconds: nil, speaker: nil),
],
chapters: nil,
truncated: nil
)
XCTAssertEqual(transcript.previewText, "Hello world again")
let longTranscript = ShareLinkPreview.Transcript(
language: nil,
segments: [
.init(
text: String(repeating: family, count: 241), startSeconds: nil,
durationSeconds: nil, speaker: nil)
],
chapters: nil,
truncated: nil
)
XCTAssertEqual(longTranscript.previewText?.count, 241)
XCTAssertTrue(longTranscript.previewText?.hasSuffix("") == true)
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [SharePreviewURLProtocol.self]
let client = ShareLinkPreviewClient(
session: URLSession(configuration: configuration), appVersion: "0.27.0")
let started = expectation(description: "request started")
let stopped = expectation(description: "request cancelled")
SharePreviewURLProtocol.onStart = { _, request in
XCTAssertEqual(request.value(forHTTPHeaderField: "User-Agent"), "AFFiNE/0.27.0")
XCTAssertEqual(request.value(forHTTPHeaderField: "x-affine-version"), "0.27.0")
started.fulfill()
}
SharePreviewURLProtocol.onStop = { stopped.fulfill() }
defer {
SharePreviewURLProtocol.onStart = nil
SharePreviewURLProtocol.onStop = nil
}
let task = Task {
try await client.fetch(url: "https://www.youtube.com/watch?v=video-id")
}
await fulfillment(of: [started], timeout: 1)
task.cancel()
do {
_ = try await task.value
XCTFail("Cancelled preview unexpectedly completed")
} catch {
let urlError = error as? URLError
XCTAssertTrue(error is CancellationError || urlError?.code == .cancelled)
}
await fulfillment(of: [stopped], timeout: 1)
let imageData = try XCTUnwrap(
Data(
base64Encoded:
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
)
)
let imageLoaded = expectation(description: "image loaded")
SharePreviewURLProtocol.onStart = { protocolInstance, request in
XCTAssertEqual(request.httpMethod, "GET")
XCTAssertEqual(request.url?.absoluteString, "https://app.affine.pro/api/worker/image-proxy")
XCTAssertEqual(request.value(forHTTPHeaderField: "User-Agent"), "AFFiNE/0.27.0")
XCTAssertEqual(request.value(forHTTPHeaderField: "x-affine-version"), "0.27.0")
let response = HTTPURLResponse(
url: request.url!, statusCode: 200, httpVersion: nil,
headerFields: ["Content-Type": "image/png"]
)!
protocolInstance.client?.urlProtocol(
protocolInstance, didReceive: response, cacheStoragePolicy: .notAllowed)
protocolInstance.client?.urlProtocol(protocolInstance, didLoad: imageData)
protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance)
imageLoaded.fulfill()
}
SharePreviewURLProtocol.onStop = nil
_ = try await client.fetchImage(url: "/api/worker/image-proxy")
await fulfillment(of: [imageLoaded], timeout: 1)
SharePreviewURLProtocol.onStart = { protocolInstance, request in
let response = HTTPURLResponse(
url: request.url!, statusCode: 403, httpVersion: nil, headerFields: nil)!
protocolInstance.client?.urlProtocol(
protocolInstance, didReceive: response, cacheStoragePolicy: .notAllowed)
protocolInstance.client?.urlProtocolDidFinishLoading(protocolInstance)
}
do {
_ = try await client.fetchImage(url: "/api/worker/image-proxy")
XCTFail("Failed image response unexpectedly decoded")
} catch {
XCTAssertEqual((error as? URLError)?.code, .badServerResponse)
}
let imageStarted = expectation(description: "image request started")
let imageStopped = expectation(description: "image request cancelled")
SharePreviewURLProtocol.onStart = { _, request in
XCTAssertEqual(request.value(forHTTPHeaderField: "User-Agent"), "AFFiNE/0.27.0")
XCTAssertEqual(request.value(forHTTPHeaderField: "x-affine-version"), "0.27.0")
imageStarted.fulfill()
}
SharePreviewURLProtocol.onStop = { imageStopped.fulfill() }
let imageTask = Task {
try await client.fetchImage(url: "/api/worker/image-proxy")
}
await fulfillment(of: [imageStarted], timeout: 1)
imageTask.cancel()
do {
_ = try await imageTask.value
XCTFail("Cancelled image request unexpectedly completed")
} catch {
let urlError = error as? URLError
XCTAssertTrue(error is CancellationError || urlError?.code == .cancelled)
}
await fulfillment(of: [imageStopped], timeout: 1)
}
}