mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-06-20 18:24:43 +00:00
75ec477248
The only Postbox usage in Sources/ was a `PeerId?` parameter in the public signature of openUserGeneratedUrl. Switch to the EnginePeer.Id typealias (same type as PeerId, so no call sites break) and remove the Postbox import. Add TelegramCore to BUILD deps since EnginePeer lives there; Postbox was not in BUILD (was transitively supplied). Behavior-preserving. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
271 lines
9.6 KiB
Swift
271 lines
9.6 KiB
Swift
import Foundation
|
|
import Display
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
import AccountContext
|
|
import OverlayStatusController
|
|
import UrlWhitelist
|
|
import TelegramPresentationData
|
|
import AlertComponent
|
|
import UrlHandling
|
|
|
|
public func openUserGeneratedUrl(context: AccountContext, peerId: EnginePeer.Id?, url: String, concealed: Bool, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, forceDark: Bool = false, present: @escaping (ViewController) -> Void, openResolved: @escaping (ResolvedUrl) -> Void, progress: Promise<Bool>? = nil, alertDisplayUpdated: ((ViewController?) -> Void)? = nil) -> Disposable {
|
|
var concealed = concealed
|
|
|
|
var presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
if forceDark {
|
|
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
|
}
|
|
|
|
let openImpl: () -> Disposable = {
|
|
let disposable = MetaDisposable()
|
|
var cancelImpl: (() -> Void)?
|
|
let progressSignal: Signal<Never, NoError>
|
|
|
|
if let progress {
|
|
progressSignal = Signal<Never, NoError> { subscriber in
|
|
progress.set(.single(true))
|
|
return ActionDisposable {
|
|
progress.set(.single(false))
|
|
}
|
|
}
|
|
|> runOn(Queue.mainQueue())
|
|
} else {
|
|
progressSignal = Signal<Never, NoError> { subscriber in
|
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
|
cancelImpl?()
|
|
}))
|
|
present(controller)
|
|
return ActionDisposable { [weak controller] in
|
|
Queue.mainQueue().async() {
|
|
controller?.dismiss()
|
|
}
|
|
}
|
|
}
|
|
|> runOn(Queue.mainQueue())
|
|
}
|
|
let progressDisposable = MetaDisposable()
|
|
var didStartProgress = false
|
|
|
|
cancelImpl = {
|
|
disposable.dispose()
|
|
}
|
|
|
|
var resolveSignal: Signal<ResolveUrlResult, NoError>
|
|
resolveSignal = context.sharedContext.resolveUrlWithProgress(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth)
|
|
#if DEBUG
|
|
//resolveSignal = .single(.progress) |> then(resolveSignal |> delay(2.0, queue: .mainQueue()))
|
|
#endif
|
|
|
|
disposable.set((resolveSignal
|
|
|> afterDisposed {
|
|
Queue.mainQueue().async {
|
|
progressDisposable.dispose()
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).start(next: { result in
|
|
switch result {
|
|
case .progress:
|
|
if !didStartProgress {
|
|
didStartProgress = true
|
|
progressDisposable.set(progressSignal.start())
|
|
}
|
|
case let .result(result):
|
|
progressDisposable.dispose()
|
|
openResolved(result)
|
|
}
|
|
}))
|
|
|
|
return ActionDisposable {
|
|
cancelImpl?()
|
|
}
|
|
}
|
|
|
|
let (parsedString, parsedConcealed) = parseUrl(url: url, wasConcealed: concealed)
|
|
concealed = parsedConcealed
|
|
|
|
if let parsedUrl = parseInternalUrl(sharedContext: context.sharedContext, context: context, query: url) {
|
|
if case .proxy = parsedUrl {
|
|
concealed = true
|
|
}
|
|
}
|
|
|
|
if concealed && !skipConcealedAlert {
|
|
var rawDisplayUrl: String = parsedString
|
|
let maxLength = 180
|
|
if rawDisplayUrl.count > maxLength {
|
|
rawDisplayUrl = String(rawDisplayUrl[..<rawDisplayUrl.index(rawDisplayUrl.startIndex, offsetBy: maxLength - 2)]) + "..."
|
|
}
|
|
|
|
var displayUrl = rawDisplayUrl
|
|
displayUrl = displayUrl.replacingOccurrences(of: "\u{202e}", with: "")
|
|
displayUrl = (try? punycodedFullURLString(displayUrl)) ?? displayUrl
|
|
|
|
let disposable = MetaDisposable()
|
|
|
|
let alertController = textAlertController(context: context, forceTheme: forceDark ? presentationData.theme : nil, title: nil, text: presentationData.strings.Generic_OpenHiddenLinkAlert(displayUrl).string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_No, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Yes, action: {
|
|
disposable.set(openImpl())
|
|
})])
|
|
if let alertController = alertController as? AlertScreen {
|
|
alertController.dismissed = { _ in
|
|
alertDisplayUpdated?(nil)
|
|
}
|
|
}
|
|
present(alertController)
|
|
alertDisplayUpdated?(alertController)
|
|
return disposable
|
|
} else {
|
|
return openImpl()
|
|
}
|
|
}
|
|
|
|
private enum PunycodeError: Error {
|
|
case emptyInput
|
|
case overflow
|
|
}
|
|
|
|
private struct Punycode {
|
|
private static let base = 36
|
|
private static let tmin = 1
|
|
private static let tmax = 26
|
|
private static let skew = 38
|
|
private static let damp = 700
|
|
private static let initialBias = 72
|
|
private static let initialN: UInt32 = 128
|
|
private static let delimiter: Character = "-"
|
|
|
|
/// Encodes a single DNS label to punycode WITHOUT "xn--" prefix.
|
|
static func encodeLabel(_ input: String) throws -> String {
|
|
guard !input.isEmpty else { throw PunycodeError.emptyInput }
|
|
|
|
let scalars: [UInt32] = input.unicodeScalars.map { $0.value }
|
|
var output = ""
|
|
|
|
var b = 0
|
|
for cp in scalars where cp < 0x80 {
|
|
output.unicodeScalars.append(UnicodeScalar(cp)!)
|
|
b += 1
|
|
}
|
|
|
|
var h = b
|
|
if b > 0 && h < scalars.count { output.append(delimiter) }
|
|
|
|
var n = initialN
|
|
var delta: UInt32 = 0
|
|
var bias = initialBias
|
|
|
|
func checkedAdd(_ a: UInt32, _ b: UInt32) throws -> UInt32 {
|
|
let (res, overflow) = a.addingReportingOverflow(b)
|
|
if overflow { throw PunycodeError.overflow }
|
|
return res
|
|
}
|
|
func checkedMul(_ a: UInt32, _ b: UInt32) throws -> UInt32 {
|
|
let (res, overflow) = a.multipliedReportingOverflow(by: b)
|
|
if overflow { throw PunycodeError.overflow }
|
|
return res
|
|
}
|
|
|
|
while h < scalars.count {
|
|
var m: UInt32 = UInt32.max
|
|
for cp in scalars where cp >= n {
|
|
if cp < m { m = cp }
|
|
}
|
|
|
|
let hm1 = UInt32(h + 1)
|
|
delta = try checkedAdd(delta, try checkedMul(m - n, hm1))
|
|
n = m
|
|
|
|
for cp in scalars {
|
|
if cp < n {
|
|
delta = try checkedAdd(delta, 1)
|
|
} else if cp == n {
|
|
var q = delta
|
|
var k = base
|
|
|
|
while true {
|
|
let t: UInt32
|
|
if k <= bias { t = UInt32(tmin) }
|
|
else if k >= bias + tmax { t = UInt32(tmax) }
|
|
else { t = UInt32(k - bias) }
|
|
|
|
if q < t { break }
|
|
|
|
let digit = Int(t + (q - t) % UInt32(base - Int(t)))
|
|
output.append(encodeDigit(digit))
|
|
q = (q - t) / UInt32(base - Int(t))
|
|
k += base
|
|
}
|
|
|
|
output.append(encodeDigit(Int(q)))
|
|
bias = adapt(delta: delta, numPoints: h + 1, firstTime: h == b)
|
|
delta = 0
|
|
h += 1
|
|
}
|
|
}
|
|
|
|
delta = try checkedAdd(delta, 1)
|
|
n = try checkedAdd(n, 1)
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
/// Encodes a domain name label-by-label; prefixes "xn--" only when needed.
|
|
static func encodeDomain(_ domain: String) throws -> String {
|
|
let normalized = domain.precomposedStringWithCanonicalMapping
|
|
|
|
return try normalized
|
|
.split(separator: ".", omittingEmptySubsequences: false)
|
|
.map { piece -> String in
|
|
let label = String(piece)
|
|
if label.isEmpty { return label } // keep empty labels as-is (rare)
|
|
if label.unicodeScalars.allSatisfy({ $0.isASCII }) { return label }
|
|
return "xn--" + (try encodeLabel(label))
|
|
}
|
|
.joined(separator: ".")
|
|
}
|
|
|
|
private static func encodeDigit(_ d: Int) -> Character {
|
|
precondition((0..<36).contains(d))
|
|
if d < 26 { return Character(UnicodeScalar(UInt8(97 + d))) } // a-z
|
|
else { return Character(UnicodeScalar(UInt8(48 + (d - 26)))) } // 0-9
|
|
}
|
|
|
|
private static func adapt(delta: UInt32, numPoints: Int, firstTime: Bool) -> Int {
|
|
var d = Int(delta)
|
|
d = firstTime ? d / damp : d / 2
|
|
d += d / numPoints
|
|
|
|
var k = 0
|
|
while d > ((base - tmin) * tmax) / 2 {
|
|
d /= (base - tmin)
|
|
k += base
|
|
}
|
|
|
|
return k + (base - tmin + 1) * d / (d + skew)
|
|
}
|
|
}
|
|
|
|
private enum URLPunycodeError: Error {
|
|
case invalidURL
|
|
}
|
|
|
|
/// Converts a full URL string to an ASCII-safe form:
|
|
/// - punycodes the host (IDN) using RFC 3492 punycode
|
|
/// - leaves scheme/port/user/pass intact
|
|
/// - lets URLComponents percent-encode path/query/fragment as needed
|
|
private func punycodedFullURLString(_ input: String) throws -> String {
|
|
guard var comps = URLComponents(string: input) else {
|
|
throw URLPunycodeError.invalidURL
|
|
}
|
|
|
|
// If there is a host, encode it. If it's a relative URL, host may be nil.
|
|
if let host = comps.host, !host.isEmpty {
|
|
comps.host = try Punycode.encodeDomain(host)
|
|
}
|
|
|
|
// URLComponents.string can be nil for some malformed component combinations
|
|
if let out = comps.string { return out }
|
|
throw URLPunycodeError.invalidURL
|
|
}
|