From 988dbc8149db2f780687118af813915a48c1f5ca Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 9 Oct 2023 15:31:52 +0600 Subject: [PATCH 1/9] Add new browser import sources --- DuckDuckGo.xcodeproj/project.pbxproj | 46 ++++++++++- DuckDuckGo/Common/Localizables/UserText.swift | 2 +- .../Bookmarks/HTML/BookmarkHTMLReader.swift | 7 ++ DuckDuckGo/DataImport/ChromePreferences.swift | 8 +- DuckDuckGo/DataImport/DataImport.swift | 79 ++++++++++++------- .../DataImport/Logins/CSV/CSVImporter.swift | 3 +- .../Chromium/ChromiumDataImporter.swift | 14 +++- .../Logins/Chromium/CoccocDataImporter.swift | 41 ++++++++++ .../Logins/Chromium/OperaDataImporter.swift | 41 ++++++++++ .../Logins/Chromium/VivaldiDataImporter.swift | 41 ++++++++++ .../Logins/Chromium/YandexDataImporter.swift | 41 ++++++++++ .../Logins/Firefox/FirefoxDataImporter.swift | 2 +- DuckDuckGo/DataImport/ThirdPartyBrowser.swift | 45 ++++++++++- .../BrowserImportMoreInfoViewController.swift | 4 +- .../View/BrowserImportViewController.swift | 5 +- .../View/DataImportViewController.swift | 19 ++++- .../View/FileImportViewController.swift | 10 +-- DuckDuckGo/DuckDuckGoAppStore.entitlements | 8 ++ DuckDuckGo/Statistics/PixelArguments.swift | 7 ++ .../DataImport/BrowserProfileTests.swift | 6 +- 20 files changed, 371 insertions(+), 58 deletions(-) create mode 100644 DuckDuckGo/DataImport/Logins/Chromium/CoccocDataImporter.swift create mode 100644 DuckDuckGo/DataImport/Logins/Chromium/OperaDataImporter.swift create mode 100644 DuckDuckGo/DataImport/Logins/Chromium/VivaldiDataImporter.swift create mode 100644 DuckDuckGo/DataImport/Logins/Chromium/YandexDataImporter.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index fd50c1d44d..452866361a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3520,6 +3520,18 @@ B6C2C9EF276081AB005B7F0A /* DeallocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C2C9EE276081AB005B7F0A /* DeallocationTests.swift */; }; B6C2C9F62760B659005B7F0A /* TestDataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B6C2C9F42760B659005B7F0A /* TestDataModel.xcdatamodeld */; }; B6C416A7294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C416A6294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift */; }; + B6C8CA9D2AD010CF0060E1CD /* OperaDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CA9C2AD010CF0060E1CD /* OperaDataImporter.swift */; }; + B6C8CA9E2AD010CF0060E1CD /* OperaDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CA9C2AD010CF0060E1CD /* OperaDataImporter.swift */; }; + B6C8CA9F2AD010CF0060E1CD /* OperaDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CA9C2AD010CF0060E1CD /* OperaDataImporter.swift */; }; + B6C8CAA02AD010CF0060E1CD /* OperaDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CA9C2AD010CF0060E1CD /* OperaDataImporter.swift */; }; + B6C8CAA22AD010D70060E1CD /* VivaldiDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CAA12AD010D70060E1CD /* VivaldiDataImporter.swift */; }; + B6C8CAA32AD010D70060E1CD /* VivaldiDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CAA12AD010D70060E1CD /* VivaldiDataImporter.swift */; }; + B6C8CAA42AD010D70060E1CD /* VivaldiDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CAA12AD010D70060E1CD /* VivaldiDataImporter.swift */; }; + B6C8CAA52AD010D70060E1CD /* VivaldiDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CAA12AD010D70060E1CD /* VivaldiDataImporter.swift */; }; + B6C8CAA72AD010DD0060E1CD /* YandexDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CAA62AD010DD0060E1CD /* YandexDataImporter.swift */; }; + B6C8CAA82AD010DD0060E1CD /* YandexDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CAA62AD010DD0060E1CD /* YandexDataImporter.swift */; }; + B6C8CAA92AD010DD0060E1CD /* YandexDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CAA62AD010DD0060E1CD /* YandexDataImporter.swift */; }; + B6C8CAAA2AD010DD0060E1CD /* YandexDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C8CAA62AD010DD0060E1CD /* YandexDataImporter.swift */; }; B6CA4824298CDC2E0067ECCE /* AdClickAttributionTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CA4823298CDC2E0067ECCE /* AdClickAttributionTabExtensionTests.swift */; }; B6CA4825298CE4B70067ECCE /* AdClickAttributionTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CA4823298CDC2E0067ECCE /* AdClickAttributionTabExtensionTests.swift */; }; B6D574B429472253008ED1B6 /* FBProtectionTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D574B329472253008ED1B6 /* FBProtectionTabExtension.swift */; }; @@ -3544,6 +3556,10 @@ B6E1491129A5C30A00AAFBE8 /* FBProtectionTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D574B329472253008ED1B6 /* FBProtectionTabExtension.swift */; }; B6E319382953446000DD3BCF /* Assertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E319372953446000DD3BCF /* Assertions.swift */; }; B6E61EE3263AC0C8004E11AB /* FileManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E61EE2263AC0C8004E11AB /* FileManagerExtension.swift */; }; + B6E951412AD3B3370036C46F /* CoccocDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E951402AD3B3370036C46F /* CoccocDataImporter.swift */; }; + B6E951422AD3B3370036C46F /* CoccocDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E951402AD3B3370036C46F /* CoccocDataImporter.swift */; }; + B6E951432AD3B3370036C46F /* CoccocDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E951402AD3B3370036C46F /* CoccocDataImporter.swift */; }; + B6E951442AD3B3370036C46F /* CoccocDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E951402AD3B3370036C46F /* CoccocDataImporter.swift */; }; B6EC37DE29B5D05A001ACE79 /* DownloadsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37DD29B5D05A001ACE79 /* DownloadsIntegrationTests.swift */; }; B6EC37DF29B5D05A001ACE79 /* DownloadsIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37DD29B5D05A001ACE79 /* DownloadsIntegrationTests.swift */; }; B6EC37EB29B5DA2A001ACE79 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37EA29B5DA2A001ACE79 /* main.swift */; }; @@ -4821,6 +4837,9 @@ B6C2C9EE276081AB005B7F0A /* DeallocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeallocationTests.swift; sourceTree = ""; }; B6C2C9F52760B659005B7F0A /* Permissions.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Permissions.xcdatamodel; sourceTree = ""; }; B6C416A6294A4AE500C4F2E7 /* DuckPlayerTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerTabExtension.swift; sourceTree = ""; }; + B6C8CA9C2AD010CF0060E1CD /* OperaDataImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperaDataImporter.swift; sourceTree = ""; }; + B6C8CAA12AD010D70060E1CD /* VivaldiDataImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VivaldiDataImporter.swift; sourceTree = ""; }; + B6C8CAA62AD010DD0060E1CD /* YandexDataImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YandexDataImporter.swift; sourceTree = ""; }; B6CA4823298CDC2E0067ECCE /* AdClickAttributionTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdClickAttributionTabExtensionTests.swift; sourceTree = ""; }; B6CA482F298D04690067ECCE /* MockPrivacyConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyConfiguration.swift; sourceTree = ""; }; B6D574B12947224C008ED1B6 /* ContentBlockingTabExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentBlockingTabExtension.swift; sourceTree = ""; }; @@ -4839,6 +4858,7 @@ B6DB3CFA26A17CB800D459B7 /* PermissionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionModel.swift; sourceTree = ""; }; B6E319372953446000DD3BCF /* Assertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assertions.swift; sourceTree = ""; }; B6E61EE2263AC0C8004E11AB /* FileManagerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExtension.swift; sourceTree = ""; }; + B6E951402AD3B3370036C46F /* CoccocDataImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoccocDataImporter.swift; sourceTree = ""; }; B6EC37DD29B5D05A001ACE79 /* DownloadsIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadsIntegrationTests.swift; sourceTree = ""; }; B6EC37E829B5DA2A001ACE79 /* tests-server */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "tests-server"; sourceTree = BUILT_PRODUCTS_DIR; }; B6EC37EA29B5DA2A001ACE79 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; @@ -5872,12 +5892,16 @@ 4B59023726B35F3600489384 /* Chromium */ = { isa = PBXGroup; children = ( + 4B59023C26B35F3600489384 /* BraveDataImporter.swift */, 4B59023826B35F3600489384 /* ChromeDataImporter.swift */, 4B59023926B35F3600489384 /* ChromiumLoginReader.swift */, 4BE53373286E39F10019DBFD /* ChromiumKeychainPrompt.swift */, 4B59023B26B35F3600489384 /* ChromiumDataImporter.swift */, - 4B59023C26B35F3600489384 /* BraveDataImporter.swift */, + B6E951402AD3B3370036C46F /* CoccocDataImporter.swift */, 4B8AC93226B3B06300879451 /* EdgeDataImporter.swift */, + B6C8CA9C2AD010CF0060E1CD /* OperaDataImporter.swift */, + B6C8CAA12AD010D70060E1CD /* VivaldiDataImporter.swift */, + B6C8CAA62AD010DD0060E1CD /* YandexDataImporter.swift */, ); path = Chromium; sourceTree = ""; @@ -6095,12 +6119,12 @@ 4B8AC93726B489C500879451 /* Firefox */ = { isa = PBXGroup; children = ( + 4B8AC93A26B48ADF00879451 /* ASN1Parser.swift */, + B696AFFA2AC5924800C93203 /* FileLineError.swift */, 4B8AC93826B48A5100879451 /* FirefoxLoginReader.swift */, 4B29759628281F0900187C4E /* FirefoxEncryptionKeyReader.swift */, 3701C9CD29BD040900305B15 /* FirefoxBerkeleyDatabaseReader.swift */, 4B5FF67726B602B100D42879 /* FirefoxDataImporter.swift */, - 4B8AC93A26B48ADF00879451 /* ASN1Parser.swift */, - B696AFFA2AC5924800C93203 /* FileLineError.swift */, ); path = Firefox; sourceTree = ""; @@ -10021,6 +10045,7 @@ 1D76760E2A9CE4F000DA0BD7 /* SupportedOsChecker.swift in Sources */, 3192A0102A4C4CFF0084EA89 /* BWNotRespondingAlert.swift in Sources */, 3192A0112A4C4CFF0084EA89 /* DebugUserScript.swift in Sources */, + B6C8CA9F2AD010CF0060E1CD /* OperaDataImporter.swift in Sources */, 3192A0122A4C4CFF0084EA89 /* RecentlyClosedTab.swift in Sources */, 3192A0132A4C4CFF0084EA89 /* PDFSearchTextMenuItemHandler.swift in Sources */, 3192A0142A4C4CFF0084EA89 /* DataImportViewController.swift in Sources */, @@ -10149,6 +10174,7 @@ 3192A0862A4C4CFF0084EA89 /* SharingMenu.swift in Sources */, 3192A0872A4C4CFF0084EA89 /* GrammarFeaturesManager.swift in Sources */, 3192A0882A4C4CFF0084EA89 /* WKMenuItemIdentifier.swift in Sources */, + B6C8CAA42AD010D70060E1CD /* VivaldiDataImporter.swift in Sources */, 3192A0892A4C4CFF0084EA89 /* SafariFaviconsReader.swift in Sources */, 3192A08A2A4C4CFF0084EA89 /* NSScreenExtension.swift in Sources */, 3192A08B2A4C4CFF0084EA89 /* NSBezierPathExtension.swift in Sources */, @@ -10384,6 +10410,7 @@ 3192A1622A4C4CFF0084EA89 /* MenuItemSelectors.swift in Sources */, 3192A1632A4C4CFF0084EA89 /* FaviconView.swift in Sources */, 3192A1642A4C4CFF0084EA89 /* OnboardingFlow.swift in Sources */, + B6C8CAA92AD010DD0060E1CD /* YandexDataImporter.swift in Sources */, 3192A1652A4C4CFF0084EA89 /* PasswordManagementLoginModel.swift in Sources */, BB5CB0A12A7AD59D00B312D1 /* NetworkProtectionDebugUtilities.swift in Sources */, 3192A1662A4C4CFF0084EA89 /* TabViewModel.swift in Sources */, @@ -10497,6 +10524,7 @@ 3192A1C52A4C4CFF0084EA89 /* BookmarkViewModel.swift in Sources */, 3192A1C62A4C4CFF0084EA89 /* DaxSpeech.swift in Sources */, 3192A1C72A4C4CFF0084EA89 /* DuckPlayerSchemeHandler.swift in Sources */, + B6E951432AD3B3370036C46F /* CoccocDataImporter.swift in Sources */, 3192A1C82A4C4CFF0084EA89 /* FirePopoverViewModel.swift in Sources */, 3192A1C92A4C4CFF0084EA89 /* BWCommand.swift in Sources */, 3192A1CA2A4C4CFF0084EA89 /* NSColorExtension.swift in Sources */, @@ -10696,6 +10724,7 @@ 1D36E659298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */, 3706FEBC293F6EFF00E42796 /* BWResponse.swift in Sources */, 3706FAF4293F65D500E42796 /* SafariBookmarksReader.swift in Sources */, + B6C8CA9E2AD010CF0060E1CD /* OperaDataImporter.swift in Sources */, 3706FAF5293F65D500E42796 /* SafariVersionReader.swift in Sources */, 3706FAF6293F65D500E42796 /* LoginFaviconView.swift in Sources */, 3706FEC0293F6EFF00E42796 /* BWRequest.swift in Sources */, @@ -10920,6 +10949,7 @@ 3706FBA7293F65D500E42796 /* EncryptedValueTransformer.swift in Sources */, 3706FBA8293F65D500E42796 /* PasteboardBookmark.swift in Sources */, 3706FBA9293F65D500E42796 /* PinnedTabsManager.swift in Sources */, + B6C8CAA32AD010D70060E1CD /* VivaldiDataImporter.swift in Sources */, B66260E829ACD0C900E9E3EE /* DuckPlayerTabExtension.swift in Sources */, 3706FBAA293F65D500E42796 /* HoverUserScript.swift in Sources */, 3706FBAC293F65D500E42796 /* MainMenuActions.swift in Sources */, @@ -11039,6 +11069,7 @@ 4B6785402AA7C726008A5004 /* DailyPixel.swift in Sources */, 3706FC08293F65D500E42796 /* CoreDataBookmarkImporter.swift in Sources */, 3706FC09293F65D500E42796 /* SuggestionViewModel.swift in Sources */, + B6E951422AD3B3370036C46F /* CoccocDataImporter.swift in Sources */, 3706FC0A293F65D500E42796 /* BookmarkManagedObject.swift in Sources */, 3706FC0B293F65D500E42796 /* CSVLoginExporter.swift in Sources */, 37197EAC294244D600394917 /* FutureExtension.swift in Sources */, @@ -11201,6 +11232,7 @@ 3706FC97293F65D500E42796 /* BookmarksOutlineView.swift in Sources */, 3706FC98293F65D500E42796 /* CountryList.swift in Sources */, 3706FC99293F65D500E42796 /* PreferencesSection.swift in Sources */, + B6C8CAA82AD010DD0060E1CD /* YandexDataImporter.swift in Sources */, B655124929A79465009BFE1C /* NavigationActionExtension.swift in Sources */, 3706FC9A293F65D500E42796 /* AutoconsentManagement.swift in Sources */, 3706FC9C293F65D500E42796 /* BookmarkStore.swift in Sources */, @@ -11690,6 +11722,7 @@ 4B9579772AC7AE700062CA31 /* NSPointExtension.swift in Sources */, 4B9579782AC7AE700062CA31 /* WindowsManager.swift in Sources */, 4B9579792AC7AE700062CA31 /* BWRequest.swift in Sources */, + B6C8CAA52AD010D70060E1CD /* VivaldiDataImporter.swift in Sources */, 4B95797A2AC7AE700062CA31 /* WKWebViewConfigurationExtensions.swift in Sources */, 4B95797B2AC7AE700062CA31 /* HomePageDefaultBrowserModel.swift in Sources */, 4B95797C2AC7AE700062CA31 /* CrashReporter.swift in Sources */, @@ -11835,6 +11868,7 @@ 4B957A082AC7AE700062CA31 /* LocalUnprotectedDomains.swift in Sources */, 4B957A092AC7AE700062CA31 /* InternalUserDeciderStore.swift in Sources */, 4B957A0A2AC7AE700062CA31 /* NewWindowPolicy.swift in Sources */, + B6C8CAA02AD010CF0060E1CD /* OperaDataImporter.swift in Sources */, 4B957A0B2AC7AE700062CA31 /* NavigationBarBadgeAnimator.swift in Sources */, 4B957A0C2AC7AE700062CA31 /* NSTextViewExtension.swift in Sources */, 4B957A0D2AC7AE700062CA31 /* FutureExtension.swift in Sources */, @@ -12158,6 +12192,7 @@ 4B957B482AC7AE700062CA31 /* MouseOverButton.swift in Sources */, 4B957B492AC7AE700062CA31 /* FireInfoViewController.swift in Sources */, 4B957B4A2AC7AE700062CA31 /* LoginItem+NetworkProtection.swift in Sources */, + B6C8CAAA2AD010DD0060E1CD /* YandexDataImporter.swift in Sources */, 4B957B4B2AC7AE700062CA31 /* PermissionButton.swift in Sources */, 4B957B4C2AC7AE700062CA31 /* MoreOptionsMenu.swift in Sources */, 4B957B4D2AC7AE700062CA31 /* PermissionAuthorizationViewController.swift in Sources */, @@ -12235,6 +12270,7 @@ 4B957B942AC7AE700062CA31 /* BWCommand.swift in Sources */, 4B957B952AC7AE700062CA31 /* NSColorExtension.swift in Sources */, 4B957B962AC7AE700062CA31 /* Stored.swift in Sources */, + B6E951442AD3B3370036C46F /* CoccocDataImporter.swift in Sources */, 4B957B972AC7AE700062CA31 /* AddressBarButtonsViewController.swift in Sources */, 4B957B982AC7AE700062CA31 /* BWError.swift in Sources */, 4B957B992AC7AE700062CA31 /* ChromeDataImporter.swift in Sources */, @@ -12384,6 +12420,7 @@ 37445F992A1566420029F789 /* SyncDataProviders.swift in Sources */, 4B9292A426670D2A00AD2C21 /* PasteboardWriting.swift in Sources */, 4B92928D26670D1700AD2C21 /* BookmarkOutlineViewCell.swift in Sources */, + B6C8CA9D2AD010CF0060E1CD /* OperaDataImporter.swift in Sources */, B604085C274B8FBA00680351 /* UnprotectedDomains.xcdatamodeld in Sources */, 4BB88B5025B7BA2B006F6B06 /* TabInstrumentation.swift in Sources */, 4B59024326B35F7C00489384 /* BrowserImportViewController.swift in Sources */, @@ -12482,6 +12519,7 @@ 4BCF15DD2ABB8CFC0083F6DF /* NetworkProtectionRemoteMessagingRequest.swift in Sources */, 31B4AF532901A4F20013585E /* NSEventExtension.swift in Sources */, 85707F26276A335700DC0649 /* Onboarding.swift in Sources */, + B6E951412AD3B3370036C46F /* CoccocDataImporter.swift in Sources */, B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */, AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, @@ -12785,6 +12823,7 @@ EEC111E6294D06290086524F /* JSAlertViewModel.swift in Sources */, 4BE5336C286912D40019DBFD /* BookmarksBarCollectionViewItem.swift in Sources */, B6C0B23926E742610031CB7F /* FileDownloadError.swift in Sources */, + B6C8CAA22AD010D70060E1CD /* VivaldiDataImporter.swift in Sources */, 85589EA027BFE60E0038AD11 /* MoreOrLessView.swift in Sources */, B6A9E47026146A250067D1B9 /* DateExtension.swift in Sources */, AAE7527A263B046100B973F8 /* History.xcdatamodeld in Sources */, @@ -12793,6 +12832,7 @@ 4BB99D0126FE191E001E4761 /* ChromiumBookmarksReader.swift in Sources */, B6C0B23426E71BCD0031CB7F /* Downloads.xcdatamodeld in Sources */, AAE8B110258A456C00E81239 /* TabPreviewViewController.swift in Sources */, + B6C8CAA72AD010DD0060E1CD /* YandexDataImporter.swift in Sources */, 37CC53EC27E8A4D10028713D /* PreferencesPrivacyView.swift in Sources */, 4B0135CE2729F1AA00D54834 /* NSPasteboardExtension.swift in Sources */, 85707F31276A7DCA00DC0649 /* OnboardingViewModel.swift in Sources */, diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index ba06cb120f..ffae74187b 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -390,7 +390,7 @@ struct UserText { static let csvImportDescription = NSLocalizedString("import.logins.csv.description", value: "The CSV importer will try to match column headers to their position.\nIf there is no header, it supports two formats:\n\n1. URL, Username, Password\n2. Title, URL, Username, Password", comment: "Description text for the CSV importer") static let importLoginsSelectCSVFile = NSLocalizedString("import.logins.select-csv-file", value: "Select CSV File…", comment: "Button text for selecting a CSV file") - static let importLoginsSelectSafariCSVFile = NSLocalizedString("import.logins.select-safari-csv-file", value: "Select Passwords CSV File…", comment: "Button text for selecting a Safari CSV file") + static let importLoginsSelectBrowserCSVFile = NSLocalizedString("import.logins.select-browser-csv-file", value: "Select Passwords CSV File…", comment: "Button text for selecting a browser CSV file") static let importLoginsSelect1PasswordCSVFile = NSLocalizedString("import.logins.select-1password-csv-file", value: "Select 1Password CSV File…", comment: "Button text for selecting a 1Password CSV file") static let importLoginsSelectLastPassCSVFile = NSLocalizedString("import.logins.select-lastpass-csv-file", value: "Select LastPass CSV File…", comment: "Button text for selecting a LastPass CSV file") diff --git a/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift b/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift index 7ab2b8e5a8..5faf8c6fe5 100644 --- a/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift +++ b/DuckDuckGo/DataImport/Bookmarks/HTML/BookmarkHTMLReader.swift @@ -284,8 +284,15 @@ private extension BookmarkImportSource { case .thirdPartyBrowser(.brave), .thirdPartyBrowser(.chrome), + .thirdPartyBrowser(.chromium), + .thirdPartyBrowser(.coccoc), .thirdPartyBrowser(.edge), .thirdPartyBrowser(.firefox), + .thirdPartyBrowser(.opera), + .thirdPartyBrowser(.operaGX), + .thirdPartyBrowser(.tor), + .thirdPartyBrowser(.vivaldi), + .thirdPartyBrowser(.yandex), .thirdPartyBrowser(.onePassword8), .thirdPartyBrowser(.onePassword7), .thirdPartyBrowser(.lastPass), diff --git a/DuckDuckGo/DataImport/ChromePreferences.swift b/DuckDuckGo/DataImport/ChromePreferences.swift index e24be4699a..b3b5163c90 100644 --- a/DuckDuckGo/DataImport/ChromePreferences.swift +++ b/DuckDuckGo/DataImport/ChromePreferences.swift @@ -18,14 +18,14 @@ import AppKit -struct ChromePreferences: Decodable { +struct ChromiumPreferences: Decodable { struct AccountInfo: Decodable { let email: String? let fullName: String? } struct Profile: Decodable { - let name: String + let name: String? let createdByVersion: String? } @@ -33,13 +33,13 @@ struct ChromePreferences: Decodable { let profile: Profile init(from data: Data) throws { - var decoder = JSONDecoder() + let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase self = try decoder.decode(Self.self, from: data) } - var profileName: String { + var profileName: String? { for account in accountInfo ?? [] { switch (account.fullName, account.email) { case (.some(let fullName), .some(let email)): diff --git a/DuckDuckGo/DataImport/DataImport.swift b/DuckDuckGo/DataImport/DataImport.swift index f14e380139..7407103463 100644 --- a/DuckDuckGo/DataImport/DataImport.swift +++ b/DuckDuckGo/DataImport/DataImport.swift @@ -22,13 +22,19 @@ import SecureStorage enum DataImport { enum Source: CaseIterable, Equatable { - case brave case chrome + case chromium + case coccoc case edge case firefox + case opera + case operaGX case safari case safariTechnologyPreview + case tor + case vivaldi + case yandex case onePassword8 case onePassword7 case lastPass @@ -43,14 +49,28 @@ enum DataImport { return "Brave" case .chrome: return "Chrome" + case .chromium: + return "Chromium" + case .coccoc: + return "Cốc Cốc" case .edge: return "Edge" case .firefox: return "Firefox" + case .opera: + return "Opera" + case .operaGX: + return "OperaGX" case .safari: return "Safari" case .safariTechnologyPreview: return "Safari Technology Preview" + case .tor: + return "Tor Browser" + case .vivaldi: + return "Vivaldi" + case .yandex: + return "Yandex" case .lastPass: return "LastPass" case .onePassword7: @@ -77,7 +97,7 @@ enum DataImport { case .csv, .onePassword8, .onePassword7, .lastPass, .bookmarksHTML: // Users can always import from exported files return true - case .brave, .chrome, .edge, .firefox, .safari, .safariTechnologyPreview: + case .brave, .chrome, .chromium, .coccoc, .edge, .firefox, .opera, .operaGX, .safari, .safariTechnologyPreview, .tor, .vivaldi, .yandex: // Users can't import from browsers unless they're installed return false } @@ -111,6 +131,13 @@ enum DataImport { } struct BrowserProfileList { + + enum Constants { + static let chromiumDefaultProfileName = "Default" + static let chromiumProfilePrefix = "Profile " + static let firefoxDefaultProfileName = "default-release" + } + let browser: ThirdPartyBrowser let profiles: [BrowserProfile] @@ -122,24 +149,24 @@ enum DataImport { self.browser = browser switch browser { - case .brave, .chrome, .edge: + case .brave, .chrome, .chromium, .coccoc, .edge, .opera, .operaGX, .vivaldi, .yandex: // Chromium profiles are either named "Default", or a series of incrementing profile names, i.e. "Profile 1", "Profile 2", etc. let potentialProfiles = profileURLs.map({ - BrowserProfile.for(browser: browser, profileURL: $0) + BrowserProfile(browser: browser, profileURL: $0) }) let filteredProfiles = potentialProfiles.filter { - $0.hasNonDefaultProfileName || - $0.profileName == "Default" || - $0.profileName.hasPrefix("Profile ") + $0.chromiumPreferences != nil + || $0.profileName == Constants.chromiumDefaultProfileName + || $0.profileName.hasPrefix(Constants.chromiumProfilePrefix) } let sortedProfiles = filteredProfiles.sorted() self.profiles = sortedProfiles - case .firefox, .safari, .safariTechnologyPreview: + case .firefox, .safari, .safariTechnologyPreview, .tor: self.profiles = profileURLs.map { - BrowserProfile.for(browser: browser, profileURL: $0) + BrowserProfile(browser: browser, profileURL: $0) }.sorted() case .lastPass, .onePassword7, .onePassword8: self.profiles = [] @@ -152,10 +179,10 @@ enum DataImport { var defaultProfile: BrowserProfile? { switch browser { - case .brave, .chrome, .edge: - return profiles.first { $0.profileName == "Default" } ?? profiles.first - case .firefox: - return profiles.first { $0.profileName == "default-release" } ?? profiles.first + case .brave, .chrome, .chromium, .coccoc, .edge, .opera, .operaGX, .vivaldi, .yandex: + return profiles.first { $0.profileName == Constants.chromiumDefaultProfileName } ?? profiles.first + case .firefox, .tor: + return profiles.first { $0.profileName == Constants.firefoxDefaultProfileName } ?? profiles.first case .safari, .safariTechnologyPreview, .lastPass, .onePassword7, .onePassword8: return profiles.first } @@ -171,21 +198,13 @@ enum DataImport { let profileURL: URL var profileName: String { - return detectedChromePreferencesProfileName ?? fallbackProfileName - } - - var hasNonDefaultProfileName: Bool { - return detectedChromePreferencesProfileName != nil + return chromiumPreferences?.profileName ?? fallbackProfileName } private let browser: ThirdPartyBrowser private let fileStore: FileStore private let fallbackProfileName: String - private let detectedChromePreferencesProfileName: String? - - static func `for`(browser: ThirdPartyBrowser, profileURL: URL) -> BrowserProfile { - return BrowserProfile(browser: browser, profileURL: profileURL) - } + let chromiumPreferences: ChromiumPreferences? init(browser: ThirdPartyBrowser, profileURL: URL, fileStore: FileStore = FileManager.default) { self.browser = browser @@ -193,7 +212,7 @@ enum DataImport { self.profileURL = profileURL self.fallbackProfileName = Self.getDefaultProfileName(at: profileURL) - self.detectedChromePreferencesProfileName = Self.getChromeProfileName(at: profileURL, fileStore: fileStore) + self.chromiumPreferences = Self.getChromiumProfilePreferences(at: profileURL, fileStore: fileStore) } var hasBrowserData: Bool { @@ -204,7 +223,7 @@ enum DataImport { let profileDirectoryContentsSet = Set(profileDirectoryContents) switch browser { - case .brave, .chrome, .edge: + case .brave, .chrome, .chromium, .coccoc, .edge, .opera, .operaGX, .vivaldi: let hasChromiumLogins = ChromiumLoginReader.LoginDataFileName.allCases.contains { loginFileName in return profileDirectoryContentsSet.contains(loginFileName.rawValue) } @@ -212,7 +231,7 @@ enum DataImport { let hasChromiumBookmarks = profileDirectoryContentsSet.contains(ChromiumBookmarksReader.Constants.defaultBookmarksFileName) return hasChromiumLogins || hasChromiumBookmarks - case .firefox: + case .firefox, .tor: let hasFirefoxLogins = FirefoxLoginReader.DataFormat.allCases.contains { dataFormat in let (databaseName, loginFileName) = dataFormat.formatFileNames @@ -231,7 +250,7 @@ enum DataImport { return profileURL.lastPathComponent.components(separatedBy: ".").last ?? profileURL.lastPathComponent } - private static func getChromeProfileName(at profileURL: URL, fileStore: FileStore) -> String? { + private static func getChromiumProfilePreferences(at profileURL: URL, fileStore: FileStore) -> ChromiumPreferences? { guard let profileDirectoryContents = try? fileStore.directoryContents(at: profileURL.path) else { return nil } @@ -241,9 +260,9 @@ enum DataImport { } if profileDirectoryContents.contains(Constants.chromiumPreferencesFileName), - let chromePreferenceData = fileStore.loadData(at: profileURL.appendingPathComponent(Constants.chromiumPreferencesFileName)), - let chromePreferences = try? ChromePreferences(from: chromePreferenceData) { - return chromePreferences.profileName + let preferencesData = fileStore.loadData(at: profileURL.appendingPathComponent(Constants.chromiumPreferencesFileName)), + let preferences = try? ChromiumPreferences(from: preferencesData) { + return preferences } return nil diff --git a/DuckDuckGo/DataImport/Logins/CSV/CSVImporter.swift b/DuckDuckGo/DataImport/Logins/CSV/CSVImporter.swift index 9735181030..065a25957f 100644 --- a/DuckDuckGo/DataImport/Logins/CSV/CSVImporter.swift +++ b/DuckDuckGo/DataImport/Logins/CSV/CSVImporter.swift @@ -70,7 +70,8 @@ final class CSVImporter: DataImporter { switch source { case .onePassword7, .onePassword8: self.init(titleIndex: 3, urlIndex: 5, usernameIndex: 6, passwordIndex: 2, maximumIndex: 7) - case .lastPass, .firefox, .edge, .chrome, .brave, .safari, .safariTechnologyPreview, .csv, .bookmarksHTML: + case .lastPass, .firefox, .edge, .chrome, .chromium, .coccoc, .brave, .opera, .operaGX, + .safari, .safariTechnologyPreview, .tor, .vivaldi, .yandex, .csv, .bookmarksHTML: return nil } } diff --git a/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift b/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift index bff180da0a..e356487226 100644 --- a/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift +++ b/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift @@ -21,11 +21,11 @@ import Foundation internal class ChromiumDataImporter: DataImporter { var processName: String { - fatalError("Subclasses must provide their own process name") + "Chromium" } var source: DataImport.Source { - fatalError("Subclasses must return a source") + .chromium } private let applicationDataDirectoryURL: URL @@ -40,6 +40,16 @@ internal class ChromiumDataImporter: DataImporter { self.faviconManager = faviconManager } + convenience init(loginImporter: LoginImporter, bookmarkImporter: BookmarkImporter) { + let applicationSupport = URL.nonSandboxApplicationSupportDirectoryURL + let defaultDataURL = applicationSupport.appendingPathComponent("Chromium/Default/") + + self.init(applicationDataDirectoryURL: defaultDataURL, + loginImporter: loginImporter, + bookmarkImporter: bookmarkImporter, + faviconManager: FaviconManager.shared) + } + func importableTypes() -> [DataImport.DataType] { return [.logins, .bookmarks] } diff --git a/DuckDuckGo/DataImport/Logins/Chromium/CoccocDataImporter.swift b/DuckDuckGo/DataImport/Logins/Chromium/CoccocDataImporter.swift new file mode 100644 index 0000000000..c718be040f --- /dev/null +++ b/DuckDuckGo/DataImport/Logins/Chromium/CoccocDataImporter.swift @@ -0,0 +1,41 @@ +// +// CoccocDataImporter.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// 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 + +final class CoccocDataImporter: ChromiumDataImporter { + + override var processName: String { + return "CocCoc" + } + + override var source: DataImport.Source { + return .coccoc + } + + init(loginImporter: LoginImporter, bookmarkImporter: BookmarkImporter) { + let applicationSupport = URL.nonSandboxApplicationSupportDirectoryURL + let defaultDataURL = applicationSupport.appendingPathComponent("Coccoc/Default/") + + super.init(applicationDataDirectoryURL: defaultDataURL, + loginImporter: loginImporter, + bookmarkImporter: bookmarkImporter, + faviconManager: FaviconManager.shared) + } + +} diff --git a/DuckDuckGo/DataImport/Logins/Chromium/OperaDataImporter.swift b/DuckDuckGo/DataImport/Logins/Chromium/OperaDataImporter.swift new file mode 100644 index 0000000000..f426354ba5 --- /dev/null +++ b/DuckDuckGo/DataImport/Logins/Chromium/OperaDataImporter.swift @@ -0,0 +1,41 @@ +// +// OperaDataImporter.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// 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 + +final class OperaDataImporter: ChromiumDataImporter { + + override var processName: String { + return "Opera" + } + + override var source: DataImport.Source { + return .opera + } + + init(loginImporter: LoginImporter, bookmarkImporter: BookmarkImporter, gx: Bool) { + let applicationSupport = URL.nonSandboxApplicationSupportDirectoryURL + let defaultDataURL = applicationSupport.appendingPathComponent("com.operasoftware.Opera\(gx ? "GX" : "")/") + + super.init(applicationDataDirectoryURL: defaultDataURL, + loginImporter: loginImporter, + bookmarkImporter: bookmarkImporter, + faviconManager: FaviconManager.shared) + } + +} diff --git a/DuckDuckGo/DataImport/Logins/Chromium/VivaldiDataImporter.swift b/DuckDuckGo/DataImport/Logins/Chromium/VivaldiDataImporter.swift new file mode 100644 index 0000000000..6ea12d0c56 --- /dev/null +++ b/DuckDuckGo/DataImport/Logins/Chromium/VivaldiDataImporter.swift @@ -0,0 +1,41 @@ +// +// VivaldiDataImporter.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// 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 + +final class VivaldiDataImporter: ChromiumDataImporter { + + override var processName: String { + return "Vivaldi" + } + + override var source: DataImport.Source { + return .vivaldi + } + + init(loginImporter: LoginImporter, bookmarkImporter: BookmarkImporter) { + let applicationSupport = URL.nonSandboxApplicationSupportDirectoryURL + let defaultDataURL = applicationSupport.appendingPathComponent("Vivaldi/Default/") + + super.init(applicationDataDirectoryURL: defaultDataURL, + loginImporter: loginImporter, + bookmarkImporter: bookmarkImporter, + faviconManager: FaviconManager.shared) + } + +} diff --git a/DuckDuckGo/DataImport/Logins/Chromium/YandexDataImporter.swift b/DuckDuckGo/DataImport/Logins/Chromium/YandexDataImporter.swift new file mode 100644 index 0000000000..8055fb9e14 --- /dev/null +++ b/DuckDuckGo/DataImport/Logins/Chromium/YandexDataImporter.swift @@ -0,0 +1,41 @@ +// +// YandexDataImporter.swift +// +// Copyright © 2023 DuckDuckGo. All rights reserved. +// +// 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 + +final class YandexDataImporter: ChromiumDataImporter { + + override var processName: String { + return "Yandex" + } + + override var source: DataImport.Source { + return .yandex + } + + init(loginImporter: LoginImporter, bookmarkImporter: BookmarkImporter) { + let applicationSupport = URL.nonSandboxApplicationSupportDirectoryURL + let defaultDataURL = applicationSupport.appendingPathComponent("Yandex/YandexBrowser/Default/") + + super.init(applicationDataDirectoryURL: defaultDataURL, + loginImporter: loginImporter, + bookmarkImporter: bookmarkImporter, + faviconManager: FaviconManager.shared) + } + +} diff --git a/DuckDuckGo/DataImport/Logins/Firefox/FirefoxDataImporter.swift b/DuckDuckGo/DataImport/Logins/Firefox/FirefoxDataImporter.swift index ceddc3eb5e..c01aef8b1f 100644 --- a/DuckDuckGo/DataImport/Logins/Firefox/FirefoxDataImporter.swift +++ b/DuckDuckGo/DataImport/Logins/Firefox/FirefoxDataImporter.swift @@ -19,7 +19,7 @@ import Foundation import SecureStorage -final class FirefoxDataImporter: DataImporter { +internal class FirefoxDataImporter: DataImporter { var primaryPassword: String? diff --git a/DuckDuckGo/DataImport/ThirdPartyBrowser.swift b/DuckDuckGo/DataImport/ThirdPartyBrowser.swift index 6428ed4b75..aa5ee43c85 100644 --- a/DuckDuckGo/DataImport/ThirdPartyBrowser.swift +++ b/DuckDuckGo/DataImport/ThirdPartyBrowser.swift @@ -32,10 +32,17 @@ enum ThirdPartyBrowser: CaseIterable { case brave case chrome + case chromium + case coccoc case edge case firefox + case opera + case operaGX case safari case safariTechnologyPreview + case tor + case vivaldi + case yandex case lastPass case onePassword7 case onePassword8 @@ -49,10 +56,17 @@ enum ThirdPartyBrowser: CaseIterable { switch source { case .brave: return .brave case .chrome: return .chrome + case .chromium: return .chromium + case .coccoc: return .coccoc case .edge: return .edge case .firefox: return .firefox + case .opera: return .opera + case .operaGX: return .operaGX case .safari: return .safari case .safariTechnologyPreview: return .safariTechnologyPreview + case .tor: return .tor + case .vivaldi: return .vivaldi + case .yandex: return .yandex case .lastPass: return .lastPass case .onePassword7: return .onePassword7 case .onePassword8: return .onePassword8 @@ -76,10 +90,17 @@ enum ThirdPartyBrowser: CaseIterable { switch self { case .brave: return .brave case .chrome: return .chrome + case .chromium: return .chromium + case .coccoc: return .coccoc case .edge: return .edge case .firefox: return .firefox + case .opera: return .opera + case .operaGX: return .operaGX case .safari: return .safari case .safariTechnologyPreview: return .safariTechnologyPreview + case .tor: return .tor + case .vivaldi: return .vivaldi + case .yandex: return .yandex case .onePassword7: return .onePassword7 case .onePassword8: return .onePassword8 case .lastPass: return .lastPass @@ -120,14 +141,25 @@ enum ThirdPartyBrowser: CaseIterable { private var bundleIdentifiers: BundleIdentifiers { switch self { case .brave: return BundleIdentifiers(productionBundleID: "com.brave.Browser", relatedBundleIDs: ["com.brave.Browser.nightly"]) - case .chrome: return BundleIdentifiers(productionBundleID: "com.google.Chrome", relatedBundleIDs: ["com.google.Chrome.canary"]) + case .chrome: return BundleIdentifiers(productionBundleID: "com.google.Chrome", relatedBundleIDs: [ + "com.google.Chrome.canary", + "com.google.Chrome.beta", + "com.google.Chrome.dev" + ]) + case .chromium: return BundleIdentifiers(productionBundleID: "org.chromium.Chromium", relatedBundleIDs: []) + case .coccoc: return BundleIdentifiers(productionBundleID: "com.coccoc.Coccoc", relatedBundleIDs: []) case .edge: return BundleIdentifiers(productionBundleID: "com.microsoft.edgemac", relatedBundleIDs: []) case .firefox: return BundleIdentifiers(productionBundleID: "org.mozilla.firefox", relatedBundleIDs: [ "org.mozilla.nightly", "org.mozilla.firefoxdeveloperedition" ]) + case .opera: return BundleIdentifiers(productionBundleID: "com.operasoftware.Opera", relatedBundleIDs: []) + case .operaGX: return BundleIdentifiers(productionBundleID: "com.operasoftware.OperaGX", relatedBundleIDs: []) case .safari: return BundleIdentifiers(productionBundleID: "com.apple.safari", relatedBundleIDs: []) case .safariTechnologyPreview: return BundleIdentifiers(productionBundleID: "com.apple.SafariTechnologyPreview", relatedBundleIDs: []) + case .tor: return BundleIdentifiers(productionBundleID: "org.torproject.torbrowser", relatedBundleIDs: []) + case .vivaldi: return BundleIdentifiers(productionBundleID: "com.vivaldi.Vivaldi", relatedBundleIDs: []) + case .yandex: return BundleIdentifiers(productionBundleID: "ru.yandex.desktop.yandex-browser", relatedBundleIDs: []) case .onePassword7: return BundleIdentifiers(productionBundleID: "com.agilebits.onepassword7", relatedBundleIDs: [ "com.agilebits.onepassword", "com.agilebits.onepassword4" @@ -158,11 +190,12 @@ enum ThirdPartyBrowser: CaseIterable { } guard let profilePath = profilesDirectory(applicationSupportURL: applicationSupportURL), - let potentialProfileURLs = try? FileManager.default.contentsOfDirectory(at: profilePath, + var potentialProfileURLs = try? FileManager.default.contentsOfDirectory(at: profilePath, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]).filter(\.hasDirectoryPath) else { return nil } + potentialProfileURLs.append(profilePath) return DataImport.BrowserProfileList(browser: self, profileURLs: potentialProfileURLs) } @@ -179,14 +212,22 @@ enum ThirdPartyBrowser: CaseIterable { } // Returns the URL to the profiles for a given browser. This directory will contain a list of directories, each representing a profile. + // swiftlint:disable:next cyclomatic_complexity private func profilesDirectory(applicationSupportURL: URL) -> URL? { switch self { case .brave: return applicationSupportURL.appendingPathComponent("BraveSoftware/Brave-Browser/") case .chrome: return applicationSupportURL.appendingPathComponent("Google/Chrome/") + case .chromium: return applicationSupportURL.appendingPathComponent("Chromium/") + case .coccoc: return applicationSupportURL.appendingPathComponent("Coccoc/") case .edge: return applicationSupportURL.appendingPathComponent("Microsoft Edge/") case .firefox: return applicationSupportURL.appendingPathComponent("Firefox/Profiles/") + case .opera: return applicationSupportURL.appendingPathComponent("com.operasoftware.Opera/") + case .operaGX: return applicationSupportURL.appendingPathComponent("com.operasoftware.OperaGX/") case .safari: return URL.nonSandboxLibraryDirectoryURL.appendingPathComponent("Safari/") case .safariTechnologyPreview: return URL.nonSandboxLibraryDirectoryURL.appendingPathComponent("SafariTechnologyPreview/") + case .tor: return applicationSupportURL.appendingPathComponent("TorBrowser-Data/Browser/") + case .vivaldi: return applicationSupportURL.appendingPathComponent("Vivaldi/") + case .yandex: return applicationSupportURL.appendingPathComponent("Yandex/YandexBrowser/") case .lastPass, .onePassword7, .onePassword8: return nil } } diff --git a/DuckDuckGo/DataImport/View/BrowserImportMoreInfoViewController.swift b/DuckDuckGo/DataImport/View/BrowserImportMoreInfoViewController.swift index 53374dc830..12e8ba2ea1 100644 --- a/DuckDuckGo/DataImport/View/BrowserImportMoreInfoViewController.swift +++ b/DuckDuckGo/DataImport/View/BrowserImportMoreInfoViewController.swift @@ -49,10 +49,10 @@ final class BrowserImportMoreInfoViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() switch source { - case .chrome, .edge, .brave: + case .chrome, .chromium, .coccoc, .edge, .brave, .opera, .operaGX, .vivaldi, .yandex: label.stringValue = UserText.importFromChromiumMoreInfo - case .firefox: + case .firefox, .tor: label.stringValue = UserText.importFromFirefoxMoreInfo case .safari, .safariTechnologyPreview, .csv, .lastPass, .onePassword7, .onePassword8, .bookmarksHTML: diff --git a/DuckDuckGo/DataImport/View/BrowserImportViewController.swift b/DuckDuckGo/DataImport/View/BrowserImportViewController.swift index d61c77f52f..bd486bdbe4 100644 --- a/DuckDuckGo/DataImport/View/BrowserImportViewController.swift +++ b/DuckDuckGo/DataImport/View/BrowserImportViewController.swift @@ -113,8 +113,11 @@ final class BrowserImportViewController: NSViewController { passwordsWarningLabel.isHidden = false return } - passwordsWarningLabel.isHidden = safariMajorVersion >= 15 + case .yandex, .tor: + passwordsCheckbox.isHidden = true + bookmarksCheckbox.title = UserText.bookmarkImportBookmarks + passwordsWarningLabel.isHidden = true default: bookmarksCheckbox.title = UserText.bookmarkImportBookmarks passwordsWarningLabel.isHidden = true diff --git a/DuckDuckGo/DataImport/View/DataImportViewController.swift b/DuckDuckGo/DataImport/View/DataImportViewController.swift index 737d725279..5883b4fe28 100644 --- a/DuckDuckGo/DataImport/View/DataImportViewController.swift +++ b/DuckDuckGo/DataImport/View/DataImportViewController.swift @@ -182,7 +182,7 @@ final class DataImportViewController: NSViewController { case .csv, .lastPass, .onePassword7, .onePassword8, .bookmarksHTML: self.viewState = ViewState(selectedImportSource: source, interactionState: .unableToImport) - case .chrome, .firefox, .brave, .edge, .safari, .safariTechnologyPreview: + case .chrome, .chromium, .coccoc, .firefox, .brave, .edge, .opera, .operaGX, .safari, .safariTechnologyPreview, .tor, .vivaldi, .yandex: let interactionState: InteractionState switch (source, loginsSelected) { case (.safari, _), @@ -216,6 +216,7 @@ final class DataImportViewController: NSViewController { } } + // swiftlint:disable:next cyclomatic_complexity private func throwingRefreshDataImporter() throws { let bookmarkImporter = CoreDataBookmarkImporter(bookmarkManager: LocalBookmarkManager.shared) @@ -224,9 +225,21 @@ final class DataImportViewController: NSViewController { self.dataImporter = try BraveDataImporter(loginImporter: secureVaultImporter(), bookmarkImporter: bookmarkImporter) case .chrome: self.dataImporter = try ChromeDataImporter(loginImporter: secureVaultImporter(), bookmarkImporter: bookmarkImporter) + case .chromium: + self.dataImporter = try ChromiumDataImporter(loginImporter: secureVaultImporter(), bookmarkImporter: bookmarkImporter) + case .coccoc: + self.dataImporter = try CoccocDataImporter(loginImporter: secureVaultImporter(), bookmarkImporter: bookmarkImporter) case .edge: self.dataImporter = try EdgeDataImporter(loginImporter: secureVaultImporter(), bookmarkImporter: bookmarkImporter) - case .firefox: + case .opera, .operaGX: + self.dataImporter = try OperaDataImporter(loginImporter: secureVaultImporter(), + bookmarkImporter: bookmarkImporter, + gx: viewState.selectedImportSource == .operaGX) + case .vivaldi: + self.dataImporter = try VivaldiDataImporter(loginImporter: secureVaultImporter(), bookmarkImporter: bookmarkImporter) + case .yandex: + self.dataImporter = try YandexDataImporter(loginImporter: secureVaultImporter(), bookmarkImporter: bookmarkImporter) + case .firefox, .tor: self.dataImporter = try FirefoxDataImporter(loginImporter: secureVaultImporter(), bookmarkImporter: bookmarkImporter, faviconManager: FaviconManager.shared) @@ -321,7 +334,7 @@ final class DataImportViewController: NSViewController { } fallthrough - case .brave, .chrome, .edge, .firefox: + case .brave, .chrome, .chromium, .coccoc, .edge, .firefox, .opera, .operaGX, .tor, .vivaldi, .yandex: if case let .completedImport(summary) = interactionState { return BrowserImportSummaryViewController.create(importSummary: summary) } else if case let .permissionsRequired(types) = interactionState { diff --git a/DuckDuckGo/DataImport/View/FileImportViewController.swift b/DuckDuckGo/DataImport/View/FileImportViewController.swift index 505b8ea2cd..e531df4fa5 100644 --- a/DuckDuckGo/DataImport/View/FileImportViewController.swift +++ b/DuckDuckGo/DataImport/View/FileImportViewController.swift @@ -107,13 +107,13 @@ final class FileImportViewController: NSViewController { private func renderAwaitingFileSelectionState() { switch importSource { - case .safari, .safariTechnologyPreview: + case .safari, .safariTechnologyPreview, .yandex: descriptionLabel.isHidden = true safariInfoView.isHidden = false lastPassInfoView.isHidden = true onePassword7InfoView.isHidden = true onePassword8InfoView.isHidden = true - selectFileButton.title = UserText.importLoginsSelectSafariCSVFile + selectFileButton.title = UserText.importLoginsSelectBrowserCSVFile case .onePassword7: descriptionLabel.isHidden = true safariInfoView.isHidden = true @@ -136,7 +136,7 @@ final class FileImportViewController: NSViewController { onePassword8InfoView.isHidden = true selectFileButton.title = UserText.importLoginsSelectLastPassCSVFile - case .brave, .chrome, .edge, .firefox: + case .brave, .chrome, .chromium, .coccoc, .edge, .firefox, .opera, .operaGX, .tor, .vivaldi: assertionFailure("CSV Import not supported for \(importSource)") fallthrough case .csv: @@ -208,9 +208,9 @@ final class FileImportViewController: NSViewController { switch importSource { case .bookmarksHTML: delegate?.fileImportViewController(self, didSelectBookmarksFileWithURL: selectedURL) - case .csv, .onePassword8, .onePassword7, .lastPass, .safari, .safariTechnologyPreview: + case .csv, .onePassword8, .onePassword7, .lastPass, .safari, .safariTechnologyPreview, .yandex: delegate?.fileImportViewController(self, didSelectCSVFileWithURL: selectedURL) - case .brave, .chrome, .edge, .firefox: + case .brave, .chrome, .chromium, .coccoc, .edge, .firefox, .opera, .operaGX, .tor, .vivaldi: break } } else { diff --git a/DuckDuckGo/DuckDuckGoAppStore.entitlements b/DuckDuckGo/DuckDuckGoAppStore.entitlements index 340f405420..cfabed1af6 100644 --- a/DuckDuckGo/DuckDuckGoAppStore.entitlements +++ b/DuckDuckGo/DuckDuckGoAppStore.entitlements @@ -26,6 +26,14 @@ /Library/Application Support/BraveSoftware/Brave-Browser/ /Library/Application Support/Firefox/ /Library/Application Support/Microsoft Edge/ + /Library/Application Support/Vivaldi/ + /Library/Application Support/com.operasoftware.Opera/ + /Library/Application Support/com.operasoftware.OperaGX/ + /Library/Application Support/Yandex/YandexBrowser/ + /Library/Application Support/Chromium/ + /Library/Application Support/Coccoc/ + /Library/Application Support/TorBrowser-Data/Browser/ + /Library/Application Support/Arc/User Data/ keychain-access-groups diff --git a/DuckDuckGo/Statistics/PixelArguments.swift b/DuckDuckGo/Statistics/PixelArguments.swift index 0157982970..f6e81355c3 100644 --- a/DuckDuckGo/Statistics/PixelArguments.swift +++ b/DuckDuckGo/Statistics/PixelArguments.swift @@ -129,6 +129,8 @@ extension DataImport.Source: CustomStringConvertible { switch self { case .brave: return "source-brave" case .chrome: return "source-chrome" + case .chromium: return "source-chromium" + case .coccoc: return "source-coccoc" case .csv: return "source-csv" case .lastPass: return "source-lastpass" case .onePassword7: return "source-1password" @@ -138,6 +140,11 @@ extension DataImport.Source: CustomStringConvertible { case .safari: return "source-safari" case .safariTechnologyPreview: return "source-safari-technology-preview" case .bookmarksHTML: return "source-bookmarks-html" + case .opera: return "source-opera" + case .operaGX: return "source-operagx" + case .tor: return "source-tor" + case .vivaldi: return "source-vivaldi" + case .yandex: return "source-yandex" } } } diff --git a/UnitTests/DataImport/BrowserProfileTests.swift b/UnitTests/DataImport/BrowserProfileTests.swift index 408d99aff2..1dc15acb7e 100644 --- a/UnitTests/DataImport/BrowserProfileTests.swift +++ b/UnitTests/DataImport/BrowserProfileTests.swift @@ -89,7 +89,7 @@ class BrowserProfileListTests: XCTestCase { let profile = DataImport.BrowserProfile(browser: .chrome, profileURL: profileURL, fileStore: fileStore) XCTAssertEqual(profile.profileName, "User Name (profile@duck.com)") - XCTAssertTrue(profile.hasNonDefaultProfileName) + XCTAssertNotNil(profile.chromiumPreferences?.profileName) } func testWhenGettingProfileName_AndProfileHasNoDetectedChromiumName_ThenDetectedNameIsUsed() { @@ -111,7 +111,7 @@ class BrowserProfileListTests: XCTestCase { let profile = DataImport.BrowserProfile(browser: .chrome, profileURL: profileURL, fileStore: fileStore) XCTAssertEqual(profile.profileName, "ChromeProfile") - XCTAssertTrue(profile.hasNonDefaultProfileName) + XCTAssertNotNil(profile.chromiumPreferences?.profileName) } func testWhenGettingProfileName_AndChromiumPreferencesAreDetected_AndProfileNameIsSystemProfile_ThenProfileHasDefaultProfileName() { @@ -132,7 +132,7 @@ class BrowserProfileListTests: XCTestCase { let profile = DataImport.BrowserProfile(browser: .chrome, profileURL: profileURL, fileStore: fileStore) XCTAssertEqual(profile.profileName, "System Profile") - XCTAssertFalse(profile.hasNonDefaultProfileName) + XCTAssertNil(profile.chromiumPreferences?.profileName) } private func profile(named name: String) -> URL { From 5cdfa77ebc6d939de3b087b54587210ca05df2fa Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 9 Oct 2023 16:52:11 +0600 Subject: [PATCH 2/9] fix tests --- UnitTests/DataImport/ThirdPartyBrowserTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/UnitTests/DataImport/ThirdPartyBrowserTests.swift b/UnitTests/DataImport/ThirdPartyBrowserTests.swift index 469f16e2ab..1ae1558f65 100644 --- a/UnitTests/DataImport/ThirdPartyBrowserTests.swift +++ b/UnitTests/DataImport/ThirdPartyBrowserTests.swift @@ -81,9 +81,9 @@ class ThirdPartyBrowserTests: XCTestCase { return } - XCTAssertEqual(list.profiles.count, 2) + let validProfiles = list.profiles.filter { $0.hasBrowserData } + XCTAssertEqual(validProfiles.count, 2) XCTAssertEqual(list.defaultProfile?.profileName, "default-release") - XCTAssertTrue(list.profiles.allSatisfy { profile in return profile.hasBrowserData }) } func testWhenGettingBrowserProfiles_AndFirefoxProfileOnlyHasBookmarksData_ThenFirefoxProfileIsReturned() throws { @@ -107,9 +107,9 @@ class ThirdPartyBrowserTests: XCTestCase { return } - XCTAssertEqual(list.profiles.count, 1) + let validProfiles = list.profiles.filter { $0.hasBrowserData } + XCTAssertEqual(validProfiles.count, 1) XCTAssertEqual(list.defaultProfile?.profileName, "default-release") - XCTAssertTrue(list.profiles.allSatisfy { profile in return profile.hasBrowserData }) } private func key4DatabaseURL() -> URL { From 24a24ac9196c74771f3d2bf9d9f47e5acfb40481 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 10 Oct 2023 10:17:36 +0600 Subject: [PATCH 3/9] Add Passwords import as CSV step to Yandex --- .../Chromium/ChromiumDataImporter.swift | 16 +- .../Logins/Chromium/YandexDataImporter.swift | 17 +- .../BrowserImportMoreInfoViewController.swift | 4 +- .../View/BrowserImportViewController.swift | 2 +- .../DataImport/View/DataImport.storyboard | 558 +++++++++++++++--- .../View/DataImportViewController.swift | 19 +- .../View/FileImportViewController.swift | 18 +- 7 files changed, 518 insertions(+), 116 deletions(-) diff --git a/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift b/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift index e356487226..5c28ab7a74 100644 --- a/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift +++ b/DuckDuckGo/DataImport/Logins/Chromium/ChromiumDataImporter.swift @@ -30,17 +30,17 @@ internal class ChromiumDataImporter: DataImporter { private let applicationDataDirectoryURL: URL private let bookmarkImporter: BookmarkImporter - private let loginImporter: LoginImporter + private let loginImporter: LoginImporter? private let faviconManager: FaviconManagement - init(applicationDataDirectoryURL: URL, loginImporter: LoginImporter, bookmarkImporter: BookmarkImporter, faviconManager: FaviconManagement) { + init(applicationDataDirectoryURL: URL, loginImporter: LoginImporter?, bookmarkImporter: BookmarkImporter, faviconManager: FaviconManagement) { self.applicationDataDirectoryURL = applicationDataDirectoryURL self.loginImporter = loginImporter self.bookmarkImporter = bookmarkImporter self.faviconManager = faviconManager } - convenience init(loginImporter: LoginImporter, bookmarkImporter: BookmarkImporter) { + convenience init(loginImporter: LoginImporter?, bookmarkImporter: BookmarkImporter) { let applicationSupport = URL.nonSandboxApplicationSupportDirectoryURL let defaultDataURL = applicationSupport.appendingPathComponent("Chromium/Default/") @@ -54,18 +54,18 @@ internal class ChromiumDataImporter: DataImporter { return [.logins, .bookmarks] } - func importData(types: [DataImport.DataType], - from profile: DataImport.BrowserProfile?, - completion: @escaping (DataImportResult) -> Void) { + final func importData(types: [DataImport.DataType], + from profile: DataImport.BrowserProfile?, + completion: @escaping (DataImportResult) -> Void) { let result = importData(types: types, from: profile) completion(result) } - private func importData(types: [DataImport.DataType], from profile: DataImport.BrowserProfile?) -> DataImportResult { + func importData(types: [DataImport.DataType], from profile: DataImport.BrowserProfile?) -> DataImportResult { var summary = DataImport.Summary() let dataDirectoryURL = profile?.profileURL ?? applicationDataDirectoryURL - if types.contains(.logins) { + if types.contains(.logins), let loginImporter { let loginReader = ChromiumLoginReader(chromiumDataDirectoryURL: dataDirectoryURL, source: source, processName: processName) let loginResult = loginReader.readLogins() diff --git a/DuckDuckGo/DataImport/Logins/Chromium/YandexDataImporter.swift b/DuckDuckGo/DataImport/Logins/Chromium/YandexDataImporter.swift index 8055fb9e14..402b1c16c7 100644 --- a/DuckDuckGo/DataImport/Logins/Chromium/YandexDataImporter.swift +++ b/DuckDuckGo/DataImport/Logins/Chromium/YandexDataImporter.swift @@ -28,14 +28,27 @@ final class YandexDataImporter: ChromiumDataImporter { return .yandex } - init(loginImporter: LoginImporter, bookmarkImporter: BookmarkImporter) { + init(bookmarkImporter: BookmarkImporter) { let applicationSupport = URL.nonSandboxApplicationSupportDirectoryURL let defaultDataURL = applicationSupport.appendingPathComponent("Yandex/YandexBrowser/Default/") super.init(applicationDataDirectoryURL: defaultDataURL, - loginImporter: loginImporter, + loginImporter: nil, bookmarkImporter: bookmarkImporter, faviconManager: FaviconManager.shared) } + override func importData(types: [DataImport.DataType], from profile: DataImport.BrowserProfile?) -> DataImportResult { + var result = super.importData(types: types.filter { $0 != .logins }, from: profile) + + if case .success(var summary) = result, + types.contains(.logins) { + + summary.loginsResult = .awaited + result = .success(summary) + } + + return result + } + } diff --git a/DuckDuckGo/DataImport/View/BrowserImportMoreInfoViewController.swift b/DuckDuckGo/DataImport/View/BrowserImportMoreInfoViewController.swift index 12e8ba2ea1..b1e70cc67f 100644 --- a/DuckDuckGo/DataImport/View/BrowserImportMoreInfoViewController.swift +++ b/DuckDuckGo/DataImport/View/BrowserImportMoreInfoViewController.swift @@ -49,13 +49,13 @@ final class BrowserImportMoreInfoViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() switch source { - case .chrome, .chromium, .coccoc, .edge, .brave, .opera, .operaGX, .vivaldi, .yandex: + case .chrome, .chromium, .coccoc, .edge, .brave, .opera, .operaGX, .vivaldi: label.stringValue = UserText.importFromChromiumMoreInfo case .firefox, .tor: label.stringValue = UserText.importFromFirefoxMoreInfo - case .safari, .safariTechnologyPreview, .csv, .lastPass, .onePassword7, .onePassword8, .bookmarksHTML: + case .safari, .safariTechnologyPreview, .yandex, .csv, .lastPass, .onePassword7, .onePassword8, .bookmarksHTML: fatalError("Unsupported source for more info") } } diff --git a/DuckDuckGo/DataImport/View/BrowserImportViewController.swift b/DuckDuckGo/DataImport/View/BrowserImportViewController.swift index bd486bdbe4..f3beb2daac 100644 --- a/DuckDuckGo/DataImport/View/BrowserImportViewController.swift +++ b/DuckDuckGo/DataImport/View/BrowserImportViewController.swift @@ -114,7 +114,7 @@ final class BrowserImportViewController: NSViewController { return } passwordsWarningLabel.isHidden = safariMajorVersion >= 15 - case .yandex, .tor: + case .tor: passwordsCheckbox.isHidden = true bookmarksCheckbox.title = UserText.bookmarkImportBookmarks passwordsWarningLabel.isHidden = true diff --git a/DuckDuckGo/DataImport/View/DataImport.storyboard b/DuckDuckGo/DataImport/View/DataImport.storyboard index 2c75c784c1..521875891f 100644 --- a/DuckDuckGo/DataImport/View/DataImport.storyboard +++ b/DuckDuckGo/DataImport/View/DataImport.storyboard @@ -1,7 +1,7 @@ - + - + @@ -16,7 +16,7 @@ - + @@ -26,9 +26,6 @@ - - - @@ -38,6 +35,9 @@ + + + @@ -111,7 +111,7 @@ Gw - + @@ -133,7 +133,7 @@ Gw - + @@ -180,7 +180,7 @@ Gw - + @@ -200,7 +200,7 @@ Gw - + @@ -222,7 +222,7 @@ Gw - + @@ -252,7 +252,7 @@ Gw - + @@ -282,7 +282,7 @@ Gw - + @@ -312,7 +312,7 @@ Gw - + @@ -387,7 +387,7 @@ Gw - + @@ -407,7 +407,7 @@ Gw - + @@ -438,7 +438,7 @@ Gw - + @@ -507,13 +507,13 @@ Gw - + - + - - + + @@ -521,12 +521,12 @@ Gw - + - + @@ -546,7 +546,7 @@ Gw - + @@ -590,7 +590,7 @@ Gw - + @@ -610,7 +610,7 @@ Gw - + @@ -618,16 +618,16 @@ Gw - - + + - - + + @@ -657,7 +657,7 @@ Gw - + @@ -677,7 +677,7 @@ Gw - + @@ -685,7 +685,7 @@ Gw - + @@ -693,7 +693,7 @@ Gw - + @@ -724,7 +724,7 @@ Gw - + @@ -744,7 +744,7 @@ Gw - + @@ -761,7 +761,7 @@ Gw - + @@ -769,7 +769,7 @@ Gw - + @@ -803,7 +803,7 @@ Gw - + @@ -823,7 +823,7 @@ Gw - + @@ -855,7 +855,7 @@ Gw - + @@ -886,10 +886,376 @@ Gw + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -912,7 +1278,7 @@ Gw - + @@ -932,7 +1298,7 @@ Gw - + @@ -959,7 +1325,7 @@ Gw - + @@ -979,7 +1345,7 @@ Gw - + @@ -987,7 +1353,7 @@ Gw - + @@ -1016,7 +1382,7 @@ Gw - + @@ -1036,7 +1402,7 @@ Gw - + @@ -1044,7 +1410,7 @@ Gw - + @@ -1073,7 +1439,7 @@ Gw - + @@ -1093,7 +1459,7 @@ Gw - + @@ -1120,7 +1486,7 @@ Gw - + @@ -1140,7 +1506,7 @@ Gw - + @@ -1174,7 +1540,7 @@ Gw - + @@ -1216,7 +1582,7 @@ Gw - + @@ -1237,7 +1603,7 @@ Gw - + @@ -1260,7 +1626,7 @@ Gw - + @@ -1280,7 +1646,7 @@ Gw - + @@ -1307,7 +1673,7 @@ Gw - + @@ -1327,7 +1693,7 @@ Gw - + @@ -1354,7 +1720,7 @@ Gw - + @@ -1374,7 +1740,7 @@ Gw - + @@ -1382,16 +1748,16 @@ Gw - - + + - - + + @@ -1421,7 +1787,7 @@ Gw - + @@ -1441,7 +1807,7 @@ Gw - + @@ -1468,7 +1834,7 @@ Gw - + @@ -1488,7 +1854,7 @@ Gw - + @@ -1515,7 +1881,7 @@ Gw - + @@ -1535,7 +1901,7 @@ Gw - + @@ -1571,7 +1937,7 @@ Gw - + @@ -1615,7 +1981,7 @@ Gw - + @@ -1636,7 +2002,7 @@ Gw - + @@ -1659,7 +2025,7 @@ Gw - + @@ -1679,7 +2045,7 @@ Gw - + @@ -1706,7 +2072,7 @@ Gw - + @@ -1726,7 +2092,7 @@ Gw - + @@ -1753,7 +2119,7 @@ Gw - + @@ -1773,7 +2139,7 @@ Gw - + @@ -1800,7 +2166,7 @@ Gw - + @@ -1820,7 +2186,7 @@ Gw - + @@ -1847,7 +2213,7 @@ Gw - + @@ -1867,8 +2233,8 @@ Gw - - + + @@ -1901,7 +2267,7 @@ Gw - + @@ -1932,7 +2298,7 @@ Gw