Merge pull request #3384 from vector-im/riot_3137

Warn if link text doesn't match link target
This commit is contained in:
ismailgulek 2020-07-16 18:45:25 +03:00 committed by GitHub
commit 490c002682
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 125 additions and 1 deletions

View file

@ -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).

View file

@ -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 */,

View file

@ -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.";

View file

@ -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")

View file

@ -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)
@ -3156,6 +3194,7 @@
break;
}
}
}
break;
case UITextItemInteractionPresentActions:
{
@ -5938,4 +5977,3 @@
}
@end

View 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
}
}