mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge pull request #3384 from vector-im/riot_3137
Warn if link text doesn't match link target
This commit is contained in:
commit
490c002682
6 changed files with 125 additions and 1 deletions
|
@ -8,6 +8,7 @@ Improvements:
|
|||
* Strings: Use you instead of display name on notice events (#3282).
|
||||
* Third-party licences: Add license for FlowCommoniOS (#3415).
|
||||
* Lazy-loading: Remove lazy loading labs setting, enable it by default (#3389).
|
||||
* Room: Show alert if link text does not match link target (#3137).
|
||||
|
||||
Bug fix:
|
||||
* Xcode11: Fix content change error when dragging start chat page (PR #3075).
|
||||
|
|
|
@ -795,6 +795,7 @@
|
|||
ECB101322477CFDB00CF8C11 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB1012E2477CFDB00CF8C11 /* UIDevice.swift */; };
|
||||
ECB101332477CFDB00CF8C11 /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB1012F2477CFDB00CF8C11 /* UITableViewCell.swift */; };
|
||||
ECB101362477D00700CF8C11 /* UniversalLink.m in Sources */ = {isa = PBXBuildFile; fileRef = ECB101352477D00700CF8C11 /* UniversalLink.m */; };
|
||||
ECDC15F224AF41D2003437CF /* FormattedBodyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDC15F124AF41D2003437CF /* FormattedBodyParser.swift */; };
|
||||
F05927C91FDED836009F2A68 /* MXGroup+Riot.m in Sources */ = {isa = PBXBuildFile; fileRef = F05927C71FDED835009F2A68 /* MXGroup+Riot.m */; };
|
||||
F083BD1E1E7009ED00A9B29C /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F083BB0D1E7009EC00A9B29C /* AppDelegate.m */; };
|
||||
F083BDE61E7009ED00A9B29C /* busy.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = F083BBDB1E7009EC00A9B29C /* busy.mp3 */; };
|
||||
|
@ -1881,6 +1882,7 @@
|
|||
ECB1012F2477CFDB00CF8C11 /* UITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableViewCell.swift; sourceTree = "<group>"; };
|
||||
ECB101342477D00700CF8C11 /* UniversalLink.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UniversalLink.h; sourceTree = "<group>"; };
|
||||
ECB101352477D00700CF8C11 /* UniversalLink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UniversalLink.m; sourceTree = "<group>"; };
|
||||
ECDC15F124AF41D2003437CF /* FormattedBodyParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyParser.swift; sourceTree = "<group>"; };
|
||||
F05927C71FDED835009F2A68 /* MXGroup+Riot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MXGroup+Riot.m"; sourceTree = "<group>"; };
|
||||
F05927C81FDED835009F2A68 /* MXGroup+Riot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MXGroup+Riot.h"; sourceTree = "<group>"; };
|
||||
F083BB031E7005FD00A9B29C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
|
@ -4807,6 +4809,7 @@
|
|||
B1DB4F0D22316FFF0065DBFA /* UserNameColorGenerator.swift */,
|
||||
B197B7C5243DE947005ABBF3 /* EncryptionTrustLevelBadgeImageHelper.swift */,
|
||||
EC2B4EF024A1EEBD005EB739 /* DataProtectionHelper.swift */,
|
||||
ECDC15F124AF41D2003437CF /* FormattedBodyParser.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
|
@ -5822,6 +5825,7 @@
|
|||
B1B557B420EF5AEF00210D55 /* EventDetailsView.m in Sources */,
|
||||
EC85D751247C0E8F002C44C9 /* UNUserNotificationCenter.swift in Sources */,
|
||||
B1BEE73823DF44A60003A4CB /* UserVerificationSessionsStatusViewAction.swift in Sources */,
|
||||
ECDC15F224AF41D2003437CF /* FormattedBodyParser.swift in Sources */,
|
||||
B1B5577E20EE84BF00210D55 /* IncomingCallView.m in Sources */,
|
||||
B1CE83E22422817200D07506 /* KeyVerificationVerifyBySASViewState.swift in Sources */,
|
||||
B1DCC62822E60CE300625807 /* EmojiCategory.swift in Sources */,
|
||||
|
|
|
@ -366,6 +366,9 @@
|
|||
"media_type_accessibility_file" = "File";
|
||||
"media_type_accessibility_sticker" = "Sticker";
|
||||
|
||||
"external_link_confirmation_title" = "Double-check this link";
|
||||
"external_link_confirmation_message" = "The link %@ is taking you to another site: %@\n\nAre you sure you want to continue?";
|
||||
|
||||
// Unknown devices
|
||||
"unknown_devices_alert_title" = "Room contains unknown sessions";
|
||||
"unknown_devices_alert" = "This room contains unknown sessions which have not been verified.\nThis means there is no guarantee that the sessions belong to the users they claim to.\nWe recommend you go through the verification process for each session before continuing, but you can resend the message without verifying if you prefer.";
|
||||
|
|
|
@ -1122,6 +1122,14 @@ internal enum VectorL10n {
|
|||
internal static func eventFormatterWidgetRemovedByYou(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "event_formatter_widget_removed_by_you", p1)
|
||||
}
|
||||
/// The link %@ is taking you to another site: %@\n\nAre you sure you want to continue?
|
||||
internal static func externalLinkConfirmationMessage(_ p1: String, _ p2: String) -> String {
|
||||
return VectorL10n.tr("Vector", "external_link_confirmation_message", p1, p2)
|
||||
}
|
||||
/// Double-check this link
|
||||
internal static var externalLinkConfirmationTitle: String {
|
||||
return VectorL10n.tr("Vector", "external_link_confirmation_title")
|
||||
}
|
||||
/// File upload
|
||||
internal static var fileUploadErrorTitle: String {
|
||||
return VectorL10n.tr("Vector", "file_upload_error_title")
|
||||
|
|
|
@ -214,6 +214,9 @@
|
|||
|
||||
// Homeserver notices
|
||||
MXServerNotices *serverNotices;
|
||||
|
||||
// Formatted body parser for events
|
||||
FormattedBodyParser *formattedBodyParser;
|
||||
}
|
||||
|
||||
@property (nonatomic, weak) IBOutlet UIView *overlayContainerView;
|
||||
|
@ -291,6 +294,7 @@
|
|||
// Setup `MXKViewControllerHandling` properties
|
||||
self.enableBarTintColorStatusChange = NO;
|
||||
self.rageShakeManager = [RageShakeManager sharedManager];
|
||||
formattedBodyParser = [FormattedBodyParser new];
|
||||
|
||||
_showExpandedHeader = NO;
|
||||
_showMissedDiscussionsBadge = YES;
|
||||
|
@ -3145,6 +3149,40 @@
|
|||
shouldDoAction = NO;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
MXEvent *tappedEvent = userInfo[kMXKRoomBubbleCellEventKey];
|
||||
NSString *format = tappedEvent.content[@"format"];
|
||||
NSString *formattedBody = tappedEvent.content[@"formatted_body"];
|
||||
// if an html formatted body exists
|
||||
if ([format isEqualToString:kMXRoomMessageFormatHTML] && formattedBody)
|
||||
{
|
||||
NSURL *visibleURL = [formattedBodyParser getVisibleURLForURL:url inFormattedBody:formattedBody];
|
||||
|
||||
if (visibleURL && ![url isEqual:visibleURL])
|
||||
{
|
||||
// urls are different, show confirmation alert
|
||||
NSString *formatStr = NSLocalizedStringFromTable(@"external_link_confirmation_message", @"Vector", nil);
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"external_link_confirmation_title", @"Vector", nil) message:[NSString stringWithFormat:formatStr, visibleURL.absoluteString, url.absoluteString] preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *continueAction = [UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"continue", @"Vector", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
|
||||
// Try to open the link
|
||||
[[UIApplication sharedApplication] vc_open:url completionHandler:^(BOOL success) {
|
||||
if (!success)
|
||||
{
|
||||
[self showUnableToOpenLinkErrorAlert];
|
||||
}
|
||||
}];
|
||||
}];
|
||||
|
||||
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedStringFromTable(@"cancel", @"Vector", nil) style:UIAlertActionStyleCancel handler:nil];
|
||||
|
||||
[alert addAction:continueAction];
|
||||
[alert addAction:cancelAction];
|
||||
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
// Try to open the link
|
||||
[[UIApplication sharedApplication] vc_open:url completionHandler:^(BOOL success) {
|
||||
if (!success)
|
||||
|
@ -3154,6 +3192,7 @@
|
|||
}];
|
||||
shouldDoAction = NO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -5938,4 +5977,3 @@
|
|||
}
|
||||
|
||||
@end
|
||||
|
||||
|
|
70
Riot/Utils/FormattedBodyParser.swift
Normal file
70
Riot/Utils/FormattedBodyParser.swift
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Copyright 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
@objcMembers
|
||||
final class FormattedBodyParser: NSObject {
|
||||
|
||||
private struct HTMLURLAnchor {
|
||||
let link: URL
|
||||
let content: String
|
||||
}
|
||||
|
||||
private enum Constants {
|
||||
static let htmlURLAnchorTagRegexPattern = "<a href=\"(.*?)\">([^<]*)</a>"
|
||||
}
|
||||
|
||||
private lazy var htmlURLAnchorRegex: NSRegularExpression? = {
|
||||
return try? NSRegularExpression(pattern: Constants.htmlURLAnchorTagRegexPattern, options: .caseInsensitive)
|
||||
}()
|
||||
|
||||
private func getHTMLURLAnchors(forURL url: URL, inFormattedBody formattedBody: String) -> [HTMLURLAnchor] {
|
||||
// Use regex here self.htmlURLAnchorRegex
|
||||
// build and return list with `HTMLURLAnchor`
|
||||
|
||||
guard let regex = htmlURLAnchorRegex else {
|
||||
return []
|
||||
}
|
||||
|
||||
return regex.matches(in: formattedBody, options: [], range: NSRange(formattedBody.startIndex..., in: formattedBody)).compactMap { (result) -> HTMLURLAnchor? in
|
||||
guard result.numberOfRanges > 2 else { return nil }
|
||||
guard let urlRange = Range(result.range(at: 1), in: formattedBody) else {
|
||||
return nil
|
||||
}
|
||||
let urlString = String(formattedBody[urlRange])
|
||||
guard let contentRange = Range(result.range(at: 2), in: formattedBody) else {
|
||||
return nil
|
||||
}
|
||||
let content = String(formattedBody[contentRange])
|
||||
// ignore invalid urls
|
||||
guard let link = URL(string: urlString) else { return nil }
|
||||
// ignore other links
|
||||
guard link == url else { return nil }
|
||||
return HTMLURLAnchor(link: link, content: content)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets visible url for a given url. Assumes formattedBody has one or more links like: '<a href="https://example.com/given">https://example.com/visible</a>'
|
||||
/// - Parameter url: the url given as target
|
||||
/// - Parameter formattedBody: html formatted body
|
||||
/// - Returns: visible url if found, otherwise nil
|
||||
func getVisibleURL(forURL url: URL, inFormattedBody formattedBody: String) -> URL? {
|
||||
// TODO: returning first link here. Get url range in formattedBody to detect which link is actually tapped.
|
||||
return self.getHTMLURLAnchors(forURL: url, inFormattedBody: formattedBody).compactMap { URL(string: $0.content) }.first
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue