From c4e104939634bbca429e7cc9b3eac0661f7476ea Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 27 Apr 2026 01:36:16 +0200 Subject: [PATCH] Various fixes --- .../Sources/ChatController.swift | 14 ++- .../ChatMessagePollBubbleContentNode.swift | 8 ++ .../Sources/ChatHistoryListNode.swift | 12 +- .../WebUI/Sources/WebAppController.swift | 3 +- submodules/WebUI/Sources/WebAppWebView.swift | 104 ++++++++++++++---- 5 files changed, 113 insertions(+), 28 deletions(-) diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 7e723704f1..7b1781e32a 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -67,6 +67,8 @@ public final class ChatMessageItemAssociatedData: Equatable { public let isSuspiciousPeer: Bool public let showTextAsPlaceholder: Bool public let accountCountry: String? + public let isParticipant: Bool + public let invitedOn: Int32? public init( automaticDownloadPeerType: MediaAutoDownloadPeerType, @@ -104,7 +106,9 @@ public final class ChatMessageItemAssociatedData: Equatable { showSensitiveContent: Bool = false, isSuspiciousPeer: Bool = false, showTextAsPlaceholder: Bool = false, - accountCountry: String? = nil + accountCountry: String? = nil, + isParticipant: Bool = false, + invitedOn: Int32? = nil ) { self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadPeerId = automaticDownloadPeerId @@ -142,6 +146,8 @@ public final class ChatMessageItemAssociatedData: Equatable { self.isSuspiciousPeer = isSuspiciousPeer self.showTextAsPlaceholder = showTextAsPlaceholder self.accountCountry = accountCountry + self.isParticipant = isParticipant + self.invitedOn = invitedOn } public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool { @@ -241,6 +247,12 @@ public final class ChatMessageItemAssociatedData: Equatable { if lhs.accountCountry != rhs.accountCountry { return false } + if lhs.isParticipant != rhs.isParticipant { + return false + } + if lhs.invitedOn != rhs.invitedOn { + return false + } return true } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 817dfb74ca..d7cd526a60 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -2690,6 +2690,14 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if !poll.countries.isEmpty, let accountCountry = item.associatedData.accountCountry, !poll.countries.contains(accountCountry) { isRestricted = true } + if poll.restrictToSubscribers { + let period: Int32 = item.context.account.testingEnvironment ? 5 * 60 : 24 * 60 * 60 + if !item.associatedData.isParticipant { + isRestricted = true + } else if let invitedOn = item.associatedData.invitedOn, invitedOn + period > Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) { + isRestricted = true + } + } orderedPollOptions = resolvedOptionOrder(for: item) diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index a36e3ea1f1..089a3a00d6 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -373,6 +373,8 @@ private func extractAssociatedData( var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var contactsPeerIds: Set = Set() var channelDiscussionGroup: ChatMessageItemAssociatedData.ChannelDiscussionGroupStatus = .unknown + var isParticipant = false + var invitedOn: Int32? if case let .peer(peerId) = chatLocation { automaticDownloadPeerId = peerId @@ -396,11 +398,15 @@ private func extractAssociatedData( } else if peerId.namespace == Namespaces.Peer.CloudChannel { for entry in view.additionalData { if case let .peer(_, value) = entry { - if let channel = value as? TelegramChannel, case .group = channel.info { - automaticMediaDownloadPeerType = .group + if let channel = value as? TelegramChannel { + if case .group = channel.info { + automaticMediaDownloadPeerType = .group + } + isParticipant = channel.participationStatus == .member } } else if case let .cachedPeerData(dataPeerId, cachedData) = entry, dataPeerId == peerId { if let cachedData = cachedData as? CachedChannelData { + invitedOn = cachedData.invitedOn switch cachedData.linkedDiscussionPeerId { case let .known(value): channelDiscussionGroup = .known(value) @@ -422,7 +428,7 @@ private func extractAssociatedData( automaticDownloadPeerId = message.peerId } - return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, areStarReactionsEnabled: areStarReactionsEnabled, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, isSuspiciousPeer: isSuspiciousPeer, accountCountry: accountCountry) + return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, preferredStoryHighQuality: preferredStoryHighQuality, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: savedMessageTags, defaultReaction: defaultReaction, areStarReactionsEnabled: areStarReactionsEnabled, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId, recommendedChannels: recommendedChannels, audioTranscriptionTrial: audioTranscriptionTrial, chatThemes: chatThemes, deviceContactsNumbers: deviceContactsNumbers, isInline: isInline, showSensitiveContent: showSensitiveContent, isSuspiciousPeer: isSuspiciousPeer, accountCountry: accountCountry, isParticipant: isParticipant, invitedOn: invitedOn) } private extension ChatHistoryLocationInput { diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 2da05a1503..a72f287f7a 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -516,6 +516,7 @@ public final class WebAppController: ViewController, AttachmentContainable { return } #endif*/ + self.webView?.bindTrustedOrigin(from: url) self.webView?.load(URLRequest(url: url)) } @@ -1092,7 +1093,7 @@ public final class WebAppController: ViewController, AttachmentContainable { guard let controller = self.controller else { return } - guard message.frameInfo.isMainFrame else { + guard self.webView?.isTrustedMainFrameMessage(message) == true else { return } guard let body = message.body as? [String: Any] else { diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index a7fb70770c..f722ace6e7 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -36,11 +36,27 @@ private class WebViewTouchGestureRecognizer: UITapGestureRecognizer { } } -private let eventProxySource = "var TelegramWebviewProxyProto = function() {}; " + - "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + - "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + - "}; " + -"var TelegramWebviewProxy = new TelegramWebviewProxyProto();" +private func jsStringLiteral(_ value: String) -> String { + if let data = try? JSONSerialization.data(withJSONObject: [value], options: []), let string = String(data: data, encoding: .utf8), string.hasPrefix("["), string.hasSuffix("]") { + return String(string.dropFirst().dropLast()) + } + return "\"\"" +} + +private func eventProxySource(trustedOrigin: String) -> String { + return """ + (function() { + if (window.location.origin !== \(jsStringLiteral(trustedOrigin))) { + return; + } + var TelegramWebviewProxyProto = function() {}; + TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { + window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); + }; + window.TelegramWebviewProxy = new TelegramWebviewProxyProto(); + })(); + """ +} private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea):not([\"contenteditable\"=\"true\"]){-webkit-user-select:none;}';" + " var head = document.head || document.getElementsByTagName('head')[0];" @@ -91,6 +107,7 @@ function tgBrowserDisconnectObserver() { final class WebAppWebView: WKWebView { var handleScriptMessage: (WKScriptMessage) -> Void = { _ in } + private(set) var trustedOrigin: String? var customInsets: UIEdgeInsets = .zero { didSet { @@ -134,8 +151,6 @@ final class WebAppWebView: WKWebView { let contentController = WKUserContentController() var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? - let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: true) - contentController.addUserScript(eventProxyScript) contentController.add(WeakGameScriptMessageHandler { message in handleScriptMessageImpl?(message) }, name: "performAction") @@ -187,6 +202,36 @@ final class WebAppWebView: WKWebView { print() } + func bindTrustedOrigin(from url: URL) { + guard self.trustedOrigin == nil else { + return + } + guard let origin = normalizedOrigin(url: url) else { + return + } + + self.trustedOrigin = origin + + let eventProxyScript = WKUserScript(source: eventProxySource(trustedOrigin: origin), injectionTime: .atDocumentStart, forMainFrameOnly: true) + self.configuration.userContentController.addUserScript(eventProxyScript) + } + + func isTrustedMainFrameMessage(_ message: WKScriptMessage) -> Bool { + guard message.frameInfo.isMainFrame else { + return false + } + guard let trustedOrigin = self.trustedOrigin else { + return false + } + guard message.frameInfo.securityOriginString == trustedOrigin else { + return false + } + if let currentOrigin = self.origin, currentOrigin != trustedOrigin { + return false + } + return true + } + override func didMoveToSuperview() { super.didMoveToSuperview() @@ -223,6 +268,9 @@ final class WebAppWebView: WKWebView { } func sendEvent(name: String, data: String?) { + guard let trustedOrigin = self.trustedOrigin, self.origin == trustedOrigin else { + return + } let script = "window.TelegramGameProxy && window.TelegramGameProxy.receiveEvent && window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data ?? "null"))" self.evaluateJavaScript(script, completionHandler: { _, _ in }) @@ -279,29 +327,39 @@ final class WebAppWebView: WKWebView { } var origin: String? { - guard let url = self.url, let scheme = url.scheme, let host = url.host else { + guard let url = self.url else { return nil } - let port = url.port - var origin = "\(scheme)://\(host)" - if let port { - origin += ":\(port)" - } - return origin + return normalizedOrigin(url: url) } } extension WKFrameInfo { var securityOriginString: String { let securityOrigin = self.securityOrigin - var origin = "" - origin.append(securityOrigin.protocol) - origin.append("://") - origin.append(securityOrigin.host) - if securityOrigin.port != 0 { - origin.append(":") - origin.append("\(securityOrigin.port)") - } - return origin + return normalizedOrigin(scheme: securityOrigin.protocol, host: securityOrigin.host, port: securityOrigin.port == 0 ? nil : securityOrigin.port) ?? "" + } +} + +private func normalizedOrigin(url: URL) -> String? { + return normalizedOrigin(scheme: url.scheme, host: url.host, port: url.port) +} + +private func normalizedOrigin(scheme: String?, host: String?, port: Int?) -> String? { + guard let scheme = scheme?.lowercased(), !scheme.isEmpty, let host = host?.lowercased(), !host.isEmpty else { + return nil + } + + let includePort: Bool + if let port { + includePort = !(scheme == "http" && port == 80) && !(scheme == "https" && port == 443) + } else { + includePort = false + } + + if includePort, let port { + return "\(scheme)://\(host):\(port)" + } else { + return "\(scheme)://\(host)" } }