From dadbeca766f53c8cf2fe66dafe27f3b13443dfd5 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 4 Oct 2022 10:49:26 +0300 Subject: [PATCH] Include browser version in parsed user agent (PSG-761) (#6788) * Update UserSessionInfo structure to include client version * Add string for browser * Update user agent parser to parse browser version too * Add browser row into the session details * Add changelog * Fix tests * Run Swift format --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++ .../UserSessions/Common/UserAgentParser.swift | 48 ++++++++++++----- .../UserSessions/Common/UserSessionInfo.swift | 7 ++- .../Common/View/UserSessionCardView.swift | 3 +- .../MockUserSessionDetailsScreenState.swift | 6 ++- .../UserSessionDetailsViewModelTests.swift | 6 ++- .../UserSessionDetailsViewModel.swift | 6 +++ .../MockUserSessionOverviewScreenState.swift | 6 ++- .../UserSessionOverviewViewModelTests.swift | 5 +- .../MatrixSDK/UserSessionsDataProvider.swift | 4 +- .../UserSessionsDataProviderProtocol.swift | 2 +- .../UserSessionsOverviewService.swift | 3 +- .../MockUserSessionsOverviewService.swift | 12 +++-- RiotTests/UserAgentParserTests.swift | 54 ++++++++++--------- changelog.d/pr-6788.change | 1 + 16 files changed, 109 insertions(+), 59 deletions(-) create mode 100644 changelog.d/pr-6788.change diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 8cf427e044..cb141e966d 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2411,6 +2411,7 @@ To enable access, tap Settings> Location and select Always"; "user_session_details_device_ip_address" = "IP address"; "user_session_details_device_ip_location" = "IP location"; "user_session_details_device_model" = "Model"; +"user_session_details_device_browser" = "Browser"; "user_session_details_device_os" = "Operating System"; "user_session_details_application_name" = "Name"; "user_session_details_application_version" = "Version"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 2e02e09541..f8c0cc121c 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -8499,6 +8499,10 @@ public class VectorL10n: NSObject { public static var userSessionDetailsApplicationVersion: String { return VectorL10n.tr("Vector", "user_session_details_application_version") } + /// Browser + public static var userSessionDetailsDeviceBrowser: String { + return VectorL10n.tr("Vector", "user_session_details_device_browser") + } /// IP address public static var userSessionDetailsDeviceIpAddress: String { return VectorL10n.tr("Vector", "user_session_details_device_ip_address") diff --git a/RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift b/RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift index d4fe29466e..2c0a10487b 100644 --- a/RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/UserAgentParser.swift @@ -125,18 +125,28 @@ enum UserAgentParser { // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36 private static func parseDesktop(_ userAgent: String) -> UserAgent { var deviceOS: String? - let browserName = browserName(for: userAgent) + let browserInfo = browserInfo(for: userAgent) if let deviceInfo = findFirstDeviceInfo(in: userAgent) { let deviceInfoComponents = deviceInfo.components(separatedBy: "; ") - deviceOS = deviceInfoComponents[safe: 1]?.hasPrefix("Android") == true ? deviceInfoComponents[safe: 1] : deviceInfoComponents.first + if deviceInfoComponents[safe: 1]?.hasPrefix("Android") == true { + deviceOS = deviceInfoComponents[safe: 1] + } else if deviceInfoComponents.first == "Macintosh" { + var osFull = deviceInfoComponents[safe: 1] + osFull = osFull?.replacingOccurrences(of: "Intel ", with: "") + osFull = osFull?.replacingOccurrences(of: "Mac OS X", with: "macOS") + osFull = osFull?.replacingOccurrences(of: "_", with: ".") + deviceOS = osFull + } else { + deviceOS = deviceInfoComponents.first + } } return UserAgent(deviceType: .desktop, - deviceModel: browserName, + deviceModel: nil, deviceOS: deviceOS, - clientName: nil, - clientVersion: nil) + clientName: browserInfo.name, + clientVersion: browserInfo.version) } // Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 @@ -164,20 +174,30 @@ enum UserAgentParser { return nil } - private static func browserName(for userAgent: String) -> String? { + private static func browserInfo(for userAgent: String) -> (name: String?, version: String?) { let components = userAgent.components(separatedBy: " ") if components.last?.hasPrefix("Firefox") == true { - return "Firefox" - } else if components.last?.hasPrefix("Safari") == true - && components[safe:components.count - 2]?.hasPrefix("Mobile") == true { + let version = components.last?.components(separatedBy: "/").last + return ("Firefox", version) + } else if components.last?.hasPrefix("Safari") == true, + components[safe: components.count - 2]?.hasPrefix("Mobile") == true { // mobile browser - let possibleBrowserName = components[safe:components.count - 3]?.components(separatedBy: "/").first - return possibleBrowserName == "Version" ? "Safari" : possibleBrowserName - } else if components.last?.hasPrefix("Safari") == true && components[safe:components.count - 2]?.hasPrefix("Version") == true { - return "Safari" + let possibleBrowserComponents = components[safe: components.count - 3] + if possibleBrowserComponents?.hasPrefix("Version") == true { + let version = possibleBrowserComponents?.components(separatedBy: "/").last + return ("Safari", version) + } else { + let components = possibleBrowserComponents?.components(separatedBy: "/") + return (components?.first, components?.last) + } + } else if components.last?.hasPrefix("Safari") == true, components[safe: components.count - 2]?.hasPrefix("Version") == true { + let version = components[safe: components.count - 2]?.components(separatedBy: "/").last + return ("Safari", version) } else { // regular browser - return components[safe:components.count - 2]?.components(separatedBy: "/").first + let browserComponent = components[safe: components.count - 2] + let components = browserComponent?.components(separatedBy: "/") + return (components?.first, components?[safe: 1]) } } } diff --git a/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift index 5bfbe2332c..79fe49ec25 100644 --- a/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/UserSessionInfo.swift @@ -58,8 +58,11 @@ struct UserSessionInfo: Identifiable { /// Last seen IP location let lastSeenIPLocation: String? - /// Device name - let deviceName: String? + /// Client name + let clientName: String? + + /// Client version + let clientVersion: String? /// True to indicate that session has been used under `inactiveSessionDurationTreshold` value let isActive: Bool diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift index 212434309f..8fa03b02c1 100644 --- a/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/View/UserSessionCardView.swift @@ -150,7 +150,8 @@ struct UserSessionCardViewPreview: View { deviceModel: nil, deviceOS: "iOS 15.5", lastSeenIPLocation: nil, - deviceName: "My iPhone", + clientName: "Element", + clientVersion: "1.0.0", isActive: true, isCurrent: isCurrent) viewData = UserSessionCardViewData(sessionInfo: sessionInfo) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift index 37233e141b..90fa84a36d 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift @@ -53,7 +53,8 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable { deviceModel: nil, deviceOS: "iOS 15.5", lastSeenIPLocation: nil, - deviceName: "My iPhone", + clientName: "Element", + clientVersion: "1.0.0", isActive: true, isCurrent: true) case .sessionSectionOnly: @@ -69,7 +70,8 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable { deviceModel: nil, deviceOS: "Android 4.0", lastSeenIPLocation: nil, - deviceName: "My Phone", + clientName: "Element", + clientVersion: "1.0.0", isActive: true, isCurrent: false) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift index 184725e1ec..b07751c100 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift @@ -113,7 +113,8 @@ class UserSessionDetailsViewModelTests: XCTestCase { deviceModel: String? = nil, deviceOS: String? = nil, lastSeenIPLocation: String? = nil, - deviceName: String? = nil, + clientName: String? = nil, + clientVersion: String? = nil, isActive: Bool = true, isCurrent: Bool = true) -> UserSessionInfo { UserSessionInfo(id: id, @@ -128,7 +129,8 @@ class UserSessionDetailsViewModelTests: XCTestCase { deviceModel: deviceModel, deviceOS: deviceOS, lastSeenIPLocation: lastSeenIPLocation, - deviceName: deviceName, + clientName: clientName, + clientVersion: clientVersion, isActive: isActive, isCurrent: isCurrent) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift index 475f3bb5fe..f08ecdd8e4 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift @@ -93,6 +93,12 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceModel, value: model)) } + if session.deviceType == .web, + let clientName = session.clientName, + let clientVersion = session.clientVersion { + deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceBrowser, + value: "\(clientName) \(clientVersion)")) + } if let deviceOS = session.deviceOS { deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceOs, value: deviceOS)) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift index aa505c2441..97c7ebc356 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/MockUserSessionOverviewScreenState.swift @@ -60,7 +60,8 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable { deviceModel: nil, deviceOS: "iOS 15.5", lastSeenIPLocation: nil, - deviceName: "My iPhone", + clientName: "Element", + clientVersion: "1.0.0", isActive: true, isCurrent: true) service = MockUserSessionOverviewService() @@ -77,7 +78,8 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable { deviceModel: nil, deviceOS: "macOS 12.5.1", lastSeenIPLocation: nil, - deviceName: "My Mac", + clientName: "Electron", + clientVersion: "20.1.1", isActive: false, isCurrent: false) service = MockUserSessionOverviewService() diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift index 416b0e0328..a2d0e78078 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift @@ -93,13 +93,14 @@ class UserSessionOverviewViewModelTests: XCTestCase { isVerified: false, lastSeenIP: "10.0.0.10", lastSeenTimestamp: Date().timeIntervalSince1970 - 100, - applicationName: "Element", + applicationName: "Element iOS", applicationVersion: "1.9.7", applicationURL: nil, deviceModel: "iPhone XS", deviceOS: "iOS 15.5", lastSeenIPLocation: nil, - deviceName: "Mobile", + clientName: "Element", + clientVersion: "1.9.7", isActive: true, isCurrent: true) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift index 7ab1f5a04f..a8d36cc4e7 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProvider.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,7 +44,7 @@ class UserSessionsDataProvider: UserSessionsDataProviderProtocol { session.crypto.device(withDeviceId: deviceId, ofUser: userId) } - func accountData(for eventType: String) -> [AnyHashable : Any]? { + func accountData(for eventType: String) -> [AnyHashable: Any]? { session.accountData.accountData(forEventType: eventType) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProviderProtocol.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProviderProtocol.swift index ca65e218e4..e97310a40e 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProviderProtocol.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsDataProviderProtocol.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2022 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift index a84cd7d443..273072ea71 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/MatrixSDK/UserSessionsOverviewService.swift @@ -143,7 +143,8 @@ extension UserSessionInfo { deviceModel: userAgent?.deviceModel, deviceOS: userAgent?.deviceOS, lastSeenIPLocation: nil, - deviceName: userAgent?.clientName, + clientName: userAgent?.clientName, + clientVersion: userAgent?.clientVersion, isActive: isActive, isCurrent: isCurrent) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift index 00d90238b8..a376077866 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift @@ -89,7 +89,8 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol { deviceModel: nil, deviceOS: "iOS 15.5", lastSeenIPLocation: nil, - deviceName: "My iPhone", + clientName: "Element", + clientVersion: "1.0.0", isActive: true, isCurrent: true) } @@ -107,7 +108,8 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol { deviceModel: nil, deviceOS: "macOS 12.5.1", lastSeenIPLocation: nil, - deviceName: "My Mac", + clientName: "Electron", + clientVersion: "20.0.0", isActive: active, isCurrent: false), UserSessionInfo(id: "2 verified: \(verified) active: \(active)", @@ -122,7 +124,8 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol { deviceModel: nil, deviceOS: "Windows 10", lastSeenIPLocation: nil, - deviceName: "My Windows", + clientName: "Firefox", + clientVersion: "39.0", isActive: active, isCurrent: false), UserSessionInfo(id: "3 verified: \(verified) active: \(active)", @@ -137,7 +140,8 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol { deviceModel: nil, deviceOS: "Android 4.0", lastSeenIPLocation: nil, - deviceName: "My Phone", + clientName: "Element", + clientVersion: "1.0.0", isActive: active, isCurrent: false)] } diff --git a/RiotTests/UserAgentParserTests.swift b/RiotTests/UserAgentParserTests.swift index d68306d671..e268deda6f 100644 --- a/RiotTests/UserAgentParserTests.swift +++ b/RiotTests/UserAgentParserTests.swift @@ -121,15 +121,15 @@ class UserAgentParserTests: XCTestCase { let expected = [ UserAgent(deviceType: .desktop, - deviceModel: "Electron", - deviceOS: "Macintosh", - clientName: nil, - clientVersion: nil), + deviceModel: nil, + deviceOS: "macOS 10.15.7", + clientName: "Electron", + clientVersion: "20.1.1"), UserAgent(deviceType: .desktop, - deviceModel: "Electron", + deviceModel: nil, deviceOS: "Windows NT 10.0", - clientName: nil, - clientVersion: nil) + clientName: "Electron", + clientVersion: "20.1.1") ] XCTAssertEqual(userAgents, expected) @@ -147,30 +147,30 @@ class UserAgentParserTests: XCTestCase { let expected = [ UserAgent(deviceType: .web, - deviceModel: "Chrome", - deviceOS: "Macintosh", - clientName: nil, - clientVersion: nil), + deviceModel: nil, + deviceOS: "macOS 10.15.7", + clientName: "Chrome", + clientVersion: "104.0.5112.102"), UserAgent(deviceType: .web, - deviceModel: "Chrome", + deviceModel: nil, deviceOS: "Windows NT 10.0", - clientName: nil, - clientVersion: nil), + clientName: "Chrome", + clientVersion: "104.0.5112.102"), UserAgent(deviceType: .web, - deviceModel: "Firefox", - deviceOS: "Macintosh", - clientName: nil, - clientVersion: nil), + deviceModel: nil, + deviceOS: "macOS 10.10", + clientName: "Firefox", + clientVersion: "39.0"), UserAgent(deviceType: .web, - deviceModel: "Safari", - deviceOS: "Macintosh", - clientName: nil, - clientVersion: nil), + deviceModel: nil, + deviceOS: "macOS 10.10.2", + clientName: "Safari", + clientVersion: "8.0.3"), UserAgent(deviceType: .web, - deviceModel: "Chrome", + deviceModel: nil, deviceOS: "Android 9", - clientName: nil, - clientVersion: nil) + clientName: "Chrome", + clientVersion: "69.0.3497.100") ] XCTAssertEqual(userAgents, expected) @@ -181,7 +181,8 @@ class UserAgentParserTests: XCTestCase { "Element (iPhone X; OS 15.2; 3.00)", "Element/1.9.9; iOS", "Element/1.9.7 Android", - "Element/1.9.9; iOS " + "some random string", + "Element/1.9.9; iOS ", ] let userAgents = uaStrings.map { UserAgentParser.parse($0) } @@ -189,6 +190,7 @@ class UserAgentParserTests: XCTestCase { .unknown, .unknown, .unknown, + .unknown, UserAgent(deviceType: .mobile, deviceModel: nil, deviceOS: nil, diff --git a/changelog.d/pr-6788.change b/changelog.d/pr-6788.change new file mode 100644 index 0000000000..99f00337ff --- /dev/null +++ b/changelog.d/pr-6788.change @@ -0,0 +1 @@ +User session details: Include browser version for web sessions (PSG-761).