diff --git a/components/password_manager/core/browser/import/BUILD.gn b/components/password_manager/core/browser/import/BUILD.gn new file mode 100644 index 000000000000..11307e16b952 --- /dev/null +++ b/components/password_manager/core/browser/import/BUILD.gn @@ -0,0 +1,63 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//build/config/ios/rules.gni") +import("//ios/build/config.gni") + +source_set("importer") { + sources = [ + "safari_import_results.cc", + "safari_import_results.h", + "safari_password_importer.cc", + "safari_password_importer.h", + ] + public_deps = [ + ":csv", + "//base", + "//components/password_manager/core/browser:password_form", + ] + + deps = [ + "//brave/components/password_manager/services/csv_password:lib", + "//brave/components/password_manager/services/csv_password/public/mojom:mojom", + "//build:blink_buildflags", + "//components/password_manager/core/browser/ui", + "//components/password_manager/core/common:constants", + "//components/password_manager/core/common:features", + "//components/sync/base:base", + ] + + if (use_blink) { + deps += + [ "//brave/components/password_manager/services/csv_password:service" ] + } + + configs += [ "//build/config/compiler:wexit_time_destructors" ] +} + +source_set("csv") { + sources = [ + "csv_safari_password.cc", + "csv_safari_password.h", + "csv_safari_password_iterator.cc", + "csv_safari_password_iterator.h", + "csv_safari_password_sequence.cc", + "csv_safari_password_sequence.h", + ] + public_deps = [ + "//base", + "//components/password_manager/core/browser:password_form", + "//components/password_manager/core/browser/import:csv", + ] + + deps = [ + "//build:blink_buildflags", + "//components/affiliations/core/browser:affiliations", + "//components/password_manager/core/browser/form_parsing", + "//url", + ] + + configs += [ "//build/config/compiler:wexit_time_destructors" ] +} diff --git a/components/password_manager/core/browser/import/csv_safari_password.cc b/components/password_manager/core/browser/import/csv_safari_password.cc new file mode 100644 index 000000000000..0ac17fffeb09 --- /dev/null +++ b/components/password_manager/core/browser/import/csv_safari_password.cc @@ -0,0 +1,156 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/password_manager/core/browser/import/csv_safari_password.h" + +#include +#include + +#include "base/check_op.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "components/affiliations/core/browser/affiliation_utils.h" +#include "components/password_manager/core/browser/form_parsing/form_data_parser.h" +#include "components/password_manager/core/browser/import/csv_field_parser.h" +#include "url/gurl.h" + +namespace password_manager { + +namespace { + +std::string ConvertUTF8(std::string_view str) { + std::string str_copy(str); + base::ReplaceSubstringsAfterOffset(&str_copy, 0, "\"\"", "\""); + return str_copy; +} + +} // namespace + +CSVSafariPassword::CSVSafariPassword() : status_(Status::kSemanticError) {} + +CSVSafariPassword::CSVSafariPassword(std::string title, + GURL url, + std::string username, + std::string password, + std::string notes, + GURL otp_auth_url, + Status status) + : title_(title), + url_(std::move(url)), + username_(std::move(username)), + password_(std::move(password)), + notes_(std::move(notes)), + otp_auth_url_(otp_auth_url), + status_(status) {} + +CSVSafariPassword::CSVSafariPassword(std::string title, + std::string invalid_url, + std::string username, + std::string password, + std::string notes, + GURL otp_auth_url, + Status status) + : title_(title), + url_(base::unexpected(std::move(invalid_url))), + username_(std::move(username)), + password_(std::move(password)), + notes_(std::move(notes)), + otp_auth_url_(otp_auth_url), + status_(status) {} + +CSVSafariPassword::CSVSafariPassword(const ColumnMap& map, + std::string_view row) { + if (row.empty()) { + status_ = Status::kSemanticError; + return; + } + + size_t field_idx = 0; + CSVFieldParser parser(row); + status_ = Status::kOK; + + while (parser.HasMoreFields()) { + std::string_view field; + if (!parser.NextField(&field)) { + status_ = Status::kSyntaxError; + return; + } + auto meaning_it = map.find(field_idx++); + if (meaning_it == map.end()) { + continue; + } + switch (meaning_it->second) { + case Label::kTitle: + title_ = ConvertUTF8(field); + break; + case Label::kURL: { + GURL gurl = GURL(field); + if (!gurl.is_valid()) { + url_ = base::unexpected(ConvertUTF8(field)); + } else { + url_ = gurl; + } + break; + } + case Label::kUsername: + username_ = ConvertUTF8(field); + break; + case Label::kPassword: + password_ = ConvertUTF8(field); + break; + case Label::kNotes: + notes_ = ConvertUTF8(field); + break; + case Label::kOTPAuthURL: { + GURL gurl = GURL(field); + if (!gurl.is_valid()) { + otp_auth_url_ = base::unexpected(ConvertUTF8(field)); + } else { + otp_auth_url_ = gurl; + } + break; + } + } + } +} + +CSVSafariPassword::CSVSafariPassword(const CSVSafariPassword&) = default; +CSVSafariPassword::CSVSafariPassword(CSVSafariPassword&&) = default; +CSVSafariPassword& CSVSafariPassword::operator=(const CSVSafariPassword&) = + default; +CSVSafariPassword& CSVSafariPassword::operator=(CSVSafariPassword&&) = default; +CSVSafariPassword::~CSVSafariPassword() = default; + +const std::string& CSVSafariPassword::GetTitle() const { + return title_; +} + +CSVSafariPassword::Status CSVSafariPassword::GetParseStatus() const { + return status_; +} + +const std::string& CSVSafariPassword::GetPassword() const { + return password_; +} + +const std::string& CSVSafariPassword::GetUsername() const { + return username_; +} + +const std::string& CSVSafariPassword::GetNotes() const { + return notes_; +} + +const base::expected& CSVSafariPassword::GetURL() const { + return url_; +} + +const base::expected& CSVSafariPassword::GetOTPAuthURL() + const { + return otp_auth_url_; +} + +} // namespace password_manager diff --git a/components/password_manager/core/browser/import/csv_safari_password.h b/components/password_manager/core/browser/import/csv_safari_password.h new file mode 100644 index 000000000000..320e5918a346 --- /dev/null +++ b/components/password_manager/core/browser/import/csv_safari_password.h @@ -0,0 +1,86 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_SAFARI_PASSWORD_H_ +#define BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_SAFARI_PASSWORD_H_ + +#include + +#include +#include + +#include "base/containers/flat_map.h" +#include "base/types/expected.h" +#include "url/gurl.h" + +namespace password_manager { + +class CSVSafariPassword { + public: + enum class Label { kTitle, kURL, kUsername, kPassword, kNotes, kOTPAuthURL }; + using ColumnMap = base::flat_map; + + // Status describes parsing errors. + enum class Status { + kOK = 0, + kSyntaxError = 1, + kSemanticError = 2, + }; + + CSVSafariPassword(); + explicit CSVSafariPassword(const ColumnMap& map, std::string_view csv_row); + explicit CSVSafariPassword(std::string title, + GURL url, + std::string username, + std::string password, + std::string notes, + GURL otp_auth_url, + Status status); + // This constructor creates a valid CSVPassword but with an invalid_url, i.e. + // the url is not a valid GURL. + explicit CSVSafariPassword(std::string title, + std::string invalid_url, + std::string username, + std::string password, + std::string note, + GURL otp_auth_url, + Status status); + CSVSafariPassword(const CSVSafariPassword&); + CSVSafariPassword(CSVSafariPassword&&); + CSVSafariPassword& operator=(const CSVSafariPassword&); + CSVSafariPassword& operator=(CSVSafariPassword&&); + ~CSVSafariPassword(); + + friend bool operator==(const CSVSafariPassword&, + const CSVSafariPassword&) = default; + + Status GetParseStatus() const; + + const std::string& GetTitle() const; + + const std::string& GetPassword() const; + + const std::string& GetUsername() const; + + const std::string& GetNotes() const; + + const base::expected& GetURL() const; + + const base::expected& GetOTPAuthURL() const; + + private: + std::string title_; + base::expected url_ = base::unexpected(""); + std::string username_; + std::string password_; + std::string notes_; + base::expected otp_auth_url_ = base::unexpected(""); + + Status status_; +}; + +} // namespace password_manager + +#endif // BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_SAFARI_PASSWORD_H_ diff --git a/components/password_manager/core/browser/import/csv_safari_password_iterator.cc b/components/password_manager/core/browser/import/csv_safari_password_iterator.cc new file mode 100644 index 000000000000..6e08b5f13050 --- /dev/null +++ b/components/password_manager/core/browser/import/csv_safari_password_iterator.cc @@ -0,0 +1,95 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/password_manager/core/browser/import/csv_safari_password_iterator.h" + +#include +#include + +#include "base/check.h" +#include "base/strings/string_util.h" +#include "components/password_manager/core/browser/import/csv_password_iterator.h" + +namespace password_manager { + +namespace { + +// Takes the |rest| of the CSV lines, returns the first one and stores the +// remaining ones back in |rest|. +std::string_view ExtractFirstRow(std::string_view* rest) { + DCHECK(rest); + if (!rest->empty()) { + return ConsumeCSVLine(rest); + } + return std::string_view(); +} + +} // namespace + +CSVSafariPasswordIterator::CSVSafariPasswordIterator() = default; + +CSVSafariPasswordIterator::CSVSafariPasswordIterator( + const CSVSafariPassword::ColumnMap& map, + std::string_view csv) + : map_(&map), csv_rest_(csv) { + SeekToNextValidRow(); +} + +CSVSafariPasswordIterator::CSVSafariPasswordIterator( + const CSVSafariPasswordIterator& other) { + *this = other; +} + +CSVSafariPasswordIterator& CSVSafariPasswordIterator::operator=( + const CSVSafariPasswordIterator& other) { + map_ = other.map_; + csv_rest_ = other.csv_rest_; + csv_row_ = other.csv_row_; + if (map_) { + password_.emplace(*map_, csv_row_); + } else { + password_.reset(); + } + return *this; +} + +CSVSafariPasswordIterator::~CSVSafariPasswordIterator() = default; + +CSVSafariPasswordIterator& CSVSafariPasswordIterator::operator++() { + SeekToNextValidRow(); + return *this; +} + +CSVSafariPasswordIterator CSVSafariPasswordIterator::operator++(int) { + CSVSafariPasswordIterator old = *this; + ++*this; + return old; +} + +bool CSVSafariPasswordIterator::operator==( + const CSVSafariPasswordIterator& other) const { + // There is no need to compare |password_|, because it is determined by |map_| + // and |csv_row_|. + + return csv_row_ == other.csv_row_ && csv_rest_ == other.csv_rest_ && + // The column map should reference the same map if the iterators come + // from the same sequence, and iterators from different sequences are + // not considered equal. Therefore the maps' addresses are checked + // instead of their contents. + map_ == other.map_; +} + +void CSVSafariPasswordIterator::SeekToNextValidRow() { + DCHECK(map_); + do { + csv_row_ = base::TrimString(ExtractFirstRow(&csv_rest_), "\r \t", + base::TRIM_LEADING); + } while ( + // Skip over empty lines. + csv_row_.empty() && !csv_rest_.empty()); + password_.emplace(*map_, csv_row_); +} + +} // namespace password_manager diff --git a/components/password_manager/core/browser/import/csv_safari_password_iterator.h b/components/password_manager/core/browser/import/csv_safari_password_iterator.h new file mode 100644 index 000000000000..7f7b334813ce --- /dev/null +++ b/components/password_manager/core/browser/import/csv_safari_password_iterator.h @@ -0,0 +1,56 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_SAFARI_PASSWORD_ITERATOR_H_ +#define BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_SAFARI_PASSWORD_ITERATOR_H_ + +#include + +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "brave/components/password_manager/core/browser/import/csv_safari_password.h" + +namespace password_manager { + +class CSVSafariPasswordIterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = CSVSafariPassword; + using difference_type = std::ptrdiff_t; + using pointer = const value_type*; + using reference = const value_type&; + + CSVSafariPasswordIterator(); + explicit CSVSafariPasswordIterator(const CSVSafariPassword::ColumnMap& map, + std::string_view csv); + CSVSafariPasswordIterator(const CSVSafariPasswordIterator&); + CSVSafariPasswordIterator& operator=(const CSVSafariPasswordIterator&); + ~CSVSafariPasswordIterator(); + + reference operator*() const { + DCHECK(password_); + return *password_; + } + pointer operator->() const { return &**this; } + + CSVSafariPasswordIterator& operator++(); + CSVSafariPasswordIterator operator++(int); + bool operator==(const CSVSafariPasswordIterator& other) const; + + private: + void SeekToNextValidRow(); + + raw_ptr map_ = nullptr; + std::string_view csv_rest_; + std::string_view csv_row_; + std::optional password_; +}; + +} // namespace password_manager + +#endif // BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_SAFARI_PASSWORD_ITERATOR_H_ diff --git a/components/password_manager/core/browser/import/csv_safari_password_sequence.cc b/components/password_manager/core/browser/import/csv_safari_password_sequence.cc new file mode 100644 index 000000000000..18858c0f6870 --- /dev/null +++ b/components/password_manager/core/browser/import/csv_safari_password_sequence.cc @@ -0,0 +1,168 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/password_manager/core/browser/import/csv_safari_password_sequence.h" + +#include +#include +#include +#include + +#include "base/containers/fixed_flat_map.h" +#include "base/containers/flat_set.h" +#include "base/not_fatal_until.h" +#include "base/strings/string_util.h" +#include "brave/components/password_manager/core/browser/import/csv_safari_password.h" +#include "brave/components/password_manager/core/browser/import/csv_safari_password_iterator.h" +#include "components/password_manager/core/browser/import/csv_field_parser.h" +#include "components/password_manager/core/browser/import/csv_password_iterator.h" + +namespace password_manager { + +namespace { + +// Given a CSV column |name|, returns a pointer to the matching +// CSVSafariPassword::Label or nullptr if the column name is not recognised. +const CSVSafariPassword::Label* NameToLabel(std::string_view name) { + using Label = CSVSafariPassword::Label; + // Recognised column names for origin URL, usernames and passwords. + static constexpr auto kLabelMap = + base::MakeFixedFlatMap({ + {"title", Label::kTitle}, + {"name", Label::kTitle}, + + {"url", Label::kURL}, + {"website", Label::kURL}, + {"origin", Label::kURL}, + {"hostname", Label::kURL}, + {"login_uri", Label::kURL}, + + {"username", Label::kUsername}, + {"user", Label::kUsername}, + {"login", Label::kUsername}, + {"account", Label::kUsername}, + {"login_username", Label::kUsername}, + + {"password", Label::kPassword}, + {"login_password", Label::kPassword}, + + {"note", Label::kNotes}, + {"notes", Label::kNotes}, + {"comment", Label::kNotes}, + {"comments", Label::kNotes}, + }); + + std::string trimmed_name; + // Trim leading/trailing whitespaces from |name|. + base::TrimWhitespaceASCII(name, base::TRIM_ALL, &trimmed_name); + auto it = kLabelMap.find(base::ToLowerASCII(trimmed_name)); + return it != kLabelMap.end() ? &it->second : nullptr; +} + +// Given |name| of a note column, returns its priority. +size_t GetNoteHeaderPriority(std::string_view name) { + DCHECK_EQ(*NameToLabel(name), CSVSafariPassword::Label::kNotes); + // Mapping names for note columns to their priorities. + static constexpr auto kNotesLabelsPriority = + base::MakeFixedFlatMap({ + {"note", 0}, + {"notes", 1}, + {"comment", 2}, + {"comments", 3}, + }); + + // TODO(crbug.com/40246323): record a metric if there multiple "note" columns + // in one file and which names are used. + + std::string trimmed_name; + // Trim leading/trailing whitespaces from |name|. + base::TrimWhitespaceASCII(name, base::TRIM_ALL, &trimmed_name); + auto it = kNotesLabelsPriority.find(base::ToLowerASCII(trimmed_name)); + CHECK(it != kNotesLabelsPriority.end(), base::NotFatalUntil::M130); + return it->second; +} + +} // namespace + +CSVSafariPasswordSequence::CSVSafariPasswordSequence(std::string csv) + : csv_(std::move(csv)) { + // Sanity check. + if (csv_.empty()) { + result_ = CSVSafariPassword::Status::kSyntaxError; + return; + } + data_rows_ = csv_; + + // Construct ColumnMap. + std::string_view first = ConsumeCSVLine(&data_rows_); + size_t col_index = 0; + + constexpr size_t kMaxPriority = 101; + // Mapping "note column index" -> "header name priority". + size_t note_column_index, note_column_priority = kMaxPriority; + + for (CSVFieldParser parser(first); parser.HasMoreFields(); ++col_index) { + std::string_view name; + if (!parser.NextField(&name)) { + result_ = CSVSafariPassword::Status::kSyntaxError; + return; + } + + if (const CSVSafariPassword::Label* label = NameToLabel(name)) { + // If there are multiple columns matching one of the accepted "note" field + // names, the one with the lowest priority should be used. + if (*label == CSVSafariPassword::Label::kNotes) { + size_t note_priority = GetNoteHeaderPriority(name); + if (note_column_priority > note_priority) { + note_column_index = col_index; + note_column_priority = note_priority; + } + continue; + } + map_[col_index] = *label; + } + } + + if (note_column_priority != kMaxPriority) { + map_[note_column_index] = CSVSafariPassword::Label::kNotes; + } + + base::flat_set all_labels; + for (const auto& kv : map_) { + if (!all_labels.insert(kv.second).second) { + // Multiple columns share the same label. + result_ = CSVSafariPassword::Status::kSemanticError; + return; + } + } + + // Check that each of the required labels is assigned to a column. + if (!all_labels.contains(CSVSafariPassword::Label::kURL) || + !all_labels.contains(CSVSafariPassword::Label::kUsername) || + !all_labels.contains(CSVSafariPassword::Label::kPassword)) { + result_ = CSVSafariPassword::Status::kSemanticError; + return; + } +} + +CSVSafariPasswordSequence::CSVSafariPasswordSequence( + CSVSafariPasswordSequence&&) = default; +CSVSafariPasswordSequence& CSVSafariPasswordSequence::operator=( + CSVSafariPasswordSequence&&) = default; + +CSVSafariPasswordSequence::~CSVSafariPasswordSequence() = default; + +CSVSafariPasswordIterator CSVSafariPasswordSequence::begin() const { + if (result_ != CSVSafariPassword::Status::kOK) { + return end(); + } + return CSVSafariPasswordIterator(map_, data_rows_); +} + +CSVSafariPasswordIterator CSVSafariPasswordSequence::end() const { + return CSVSafariPasswordIterator(map_, std::string_view()); +} + +} // namespace password_manager diff --git a/components/password_manager/core/browser/import/csv_safari_password_sequence.h b/components/password_manager/core/browser/import/csv_safari_password_sequence.h new file mode 100644 index 000000000000..e328045cf6fd --- /dev/null +++ b/components/password_manager/core/browser/import/csv_safari_password_sequence.h @@ -0,0 +1,43 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_SAFARI_PASSWORD_SEQUENCE_H_ +#define BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_SAFARI_PASSWORD_SEQUENCE_H_ + +#include +#include + +#include "brave/components/password_manager/core/browser/import/csv_safari_password.h" +#include "brave/components/password_manager/core/browser/import/csv_safari_password_iterator.h" + +namespace password_manager { + +class CSVSafariPasswordSequence { + public: + explicit CSVSafariPasswordSequence(std::string csv); + CSVSafariPasswordSequence(const CSVSafariPasswordSequence&) = delete; + CSVSafariPasswordSequence& operator=(const CSVSafariPasswordSequence&) = + delete; + + CSVSafariPasswordSequence(CSVSafariPasswordSequence&&); + CSVSafariPasswordSequence& operator=(CSVSafariPasswordSequence&&); + + ~CSVSafariPasswordSequence(); + + CSVSafariPasswordIterator begin() const; + CSVSafariPasswordIterator end() const; + + CSVSafariPassword::Status result() const { return result_; } + + private: + std::string csv_; + CSVSafariPassword::ColumnMap map_; + std::string_view data_rows_; + CSVSafariPassword::Status result_ = CSVSafariPassword::Status::kOK; +}; + +} // namespace password_manager + +#endif // BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_CSV_SAFARI_PASSWORD_SEQUENCE_H_ diff --git a/components/password_manager/core/browser/import/safari_import_results.cc b/components/password_manager/core/browser/import/safari_import_results.cc new file mode 100644 index 000000000000..5e230f8d37d2 --- /dev/null +++ b/components/password_manager/core/browser/import/safari_import_results.cc @@ -0,0 +1,29 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/password_manager/core/browser/import/safari_import_results.h" + +namespace password_manager { + +SafariImportEntry::SafariImportEntry() = default; +SafariImportEntry::SafariImportEntry(const SafariImportEntry& other) = default; +SafariImportEntry::SafariImportEntry(SafariImportEntry&& other) = default; +SafariImportEntry::~SafariImportEntry() = default; +SafariImportEntry& SafariImportEntry::operator=( + const SafariImportEntry& entry) = default; +SafariImportEntry& SafariImportEntry::operator=(SafariImportEntry&& entry) = + default; + +SafariImportResults::SafariImportResults() = default; +SafariImportResults::SafariImportResults(const SafariImportResults& other) = + default; +SafariImportResults::SafariImportResults(SafariImportResults&& other) = default; +SafariImportResults::~SafariImportResults() = default; +SafariImportResults& SafariImportResults::operator=( + const SafariImportResults& entry) = default; +SafariImportResults& SafariImportResults::operator=( + SafariImportResults&& entry) = default; + +} // namespace password_manager diff --git a/components/password_manager/core/browser/import/safari_import_results.h b/components/password_manager/core/browser/import/safari_import_results.h new file mode 100644 index 000000000000..0754ef180ef5 --- /dev/null +++ b/components/password_manager/core/browser/import/safari_import_results.h @@ -0,0 +1,124 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_SAFARI_IMPORT_RESULTS_H_ +#define BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_SAFARI_IMPORT_RESULTS_H_ + +#include +#include + +namespace password_manager { + +struct SafariImportEntry { + // Matches api::passwords_private::SafariImportEntryStatus. + // Needs to be kept in sync with PasswordManagerSafariImportEntryStatus in + // tools/metrics/histograms/enums.xml + enum Status { + // Should not be used. + NONE = 0, + // Any other error state. + UNKNOWN_ERROR = 1, + // Missing password field. + MISSING_PASSWORD = 2, + // Missing url field. + MISSING_URL = 3, + // Bad url formatting. + INVALID_URL = 4, + // Deprecated in crrev.com/c/4478954. + // NON_ASCII_URL = 5, + // URL is too long. + LONG_URL = 6, + // Password is too long. + LONG_PASSWORD = 7, + // Username is too long. + LONG_USERNAME = 8, + // Credential is already stored in profile store. + CONFLICT_PROFILE = 9, + // Credential is already stored in account store. + CONFLICT_ACCOUNT = 10, + // Note is too long. + LONG_NOTE = 11, + // Concatenation of imported and local notes is too long. + LONG_CONCATENATED_NOTE = 12, + // Valid credential. + VALID = 13, + kMaxValue = VALID + }; + + // The status of parsing for individual row that represents a credential + // during import process. + Status status = UNKNOWN_ERROR; + + // The url of the credential. + std::string url; + + // The username of the credential. + std::string username; + + // The password of the credential. + std::string password; + + // Unique identifier of the credential. + int id = 0; + + SafariImportEntry(); + SafariImportEntry(const SafariImportEntry& other); + SafariImportEntry(SafariImportEntry&& other); + ~SafariImportEntry(); + + SafariImportEntry& operator=(const SafariImportEntry& entry); + SafariImportEntry& operator=(SafariImportEntry&& entry); +}; + +struct SafariImportResults { + // Matches api::passwords_private::SafariImportResultsStatus. + enum Status { + // Should not be used. + NONE = 0, + // Any other error state. + UNKNOWN_ERROR = 1, + // Data was fully or partially imported. + SUCCESS = 2, + // Failed to read provided file. + IO_ERROR = 3, + // Header is missing, invalid or could not be read. + BAD_FORMAT = 4, + // File selection dismissed. + DISMISSED = 5, + // Size of the chosen file exceeds the limit. + MAX_FILE_SIZE = 6, + // User has already started the import flow in a different window. + IMPORT_ALREADY_ACTIVE = 7, + // User tried to import too many passwords from one file. + NUM_PASSWORDS_EXCEEDED = 8, + // Conflicts found and they need to be resolved by the user. + CONFLICTS = 9, + kMaxValue = CONFLICTS + }; + + // General status of the triggered password import process. + Status status = NONE; + + // Number of successfully imported passwords. + size_t number_imported = 0; + + // Possibly empty, list of credentials that should be shown to the user. + std::vector displayed_entries; + + // Possibly not set, name of file that user has chosen for the import. + std::string file_name; + + SafariImportResults(); + SafariImportResults(const SafariImportResults& other); + SafariImportResults(SafariImportResults&& other); + ~SafariImportResults(); + + SafariImportResults& operator=(const SafariImportResults& entry); + SafariImportResults& operator=(SafariImportResults&& entry); +}; + +} // namespace password_manager + +#endif // BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_SAFARI_IMPORT_RESULTS_H_ diff --git a/components/password_manager/core/browser/import/safari_password_importer.cc b/components/password_manager/core/browser/import/safari_password_importer.cc new file mode 100644 index 000000000000..c187376f529e --- /dev/null +++ b/components/password_manager/core/browser/import/safari_password_importer.cc @@ -0,0 +1,579 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/password_manager/core/browser/import/safari_password_importer.h" + +#include +#include +#include +#include + +#include "base/barrier_closure.h" +#include "base/files/file_util.h" +#include "base/functional/bind.h" +#include "base/location.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/task/thread_pool.h" +#include "base/types/expected.h" +#include "base/types/expected_macros.h" +#include "brave/components/password_manager/core/browser/import/csv_safari_password.h" +#include "brave/components/password_manager/core/browser/import/csv_safari_password_sequence.h" +#include "brave/components/password_manager/services/csv_password/csv_safari_password_parser_impl.h" +#include "build/blink_buildflags.h" +#include "build/build_config.h" +#include "components/password_manager/core/browser/password_form.h" +#include "components/password_manager/core/browser/ui/credential_ui_entry.h" +#include "components/password_manager/core/browser/ui/credential_utils.h" +#include "components/password_manager/core/browser/ui/saved_passwords_presenter.h" +#include "components/password_manager/core/common/password_manager_constants.h" + +#if BUILDFLAG(USE_BLINK) +#include "brave/components/password_manager/services/csv_password/csv_safari_password_parser_service.h" +#endif + +namespace password_manager { + +SafariIncomingPasswords::SafariIncomingPasswords() = default; +SafariIncomingPasswords::~SafariIncomingPasswords() = default; +SafariIncomingPasswords::SafariIncomingPasswords( + SafariIncomingPasswords&& other) = default; +SafariIncomingPasswords& SafariIncomingPasswords::operator=( + SafariIncomingPasswords&& other) = default; + +struct ConflictsResolutionCache { + ConflictsResolutionCache( + SafariIncomingPasswords incoming_passwords, + std::vector> conflicts, + SafariImportResults results, + base::Time start_time) + : incoming_passwords(std::move(incoming_passwords)), + conflicts(std::move(conflicts)), + results(std::move(results)), + start_time(start_time) {} + ~ConflictsResolutionCache() = default; + + SafariIncomingPasswords incoming_passwords; + std::vector> conflicts; + SafariImportResults results; + base::Time start_time; +}; + +namespace { + +const base::FilePath::CharType kFileExtension[] = FILE_PATH_LITERAL("csv"); +const int32_t kMaxFileSizeBytes = 150 * 1024; + +base::expected ReadFileToString( + const base::FilePath& path) { + std::optional file_size = base::GetFileSize(path); + + if (file_size.has_value()) { + if (file_size.value() > kMaxFileSizeBytes) { + return base::unexpected(SafariImportResults::Status::MAX_FILE_SIZE); + } + } + + std::string contents; + if (!base::ReadFileToString(path, &contents)) { + return base::unexpected(SafariImportResults::Status::IO_ERROR); + } + + return std::move(contents); +} + +SafariImportEntry::Status GetConflictType( + password_manager::PasswordForm::Store target_store) { + switch (target_store) { + case PasswordForm::Store::kProfileStore: + return SafariImportEntry::Status::CONFLICT_PROFILE; + case PasswordForm::Store::kAccountStore: + return SafariImportEntry::Status::CONFLICT_ACCOUNT; + case PasswordForm::Store::kNotSet: + return SafariImportEntry::Status::UNKNOWN_ERROR; + default: + NOTREACHED(); + } +} + +SafariImportEntry CreateFailedSafariImportEntry( + const CredentialUIEntry& credential, + const SafariImportEntry::Status status) { + SafariImportEntry result; + result.url = credential.GetAffiliatedDomains()[0].name; + result.username = base::UTF16ToUTF8(credential.username); + result.status = status; + return result; +} + +SafariImportEntry CreateValidSafariImportEntry( + const CredentialUIEntry& credential, + int id) { + SafariImportEntry result; + result.id = id; + result.url = credential.GetAffiliatedDomains()[0].name; + result.username = base::UTF16ToUTF8(credential.username); + result.password = base::UTF16ToUTF8(credential.password); + result.status = SafariImportEntry::VALID; + return result; +} + +base::expected +CSVSafariPasswordToCredentialUIEntry( + const CSVSafariPassword& csv_safari_password, + PasswordForm::Store store) { + auto with_status = [&](SafariImportEntry::Status status) { + SafariImportEntry entry; + entry.status = status; + // The raw URL is shown in the errors list in the UI to make it easier to + // match the listed entry with the one in the CSV file. + auto url = csv_safari_password.GetURL(); + entry.url = url.has_value() ? url.value().spec() : url.error(); + entry.username = csv_safari_password.GetUsername(); + return entry; + }; + + if (csv_safari_password.GetParseStatus() != CSVSafariPassword::Status::kOK) { + return base::unexpected( + with_status(SafariImportEntry::Status::UNKNOWN_ERROR)); + } + + const std::string& password = csv_safari_password.GetPassword(); + if (password.empty()) { + return base::unexpected( + with_status(SafariImportEntry::Status::MISSING_PASSWORD)); + } + if (password.length() > 1000) { + return base::unexpected( + with_status(SafariImportEntry::Status::LONG_PASSWORD)); + } + + if (csv_safari_password.GetUsername().length() > 1000) { + return base::unexpected( + with_status(SafariImportEntry::Status::LONG_USERNAME)); + } + + if (csv_safari_password.GetNotes().length() > 1000) { + return base::unexpected(with_status(SafariImportEntry::Status::LONG_NOTE)); + } + + ASSIGN_OR_RETURN( + GURL url, csv_safari_password.GetURL(), [&](const std::string& error) { + return with_status(error.empty() + ? SafariImportEntry::Status::MISSING_URL + : SafariImportEntry::Status::INVALID_URL); + }); + if (url.spec().length() > 2048) { + return base::unexpected(with_status(SafariImportEntry::Status::LONG_URL)); + } + if (!IsValidPasswordURL(url)) { + return base::unexpected( + with_status(SafariImportEntry::Status::INVALID_URL)); + } + + auto credential = CSVPassword( + url, csv_safari_password.GetUsername(), password, + csv_safari_password.GetNotes(), + static_cast(csv_safari_password.GetParseStatus())); + return CredentialUIEntry(credential, store); +} + +std::optional GetConflictingCredential( + const std::map>& + credentials_by_username, + const CredentialUIEntry& imported_credential) { + auto it = credentials_by_username.find(imported_credential.username); + if (it != credentials_by_username.end()) { + // Iterate over all local credentials with matching username. + for (const CredentialUIEntry& local_credential : it->second) { + // Check if `local_credential` has matching `signon_realm`, but different + // `password`. + if (local_credential.password != imported_credential.password && + base::ranges::any_of( + local_credential.facets, + [&imported_credential](const CredentialFacet& facet) { + return facet.signon_realm == + imported_credential.facets[0].signon_realm; + })) { + return local_credential; + } + } + } + return std::nullopt; +} + +std::vector GetMatchingPasswordForms( + SavedPasswordsPresenter* presenter, + const CredentialUIEntry& credential, + PasswordForm::Store store) { + // Returns matching local forms for a given `credential`, excluding grouped + // forms with different `signon_realm`. + CHECK(presenter); + std::vector results; + base::ranges::copy_if( + presenter->GetCorrespondingPasswordForms(credential), + std::back_inserter(results), [&](const PasswordForm& form) { + return form.signon_realm == credential.GetFirstSignonRealm() && + store == form.in_store; + }); + return results; +} + +std::u16string ComputeNotesConcatenation(const std::u16string& local_note, + const std::u16string& imported_note, + SafariNotesImportMetrics& metrics) { + CHECK_LE(imported_note.size(), + static_cast(constants::kMaxPasswordNoteLength)); + + if (imported_note.empty()) { + return local_note; + } + + if (local_note.empty()) { + return imported_note; + } + + if (local_note == imported_note) { + metrics.notes_duplicates_per_file_count++; + return local_note; + } + + if (local_note.find(imported_note) != std::u16string::npos) { + metrics.notes_substrings_per_file_count++; + return local_note; + } + + return base::JoinString(/*parts=*/{local_note, imported_note}, u"\n"); +} + +void MergeNotesOrReportError(const std::vector& local_forms, + const CredentialUIEntry& imported_credential, + SafariImportResults& results, + std::vector& edit_forms, + SafariNotesImportMetrics& metrics) { + const std::u16string local_note = CredentialUIEntry(local_forms).note; + const std::u16string& imported_note = imported_credential.note; + const std::u16string concatenation = + ComputeNotesConcatenation(local_note, imported_note, metrics); + + if (concatenation.size() > constants::kMaxPasswordNoteLength) { + // Notes concatenation size should not exceed 1000 characters. + results.displayed_entries.push_back(CreateFailedSafariImportEntry( + imported_credential, + SafariImportEntry::Status::LONG_CONCATENATED_NOTE)); + return; + } + + if (concatenation != local_note) { + // Local credential needs to be updated with concatenation. + for (PasswordForm form : local_forms) { + form.SetNoteWithEmptyUniqueDisplayName(concatenation); + edit_forms.emplace_back(std::move(form)); + } + metrics.notes_concatenations_per_file_count++; + } + + results.number_imported++; +} + +bool DefaultDeleteFunction(const base::FilePath& file) { + return base::DeleteFile(file); +} + +void ProcessParsedCredential( + const CredentialUIEntry& imported_credential, + SavedPasswordsPresenter* presenter, + const std::map>& + credentials_by_username, + PasswordForm::Store to_store, + SafariIncomingPasswords& incoming_passwords, + std::vector>& conflicts, + SafariImportResults& results, + SafariNotesImportMetrics& notes_metrics, + size_t& duplicates_count) { + if (!imported_credential.note.empty()) { + notes_metrics.notes_per_file_count++; + } + + // Check if there are local credentials with the same signon_realm and + // username, but different password. Such credentials are considered + // conflicts. + std::optional conflicting_credential = + GetConflictingCredential(credentials_by_username, imported_credential); + if (conflicting_credential.has_value()) { + std::vector forms = GetMatchingPasswordForms( + presenter, conflicting_credential.value(), to_store); + // Password notes are not taken into account when conflicting passwords + // are overwritten. Only the local note is persisted. + for (PasswordForm& form : forms) { + form.password_value = imported_credential.password; + } + conflicts.push_back(std::move(forms)); + return; + } + + // Check for duplicates. + std::vector forms = + GetMatchingPasswordForms(presenter, imported_credential, to_store); + if (!forms.empty()) { + duplicates_count++; + + if (imported_credential.note.empty()) { + // Duplicates are reported as successfully imported credentials. + results.number_imported++; + return; + } + + MergeNotesOrReportError( + /*local_forms=*/forms, /*imported_credential=*/imported_credential, + /*results=*/results, /*edit_forms=*/incoming_passwords.edit_forms, + /*metrics=*/notes_metrics); + return; + } + + // Valid credential with no conflicts and no duplicates. + incoming_passwords.add_credentials.push_back(imported_credential); +} + +} // namespace + +SafariPasswordImporter::SafariPasswordImporter( + SavedPasswordsPresenter* presenter) + : delete_function_(base::BindRepeating(&DefaultDeleteFunction)), + presenter_(presenter) {} + +SafariPasswordImporter::~SafariPasswordImporter() = default; + +const mojo::Remote& +SafariPasswordImporter::GetParser() { + if (!parser_) { +#if BUILDFLAG(USE_BLINK) + parser_ = LaunchCSVSafariPasswordParser(); +#else + mojo::PendingReceiver receiver = + parser_.BindNewPipeAndPassReceiver(); + + // Instantiate the implementation and bind it to the receiver + new CSVSafariPasswordParserImpl(std::move(receiver)); +#endif + + // Ensure the remote resets itself on disconnect + parser_.reset_on_disconnect(); + } + return parser_; +} + +void SafariPasswordImporter::ParseCSVSafariPasswordsInSandbox( + PasswordForm::Store to_store, + SafariImportResultsCallback results_callback, + base::expected result) { + // Currently, CSV is the only supported format. + if (result.has_value()) { + GetParser()->ParseCSV( + std::move(result.value()), + base::BindOnce(&SafariPasswordImporter::ConsumePasswords, + weak_ptr_factory_.GetWeakPtr(), to_store, + std::move(results_callback))); + } else { + SafariImportResults results; + results.status = result.error(); + // Importer is reset to the initial state, due to the error. + state_ = State::kNotStarted; + std::move(results_callback).Run(std::move(results)); + } +} + +void SafariPasswordImporter::Import( + const base::FilePath& path, + PasswordForm::Store to_store, + SafariImportResultsCallback results_callback) { + // Blocks concurrent import requests. + state_ = State::kInProgress; + file_path_ = path; + + // Posting with USER_VISIBLE priority, because the result of the import is + // visible to the user in the password settings page. + base::ThreadPool::PostTaskAndReplyWithResult( + FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()}, + base::BindOnce(&ReadFileToString, path), + base::BindOnce(&SafariPasswordImporter::ParseCSVSafariPasswordsInSandbox, + weak_ptr_factory_.GetWeakPtr(), to_store, + std::move(results_callback))); +} + +void SafariPasswordImporter::ContinueImport( + const std::vector& selected_ids, + SafariImportResultsCallback results_callback) { + CHECK(IsState(State::kConflicts)); + CHECK(conflicts_cache_); + // Blocks concurrent import requests, when switching from `kConflicts` state. + state_ = State::kInProgress; + + for (int id : selected_ids) { + conflicts_cache_->results.number_imported++; + CHECK_LT(static_cast(id), conflicts_cache_->conflicts.size()); + for (const PasswordForm& form : conflicts_cache_->conflicts[id]) { + conflicts_cache_->incoming_passwords.edit_forms.push_back(form); + } + } + + ExecuteImport( + std::move(results_callback), std::move(conflicts_cache_->results), + std::move(conflicts_cache_->incoming_passwords), + conflicts_cache_->start_time, conflicts_cache_->conflicts.size()); + + conflicts_cache_.reset(); +} + +void SafariPasswordImporter::ConsumePasswords( + PasswordForm::Store to_store, + SafariImportResultsCallback results_callback, + mojom::CSVSafariPasswordSequencePtr seq) { + // Used to aggregate final results of the current import. + SafariImportResults results; + results.file_name = file_path_.BaseName().AsUTF8Unsafe(); + CHECK_EQ(results.number_imported, 0u); + + if (!seq) { + // A nullptr returned by the parser means a bad format. + results.status = SafariImportResults::Status::BAD_FORMAT; + // Importer is reset to the initial state, due to the error. + state_ = State::kNotStarted; + std::move(results_callback).Run(std::move(results)); + return; + } + if (seq->csv_passwords.size() > constants::kMaxPasswordsPerCSVFile) { + results.status = SafariImportResults::Status::NUM_PASSWORDS_EXCEEDED; + + // Importer is reset to the initial state, due to the error. + state_ = State::kNotStarted; + std::move(results_callback).Run(results); + return; + } + + // TODO(crbug.com/40225420): Either move to earlier point or update histogram. + base::Time start_time = base::Time::Now(); + // Used to compute conflicts and duplicates. + std::map> + credentials_by_username; + for (const CredentialUIEntry& credential : presenter_->GetSavedPasswords()) { + // Don't consider credentials from a store other than the target store. + if (credential.stored_in.contains(to_store)) { + credentials_by_username[credential.username].push_back(credential); + } + } + + SafariNotesImportMetrics notes_metrics; + size_t duplicates_count = 0; // Number of duplicates per imported file. + + // Aggregate all passwords that might need to be added or updated. + SafariIncomingPasswords incoming_passwords; + + // Conflicting credential that could be updated. Each nested vector + // represents one credential, i.e. all PasswordForm's in such a vector have + // the same signon_ream, username, password. + std::vector> conflicts; + + // Go over all canonically parsed passwords: + // 1) aggregate all valid ones in `incoming_passwords` to be passed over to + // the presenter. 2) aggregate all parsing errors in the `results`. + for (const CSVSafariPassword& csv_safari_password : seq->csv_passwords) { + base::expected credential = + CSVSafariPasswordToCredentialUIEntry(csv_safari_password, to_store); + + if (!credential.has_value()) { + results.displayed_entries.emplace_back(std::move(credential.error())); + continue; + } + + ProcessParsedCredential(credential.value(), presenter_, + credentials_by_username, to_store, + incoming_passwords, conflicts, results, + notes_metrics, duplicates_count); + } + + results.number_imported += incoming_passwords.add_credentials.size(); + + if (conflicts.empty()) { + for (const std::vector& forms : conflicts) { + results.displayed_entries.push_back(CreateFailedSafariImportEntry( + CredentialUIEntry(forms), GetConflictType(to_store))); + } + + ExecuteImport(std::move(results_callback), std::move(results), + std::move(incoming_passwords), start_time, conflicts.size()); + return; + } + + state_ = State::kConflicts; + SafariImportResults conflicts_results; + conflicts_results.status = SafariImportResults::CONFLICTS; + for (size_t idx = 0; idx < conflicts.size(); idx++) { + conflicts_results.displayed_entries.push_back( + CreateValidSafariImportEntry(CredentialUIEntry(conflicts[idx]), idx)); + } + + conflicts_cache_ = std::make_unique( + std::move(incoming_passwords), std::move(conflicts), std::move(results), + start_time); + + std::move(results_callback).Run(std::move(conflicts_results)); +} + +void SafariPasswordImporter::ExecuteImport( + SafariImportResultsCallback results_callback, + SafariImportResults results, + SafariIncomingPasswords incoming_passwords, + base::Time start_time, + size_t conflicts_count) { + // Run `results_callback` when both `AddCredentials` and + // `UpdatePasswordForms` have finished running. + auto barrier_done_callback = base::BarrierClosure( + 2, base::BindOnce(base::BindOnce( + &SafariPasswordImporter::ImportFinished, + weak_ptr_factory_.GetWeakPtr(), std::move(results_callback), + std::move(results), start_time, conflicts_count))); + + presenter_->AddCredentials(incoming_passwords.add_credentials, + PasswordForm::Type::kImported, + barrier_done_callback); + presenter_->UpdatePasswordForms(incoming_passwords.edit_forms, + barrier_done_callback); +} + +void SafariPasswordImporter::ImportFinished( + SafariImportResultsCallback results_callback, + SafariImportResults results, + base::Time start_time, + size_t conflicts_count) { + if (results.displayed_entries.empty()) { + // After successful import with no errors, the user has an option to delete + // the imported file. + state_ = State::kFinished; + } else { + // After successful import with some errors, the importer is reset to the + // initial state. + state_ = State::kNotStarted; + } + + results.status = SafariImportResults::Status::SUCCESS; + std::move(results_callback).Run(std::move(results)); +} + +void SafariPasswordImporter::DeleteFile() { + CHECK(IsState(State::kFinished)); + base::ThreadPool::PostTask( + FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT}, + base::BindOnce(base::IgnoreResult(delete_function_), file_path_)); +} + +// static +std::vector> +SafariPasswordImporter::GetSupportedFileExtensions() { + return std::vector>( + 1, std::vector(1, kFileExtension)); +} + +} // namespace password_manager diff --git a/components/password_manager/core/browser/import/safari_password_importer.h b/components/password_manager/core/browser/import/safari_password_importer.h new file mode 100644 index 000000000000..a66546a37a97 --- /dev/null +++ b/components/password_manager/core/browser/import/safari_password_importer.h @@ -0,0 +1,117 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_SAFARI_PASSWORD_IMPORTER_H_ +#define BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_SAFARI_PASSWORD_IMPORTER_H_ + +#include +#include +#include + +#include "base/files/file_path.h" +#include "base/functional/callback.h" +#include "base/types/expected.h" +#include "brave/components/password_manager/core/browser/import/safari_import_results.h" +#include "brave/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser.mojom.h" +#include "components/password_manager/core/browser/password_form.h" +#include "components/password_manager/core/browser/ui/credential_ui_entry.h" +#include "mojo/public/cpp/bindings/remote.h" + +namespace password_manager { + +struct SafariNotesImportMetrics { + size_t notes_per_file_count = 0; + size_t notes_duplicates_per_file_count = 0; + size_t notes_substrings_per_file_count = 0; + size_t notes_concatenations_per_file_count = 0; +}; + +struct SafariIncomingPasswords { + SafariIncomingPasswords(); + SafariIncomingPasswords(SafariIncomingPasswords&& other); + ~SafariIncomingPasswords(); + + SafariIncomingPasswords& operator=(SafariIncomingPasswords&& other); + + std::vector add_credentials; + std::vector edit_forms; +}; + +struct ConflictsResolutionCache; +class SavedPasswordsPresenter; + +class SafariPasswordImporter { + public: + enum State { + kNotStarted = 0, + kInProgress = 1, + kConflicts = 2, + kFinished = 3, + }; + + using ConsumePasswordsCallback = + mojom::CSVSafariPasswordParser::ParseCSVCallback; + + using SafariImportResultsCallback = + base::OnceCallback; + + using DeleteFileCallback = + base::RepeatingCallback; + + explicit SafariPasswordImporter(SavedPasswordsPresenter* presenter); + SafariPasswordImporter(const SafariPasswordImporter&) = delete; + SafariPasswordImporter& operator=(const SafariPasswordImporter&) = delete; + ~SafariPasswordImporter(); + + void Import(const base::FilePath& path, + PasswordForm::Store to_store, + SafariImportResultsCallback results_callback); + + void ContinueImport(const std::vector& selected_ids, + SafariImportResultsCallback results_callback); + + void DeleteFile(); + + bool IsState(State state) const { return state_ == state; } + + static std::vector> + GetSupportedFileExtensions(); + + private: + void ParseCSVSafariPasswordsInSandbox( + PasswordForm::Store to_store, + SafariImportResultsCallback results_callback, + base::expected result); + + void ConsumePasswords(PasswordForm::Store to_store, + SafariImportResultsCallback results_callback, + mojom::CSVSafariPasswordSequencePtr seq); + + void ExecuteImport(SafariImportResultsCallback results_callback, + SafariImportResults results, + SafariIncomingPasswords incoming_passwords, + base::Time start_time, + size_t conflicts_count); + + void ImportFinished(SafariImportResultsCallback results_callback, + SafariImportResults results, + base::Time start_time, + size_t conflicts_count); + + const mojo::Remote& GetParser(); + + mojo::Remote parser_; + State state_ = State::kNotStarted; + base::FilePath file_path_; + std::unique_ptr conflicts_cache_; + + DeleteFileCallback delete_function_; + const raw_ptr presenter_; + base::WeakPtrFactory weak_ptr_factory_{this}; +}; + +} // namespace password_manager + +#endif // BRAVE_COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_IMPORT_SAFARI_PASSWORD_IMPORTER_H_ diff --git a/components/password_manager/services/csv_password/BUILD.gn b/components/password_manager/services/csv_password/BUILD.gn new file mode 100644 index 000000000000..97063f5021e8 --- /dev/null +++ b/components/password_manager/services/csv_password/BUILD.gn @@ -0,0 +1,46 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//build/config/features.gni") + +source_set("lib") { + sources = [ + "csv_safari_password_parser_impl.cc", + "csv_safari_password_parser_impl.h", + ] + + deps = [ + "//base", + "//brave/components/password_manager/core/browser/import:csv", + "//mojo/public/cpp/bindings", + ] + + public_deps = [ + "//brave/components/password_manager/services/csv_password/public/mojom", + ] + + configs += [ "//build/config/compiler:wexit_time_destructors" ] +} + +if (use_blink) { + source_set("service") { + sources = [ + "csv_safari_password_parser_service.cc", + "csv_safari_password_parser_service.h", + ] + + deps = [ + "//components/strings", + "//content/public/browser", + ] + + public_deps = [ + "//brave/components/password_manager/services/csv_password/public/mojom", + "//mojo/public/cpp/bindings", + ] + + configs += [ "//build/config/compiler:wexit_time_destructors" ] + } +} diff --git a/components/password_manager/services/csv_password/DEPS b/components/password_manager/services/csv_password/DEPS new file mode 100644 index 000000000000..59a683a35b10 --- /dev/null +++ b/components/password_manager/services/csv_password/DEPS @@ -0,0 +1,10 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +include_rules = [ + "+components/strings", + "+content/public/browser", + "+mojo/public", +] diff --git a/components/password_manager/services/csv_password/csv_safari_password_parser_impl.cc b/components/password_manager/services/csv_password/csv_safari_password_parser_impl.cc new file mode 100644 index 000000000000..95da0779a471 --- /dev/null +++ b/components/password_manager/services/csv_password/csv_safari_password_parser_impl.cc @@ -0,0 +1,35 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/password_manager/services/csv_password/csv_safari_password_parser_impl.h" + +#include +#include + +#include "brave/components/password_manager/core/browser/import/csv_safari_password.h" +#include "brave/components/password_manager/core/browser/import/csv_safari_password_sequence.h" + +namespace password_manager { + +CSVSafariPasswordParserImpl::CSVSafariPasswordParserImpl( + mojo::PendingReceiver receiver) + : receiver_(this, std::move(receiver)) {} + +CSVSafariPasswordParserImpl::~CSVSafariPasswordParserImpl() = default; + +void CSVSafariPasswordParserImpl::ParseCSV(const std::string& raw_json, + ParseCSVCallback callback) { + mojom::CSVSafariPasswordSequencePtr result = nullptr; + CSVSafariPasswordSequence seq(raw_json); + if (seq.result() == CSVSafariPassword::Status::kOK) { + result = mojom::CSVSafariPasswordSequence::New(); + for (const auto& pwd : seq) { + result->csv_passwords.push_back(pwd); + } + } + std::move(callback).Run(std::move(result)); +} + +} // namespace password_manager diff --git a/components/password_manager/services/csv_password/csv_safari_password_parser_impl.h b/components/password_manager/services/csv_password/csv_safari_password_parser_impl.h new file mode 100644 index 000000000000..eda2936fcb31 --- /dev/null +++ b/components/password_manager/services/csv_password/csv_safari_password_parser_impl.h @@ -0,0 +1,38 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_PASSWORD_MANAGER_SERVICES_CSV_PASSWORD_CSV_SAFARI_PASSWORD_PARSER_IMPL_H_ +#define BRAVE_COMPONENTS_PASSWORD_MANAGER_SERVICES_CSV_PASSWORD_CSV_SAFARI_PASSWORD_PARSER_IMPL_H_ + +#include + +#include "brave/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser.mojom.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" + +namespace password_manager { + +// Implementation of the CSVSafariPasswordParser mojom interface. +class CSVSafariPasswordParserImpl : public mojom::CSVSafariPasswordParser { + public: + // Constructs a CSVSafariPasswordParserImpl bound to |receiver|. + explicit CSVSafariPasswordParserImpl( + mojo::PendingReceiver receiver); + ~CSVSafariPasswordParserImpl() override; + CSVSafariPasswordParserImpl(const CSVSafariPasswordParserImpl&) = delete; + CSVSafariPasswordParserImpl& operator=(const CSVSafariPasswordParserImpl&) = + delete; + + // mojom::CSVSafariPasswordParser: + void ParseCSV(const std::string& raw_json, + ParseCSVCallback callback) override; + + private: + mojo::Receiver receiver_; +}; + +} // namespace password_manager + +#endif // BRAVE_COMPONENTS_PASSWORD_MANAGER_SERVICES_CSV_PASSWORD_CSV_SAFARI_PASSWORD_PARSER_IMPL_H_ diff --git a/components/password_manager/services/csv_password/csv_safari_password_parser_service.cc b/components/password_manager/services/csv_password/csv_safari_password_parser_service.cc new file mode 100644 index 000000000000..1b19f3308057 --- /dev/null +++ b/components/password_manager/services/csv_password/csv_safari_password_parser_service.cc @@ -0,0 +1,23 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/password_manager/services/csv_password/csv_safari_password_parser_service.h" + +#include "components/strings/grit/components_strings.h" +#include "content/public/browser/service_process_host.h" + +namespace password_manager { + +mojo::Remote +LaunchCSVSafariPasswordParser() { + return content::ServiceProcessHost::Launch< + password_manager::mojom::CSVSafariPasswordParser>( + content::ServiceProcessHost::Options() + .WithDisplayName( + IDS_PASSWORD_MANAGER_CSV_PASSWORD_PARSER_SERVICE_DISPLAY_NAME) + .Pass()); +} + +} // namespace password_manager diff --git a/components/password_manager/services/csv_password/csv_safari_password_parser_service.h b/components/password_manager/services/csv_password/csv_safari_password_parser_service.h new file mode 100644 index 000000000000..8005a4f46767 --- /dev/null +++ b/components/password_manager/services/csv_password/csv_safari_password_parser_service.h @@ -0,0 +1,23 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_PASSWORD_MANAGER_SERVICES_CSV_PASSWORD_CSV_SAFARI_PASSWORD_PARSER_SERVICE_H_ +#define BRAVE_COMPONENTS_PASSWORD_MANAGER_SERVICES_CSV_PASSWORD_CSV_SAFARI_PASSWORD_PARSER_SERVICE_H_ + +#include "brave/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser.mojom.h" +#include "mojo/public/cpp/bindings/remote.h" + +namespace password_manager { + +// Launches a new instance of the CSVSafariPasswordParser service in an +// isolated, sandboxed process, and returns a remote interface to control the +// service. The lifetime of the process is tied to that of the Remote. May be +// called from any thread. +mojo::Remote +LaunchCSVSafariPasswordParser(); + +} // namespace password_manager + +#endif // BRAVE_COMPONENTS_PASSWORD_MANAGER_SERVICES_CSV_PASSWORD_CSV_SAFARI_PASSWORD_PARSER_SERVICE_H_ diff --git a/components/password_manager/services/csv_password/public/mojom/BUILD.gn b/components/password_manager/services/csv_password/public/mojom/BUILD.gn new file mode 100644 index 000000000000..d74390e30ddb --- /dev/null +++ b/components/password_manager/services/csv_password/public/mojom/BUILD.gn @@ -0,0 +1,30 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//mojo/public/tools/bindings/mojom.gni") + +mojom("mojom") { + sources = [ "csv_safari_password_parser.mojom" ] + + public_deps = [ + "//sandbox/policy/mojom", + "//url/mojom:url_mojom_gurl", + ] + + cpp_typemaps = [ + { + types = [ + { + mojom = "password_manager.mojom.CSVSafariPassword" + cpp = "password_manager::CSVSafariPassword" + }, + ] + traits_headers = [ "csv_safari_password_parser_traits.h" ] + traits_sources = [ "csv_safari_password_parser_traits.cc" ] + traits_public_deps = + [ "//brave/components/password_manager/core/browser/import:csv" ] + }, + ] +} diff --git a/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser.mojom b/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser.mojom new file mode 100644 index 000000000000..ad88f6cc40f4 --- /dev/null +++ b/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser.mojom @@ -0,0 +1,37 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +module password_manager.mojom; + +import "sandbox/policy/mojom/sandbox.mojom"; +import "url/mojom/url.mojom"; + +struct CSVSafariPasswordSequence { + array csv_passwords; +}; + +struct CSVSafariPassword { + enum Status { + kOK = 0, + kSyntaxError = 1, + kSemanticError = 2, + }; + + // Indicates the parse status of a password. + Status status; + string title; + string username; + string password; + string notes; + url.mojom.Url url; + url.mojom.Url otp_auth_url; + string? invalid_url; +}; + +[ServiceSandbox=sandbox.mojom.Sandbox.kService] +interface CSVSafariPasswordParser { + ParseCSV(string raw_csv) + => (CSVSafariPasswordSequence? sequence); +}; \ No newline at end of file diff --git a/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser_traits.cc b/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser_traits.cc new file mode 100644 index 000000000000..751286a7b2e7 --- /dev/null +++ b/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser_traits.cc @@ -0,0 +1,95 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser_traits.h" + +#include "url/mojom/url_gurl_mojom_traits.h" + +namespace mojo { + +password_manager::mojom::CSVSafariPassword_Status +EnumTraits:: + ToMojom(password_manager::CSVSafariPassword::Status status) { + switch (status) { + case password_manager::CSVSafariPassword::Status::kOK: + return password_manager::mojom::CSVSafariPassword_Status::kOK; + case password_manager::CSVSafariPassword::Status::kSyntaxError: + return password_manager::mojom::CSVSafariPassword_Status::kSyntaxError; + case password_manager::CSVSafariPassword::Status::kSemanticError: + return password_manager::mojom::CSVSafariPassword_Status::kSemanticError; + } + NOTREACHED(); +} + +bool EnumTraits:: + FromMojom(password_manager::mojom::CSVSafariPassword_Status status, + password_manager::CSVSafariPassword::Status* out) { + switch (status) { + case password_manager::mojom::CSVSafariPassword_Status::kOK: + *out = password_manager::CSVSafariPassword::Status::kOK; + return true; + case password_manager::mojom::CSVSafariPassword_Status::kSyntaxError: + *out = password_manager::CSVSafariPassword::Status::kSyntaxError; + return true; + case password_manager::mojom::CSVSafariPassword_Status::kSemanticError: + *out = password_manager::CSVSafariPassword::Status::kSemanticError; + return true; + } + return false; +} + +// static +bool StructTraits:: + Read(password_manager::mojom::CSVSafariPasswordDataView data, + password_manager::CSVSafariPassword* out) { + password_manager::CSVSafariPassword::Status status; + GURL url; + std::string title; + std::string username; + std::string password; + GURL otp_auth_url; + std::string notes; + + if (!data.ReadStatus(&status)) { + return false; + } + if (!data.ReadTitle(&title)) { + return false; + } + if (!data.ReadUrl(&url)) { + return false; + } + if (!data.ReadUsername(&username)) { + return false; + } + if (!data.ReadPassword(&password)) { + return false; + } + if (!data.ReadNotes(¬es)) { + return false; + } + if (!data.ReadOtpAuthUrl(&otp_auth_url)) { + return false; + } + if (url.is_valid()) { + *out = password_manager::CSVSafariPassword(title, url, username, password, + notes, otp_auth_url, status); + return true; + } + std::optional invalid_url; + if (!data.ReadInvalidUrl(&invalid_url)) { + return false; + } + DCHECK(invalid_url.has_value()); + *out = password_manager::CSVSafariPassword(title, invalid_url.value(), + username, password, notes, + otp_auth_url, status); + return true; +} + +} // namespace mojo diff --git a/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser_traits.h b/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser_traits.h new file mode 100644 index 000000000000..0d6f7b9abc9e --- /dev/null +++ b/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser_traits.h @@ -0,0 +1,70 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_COMPONENTS_PASSWORD_MANAGER_SERVICES_CSV_PASSWORD_PUBLIC_MOJOM_CSV_SAFARI_PASSWORD_PARSER_TRAITS_H_ +#define BRAVE_COMPONENTS_PASSWORD_MANAGER_SERVICES_CSV_PASSWORD_PUBLIC_MOJOM_CSV_SAFARI_PASSWORD_PARSER_TRAITS_H_ + +#include +#include + +#include "base/types/expected.h" +#include "base/types/expected_macros.h" +#include "brave/components/password_manager/core/browser/import/csv_safari_password.h" +#include "brave/components/password_manager/services/csv_password/public/mojom/csv_safari_password_parser.mojom.h" +#include "mojo/public/cpp/bindings/struct_traits.h" + +namespace mojo { + +template <> +struct EnumTraits { + static password_manager::mojom::CSVSafariPassword_Status ToMojom( + password_manager::CSVSafariPassword::Status status); + static bool FromMojom( + password_manager::mojom::CSVSafariPassword_Status status, + password_manager::CSVSafariPassword::Status* output); +}; + +template <> +struct StructTraits { + static password_manager::CSVSafariPassword::Status status( + const password_manager::CSVSafariPassword& r) { + return r.GetParseStatus(); + } + static const GURL url(const password_manager::CSVSafariPassword& r) { + return r.GetURL().value_or(GURL()); + } + static const GURL otp_auth_url(const password_manager::CSVSafariPassword& r) { + return r.GetOTPAuthURL().value_or(GURL()); + } + static std::optional invalid_url( + const password_manager::CSVSafariPassword& r) { + RETURN_IF_ERROR(r.GetURL()); + return std::nullopt; + } + static const std::string& title( + const password_manager::CSVSafariPassword& r) { + return r.GetTitle(); + } + static const std::string& username( + const password_manager::CSVSafariPassword& r) { + return r.GetUsername(); + } + static const std::string& password( + const password_manager::CSVSafariPassword& r) { + return r.GetPassword(); + } + static const std::string& notes( + const password_manager::CSVSafariPassword& r) { + return r.GetNotes(); + } + static bool Read(password_manager::mojom::CSVSafariPasswordDataView data, + password_manager::CSVSafariPassword* out); +}; + +} // namespace mojo + +#endif // BRAVE_COMPONENTS_PASSWORD_MANAGER_SERVICES_CSV_PASSWORD_PUBLIC_MOJOM_CSV_SAFARI_PASSWORD_PARSER_TRAITS_H_ diff --git a/ios/BUILD.gn b/ios/BUILD.gn index 066f97ed6038..9f7ac9cebcfb 100644 --- a/ios/BUILD.gn +++ b/ios/BUILD.gn @@ -35,6 +35,7 @@ import("//brave/ios/browser/api/session_restore/headers.gni") import("//brave/ios/browser/api/skus/headers.gni") import("//brave/ios/browser/api/storekit_receipt/headers.gni") import("//brave/ios/browser/api/unicode/headers.gni") +import("//brave/ios/browser/api/unzip/headers.gni") import("//brave/ios/browser/api/url/headers.gni") import("//brave/ios/browser/api/url_sanitizer/headers.gni") import("//brave/ios/browser/api/web/ui/headers.gni") @@ -81,6 +82,8 @@ brave_core_public_headers = [ "//brave/ios/browser/api/bookmarks/brave_bookmarks_observer.h", "//brave/ios/browser/api/bookmarks/importer/brave_bookmarks_importer.h", "//brave/ios/browser/api/bookmarks/exporter/brave_bookmarks_exporter.h", + "//brave/ios/browser/api/history/importer/brave_history_importer.h", + "//brave/ios/browser/api/password/importer/brave_password_importer.h", "//brave/ios/browser/api/history/brave_history_api.h", "//brave/ios/browser/api/history/brave_history_observer.h", "//brave/ios/browser/api/net/certificate_utility.h", @@ -129,6 +132,7 @@ brave_core_public_headers += developer_options_code_public_headers brave_core_public_headers += browser_api_storekit_receipt_public_headers brave_core_public_headers += webcompat_reporter_public_headers brave_core_public_headers += browser_api_unicode_public_headers +brave_core_public_headers += browser_api_unzip_public_headers action("brave_core_umbrella_header") { script = "//build/config/ios/generate_umbrella_header.py" diff --git a/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarksViewController.swift b/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarksViewController.swift index 02c8773ff137..f850a5890a5e 100644 --- a/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarksViewController.swift +++ b/ios/brave-ios/Sources/Brave/Frontend/Browser/Toolbars/BottomToolbar/Menu/Bookmarks/BookmarksViewController.swift @@ -64,7 +64,7 @@ class BookmarksViewController: SiteTableViewController, ToolbarUrlActionsProtoco return self.currentFolder == nil } - private let importExportUtility = BraveCoreImportExportUtility() + private let importExportUtility = BookmarksImportExportUtility() private var documentInteractionController: UIDocumentInteractionController? private var searchBookmarksTimer: Timer? @@ -269,7 +269,7 @@ class BookmarksViewController: SiteTableViewController, ToolbarUrlActionsProtoco alert.popoverPresentationController?.barButtonItem = sender let importAction = UIAlertAction(title: Strings.bookmarksImportAction, style: .default) { [weak self] _ in - let vc = UIDocumentPickerViewController(forOpeningContentTypes: [.html]) + let vc = UIDocumentPickerViewController(forOpeningContentTypes: [.html, .zip]) vc.delegate = self self?.present(vc, animated: true) } @@ -967,9 +967,8 @@ extension BookmarksViewController { func importBookmarks(from url: URL) { isLoading = true - self.importExportUtility.importBookmarks(from: url) { [weak self] success in - guard let self = self else { return } - + Task { @MainActor in + let success = await self.importExportUtility.importBookmarks(from: url) self.isLoading = false let alert = UIAlertController( @@ -987,8 +986,8 @@ extension BookmarksViewController { func exportBookmarks(to url: URL) { isLoading = true - self.importExportUtility.exportBookmarks(to: url) { [weak self] success in - guard let self = self else { return } + Task { @MainActor in + let success = await self.importExportUtility.exportBookmarks(to: url) self.isLoading = false diff --git a/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/Bookmarks/BookmarksImportExportUtility.swift b/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/Bookmarks/BookmarksImportExportUtility.swift new file mode 100644 index 000000000000..2ac9058502be --- /dev/null +++ b/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/Bookmarks/BookmarksImportExportUtility.swift @@ -0,0 +1,238 @@ +// Copyright 2020 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import BraveCore +import BraveShared +import Data +import Foundation +import Shared +import os.log + +class BookmarksImportExportUtility { + + // Import an array of bookmarks into BraveCore + @MainActor + func importBookmarks( + from array: [BraveImportedBookmark] + ) async -> Bool { + precondition( + state == .none, + "Bookmarks Import - Error Importing while an Import/Export operation is in progress" + ) + + state = .importing + return await withCheckedContinuation { continuation in + self.importer.import(from: array, topLevelFolderName: Strings.Sync.importFolderName) { + state in + + self.state = .none + switch state { + case .started: + Logger.module.debug("Bookmarks Import - Import Started") + case .cancelled: + Logger.module.debug("Bookmarks Import - Import Cancelled") + continuation.resume(returning: false) + case .autoCompleted, .completed: + Logger.module.debug("Bookmarks Import - Import Completed") + continuation.resume(returning: true) + @unknown default: + fatalError() + } + } + } + } + + // Import bookmarks from a file into BraveCore + @MainActor + func importBookmarks(from path: URL) async -> Bool { + precondition( + state == .none, + "Bookmarks Import - Error Importing while an Import/Export operation is in progress" + ) + + let doImport = { (path: URL, nativePath: String) async -> Bool in + await withCheckedContinuation { continuation in + self.importer.import( + fromFile: nativePath, + topLevelFolderName: Strings.Sync.importFolderName, + automaticImport: true + ) { [weak self] state, bookmarks in + guard let self else { + continuation.resume(returning: false) + return + } + + self.state = .none + switch state { + case .started: + Logger.module.debug("Bookmarks Import - Import Started") + case .cancelled: + Logger.module.debug("Bookmarks Import - Import Cancelled") + continuation.resume(returning: false) + case .autoCompleted, .completed: + Logger.module.debug("Bookmarks Import - Import Completed") + continuation.resume(returning: true) + @unknown default: + fatalError() + } + } + } + } + + guard let nativePath = BookmarksImportExportUtility.nativeURLPathFromURL(path) else { + Logger.module.error("Bookmarks Import - Invalid FileSystem Path") + return false + } + + // While accessing document URL from UIDocumentPickerViewController to access the file + // startAccessingSecurityScopedResource should be called for that URL + // Reference: https://stackoverflow.com/a/73912499/2239348 + guard path.startAccessingSecurityScopedResource() else { + return false + } + + state = .importing + defer { state = .none } + + if path.pathExtension.lowercased() == "zip" { + guard + let importsPath = try? await uniqueFileName( + "SafariImports", + folder: AsyncFileManager.default.temporaryDirectory + ) + else { + return false + } + + guard let nativeImportsPath = BookmarksImportExportUtility.nativeURLPathFromURL(importsPath) + else { + return false + } + + do { + try await AsyncFileManager.default.createDirectory( + at: importsPath, + withIntermediateDirectories: true + ) + } catch { + return false + } + + defer { + Task { + try await AsyncFileManager.default.removeItem(at: importsPath) + } + } + + if await Unzip.unzip(nativePath, toDirectory: nativeImportsPath) { + let bookmarksFileURL = importsPath.appending(path: "Bookmarks").appendingPathExtension( + "html" + ) + guard + let nativeBookmarksPath = BookmarksImportExportUtility.nativeURLPathFromURL( + bookmarksFileURL + ) + else { + Logger.module.error("Bookmarks Import - Invalid FileSystem Path") + return false + } + + // Each call to startAccessingSecurityScopedResource must be balanced with a call to stopAccessingSecurityScopedResource + // (Note: this is not reference counted) + defer { path.stopAccessingSecurityScopedResource() } + return await doImport(bookmarksFileURL, nativeBookmarksPath) + } + + return false + } + + // Each call to startAccessingSecurityScopedResource must be balanced with a call to stopAccessingSecurityScopedResource + // (Note: this is not reference counted) + defer { path.stopAccessingSecurityScopedResource() } + return await doImport(path, nativePath) + } + + // Export bookmarks from BraveCore to a file + @MainActor + func exportBookmarks(to path: URL) async -> Bool { + precondition( + state == .none, + "Bookmarks Import - Error Exporting while an Import/Export operation is in progress" + ) + + guard let nativePath = BookmarksImportExportUtility.nativeURLPathFromURL(path) else { + Logger.module.error("Bookmarks Export - Invalid FileSystem Path") + return false + } + + self.state = .exporting + return await withCheckedContinuation { continuation in + self.exporter.export(toFile: nativePath) { [weak self] state in + guard let self = self else { + continuation.resume(returning: false) + return + } + + self.state = .none + switch state { + case .started: + Logger.module.debug("Bookmarks Export - Export Started") + case .errorCreatingFile, .errorWritingHeader, .errorWritingNodes: + Logger.module.debug("Bookmarks Export - Export Error") + continuation.resume(returning: false) + case .cancelled: + Logger.module.debug("Bookmarks Export - Export Cancelled") + continuation.resume(returning: false) + case .completed: + Logger.module.debug("Bookmarks Export - Export Completed") + continuation.resume(returning: true) + @unknown default: + fatalError() + } + } + } + } + + // MARK: - Private + private var state: State = .none + private let importer = BraveBookmarksImporter() + private let exporter = BraveBookmarksExporter() + + private enum State { + case importing + case exporting + case none + } +} + +// MARK: - Parsing +extension BookmarksImportExportUtility { + static func nativeURLPathFromURL(_ url: URL) -> String? { + return url.withUnsafeFileSystemRepresentation { bytes -> String? in + guard let bytes = bytes else { return nil } + return String(cString: bytes) + } + } + + func uniqueFileName(_ filename: String, folder: URL) async throws -> URL { + let basePath = folder.appending(path: filename) + let fileExtension = basePath.pathExtension + let filenameWithoutExtension = + !fileExtension.isEmpty ? String(filename.dropLast(fileExtension.count + 1)) : filename + + var proposedPath = basePath + var count = 0 + + while await AsyncFileManager.default.fileExists(atPath: proposedPath.path) { + count += 1 + + let proposedFilenameWithoutExtension = "\(filenameWithoutExtension) (\(count))" + proposedPath = folder.appending(path: proposedFilenameWithoutExtension) + .appending(path: fileExtension) + } + + return proposedPath + } +} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/BraveCoreImportExportUtility.swift b/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/BraveCoreImportExportUtility.swift deleted file mode 100644 index acc4a7c2256e..000000000000 --- a/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/BraveCoreImportExportUtility.swift +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright 2020 The Brave Authors. All rights reserved. -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -import BraveCore -import BraveShared -import Data -import Foundation -import Shared -import os.log - -class BraveCoreImportExportUtility { - - // Import an array of bookmarks into BraveCore - func importBookmarks( - from array: [BraveImportedBookmark], - _ completion: @escaping (_ success: Bool) -> Void - ) { - precondition( - state == .none, - "Bookmarks Import - Error Importing while an Import/Export operation is in progress" - ) - - state = .importing - self.queue.async { - self.importer.import(from: array, topLevelFolderName: Strings.Sync.importFolderName) { - state in - guard state != .started else { return } - - self.state = .none - DispatchQueue.main.async { - completion(true) - } - } - } - } - - // Import bookmarks from a file into BraveCore - func importBookmarks(from path: URL, _ completion: @escaping (_ success: Bool) -> Void) { - precondition( - state == .none, - "Bookmarks Import - Error Importing while an Import/Export operation is in progress" - ) - - guard let nativePath = nativeURLPathFromURL(path) else { - Logger.module.error("Bookmarks Import - Invalid FileSystem Path") - DispatchQueue.main.async { - completion(false) - } - return - } - - state = .importing - self.queue.async { - // While accessing document URL from UIDocumentPickerViewController to access the file - // startAccessingSecurityScopedResource should be called for that URL - // Reference: https://stackoverflow.com/a/73912499/2239348 - guard path.startAccessingSecurityScopedResource() else { - DispatchQueue.main.async { - completion(false) - } - return - } - - self.importer.import( - fromFile: nativePath, - topLevelFolderName: Strings.Sync.importFolderName, - automaticImport: true - ) { [weak self] state, bookmarks in - guard let self else { - // Each call to startAccessingSecurityScopedResource must be balanced with a call to stopAccessingSecurityScopedResource - // (Note: this is not reference counted) - path.stopAccessingSecurityScopedResource() - return - } - - guard state != .started else { return } - path.stopAccessingSecurityScopedResource() - - do { - try self.rethrow(state) - self.state = .none - Logger.module.info("Bookmarks Import - Completed Import Successfully") - DispatchQueue.main.async { - completion(true) - } - } catch { - self.state = .none - Logger.module.error("\(error.localizedDescription)") - DispatchQueue.main.async { - completion(false) - } - } - } - } - } - - // Import bookmarks from a file into an array - func importBookmarks( - from path: URL, - _ completion: @escaping (_ success: Bool, _ bookmarks: [BraveImportedBookmark]) -> Void - ) { - precondition( - state == .none, - "Bookmarks Import - Error Importing while an Import/Export operation is in progress" - ) - - guard let nativePath = nativeURLPathFromURL(path) else { - Logger.module.error("Bookmarks Import - Invalid FileSystem Path") - DispatchQueue.main.async { - completion(false, []) - } - return - } - - state = .importing - self.queue.async { - // To access a document URL from UIDocumentPickerViewController - // startAccessingSecurityScopedResource should be called - guard path.startAccessingSecurityScopedResource() else { - DispatchQueue.main.async { - completion(false, []) - } - return - } - - self.importer.import( - fromFile: nativePath, - topLevelFolderName: Strings.Sync.importFolderName, - automaticImport: false - ) { [weak self] state, bookmarks in - guard let self else { - path.stopAccessingSecurityScopedResource() - return - } - - guard state != .started else { return } - path.stopAccessingSecurityScopedResource() - - do { - try self.rethrow(state) - self.state = .none - Logger.module.info("Bookmarks Import - Completed Import Successfully") - DispatchQueue.main.async { - completion(true, bookmarks ?? []) - } - } catch { - self.state = .none - Logger.module.error("\(error.localizedDescription)") - DispatchQueue.main.async { - completion(false, []) - } - } - } - } - } - - // Export bookmarks from BraveCore to a file - func exportBookmarks(to path: URL, _ completion: @escaping (_ success: Bool) -> Void) { - precondition( - state == .none, - "Bookmarks Import - Error Exporting while an Import/Export operation is in progress" - ) - - guard let nativePath = nativeURLPathFromURL(path) else { - Logger.module.error("Bookmarks Export - Invalid FileSystem Path") - DispatchQueue.main.async { - completion(false) - } - return - } - - self.state = .exporting - self.queue.async { - self.exporter.export(toFile: nativePath) { [weak self] state in - guard let self = self, state != .started else { return } - - do { - try self.rethrow(state) - self.state = .none - Logger.module.info("Bookmarks Export - Completed Export Successfully") - DispatchQueue.main.async { - completion(true) - } - } catch { - self.state = .none - Logger.module.error("\(error.localizedDescription)") - DispatchQueue.main.async { - completion(false) - } - } - } - } - } - - // MARK: - Private - private var state: State = .none - private let importer = BraveBookmarksImporter() - private let exporter = BraveBookmarksExporter() - - // Serial queue because we don't want someone accidentally importing and exporting at the same time.. - private let queue = DispatchQueue(label: "brave.core.import.export.utility", qos: .userInitiated) - - private enum State { - case importing - case exporting - case none - } -} - -// MARK: - Parsing -extension BraveCoreImportExportUtility { - func nativeURLPathFromURL(_ url: URL) -> String? { - return url.withUnsafeFileSystemRepresentation { bytes -> String? in - guard let bytes = bytes else { return nil } - return String(cString: bytes) - } - } -} - -// MARK: - Errors - -private enum ParsingError: String, Error { - case errorCreatingFile = "Error Creating File" - case errorWritingHeader = "Error Writing Header" - case errorWritingNode = "Error Writing Node" - case errorUnknown = "Unknown Error" -} - -// MARK: - Private - -extension BraveCoreImportExportUtility { - private func rethrow(_ state: BraveBookmarksImporterState) throws { - switch state { - case .started, .completed, .autoCompleted: - return - case .cancelled: - throw ParsingError.errorUnknown - @unknown default: - throw ParsingError.errorUnknown - } - } - - private func rethrow(_ state: BraveBookmarksExporterState) throws { - switch state { - case .started, .completed: - return - case .errorCreatingFile: - throw ParsingError.errorCreatingFile - case .errorWritingHeader: - throw ParsingError.errorWritingHeader - case .errorWritingNodes: - throw ParsingError.errorWritingNode - case .cancelled: - throw ParsingError.errorUnknown - @unknown default: - throw ParsingError.errorUnknown - } - } -} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/History/HistoryImportExportUtility.swift b/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/History/HistoryImportExportUtility.swift new file mode 100644 index 000000000000..0a650143050e --- /dev/null +++ b/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/History/HistoryImportExportUtility.swift @@ -0,0 +1,156 @@ +// Copyright 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import BraveCore +import BraveShared +import Foundation +import os.log + +class HistoryImportExportUtility { + + @MainActor + func importHistory(from path: URL) async -> Bool { + precondition( + state == .none, + "History Import - Error Importing while an Import/Export operation is in progress" + ) + + let doImport = { (path: URL, nativePath: String) async -> Bool in + await withCheckedContinuation { continuation in + self.importer.import( + fromFile: nativePath, + automaticImport: true + ) { [weak self] state, historyItems in + guard let self else { + continuation.resume(returning: false) + return + } + + self.state = .none + switch state { + case .started: + Logger.module.debug("History Import - Import Started") + case .cancelled: + Logger.module.debug("History Import - Import Cancelled") + continuation.resume(returning: false) + case .autoCompleted, .completed: + Logger.module.debug("History Import - Import Completed") + continuation.resume(returning: true) + @unknown default: + fatalError() + } + } + } + } + + guard let nativePath = HistoryImportExportUtility.nativeURLPathFromURL(path) else { + Logger.module.error("History Import - Invalid FileSystem Path") + return false + } + + // While accessing document URL from UIDocumentPickerViewController to access the file + // startAccessingSecurityScopedResource should be called for that URL + // Reference: https://stackoverflow.com/a/73912499/2239348 + guard path.startAccessingSecurityScopedResource() else { + return false + } + + state = .importing + defer { state = .none } + + if path.pathExtension.lowercased() == "zip" { + guard + let importsPath = try? await uniqueFileName( + "SafariImports", + folder: AsyncFileManager.default.temporaryDirectory + ) + else { + return false + } + + guard let nativeImportsPath = HistoryImportExportUtility.nativeURLPathFromURL(importsPath) + else { + return false + } + + do { + try await AsyncFileManager.default.createDirectory( + at: importsPath, + withIntermediateDirectories: true + ) + } catch { + return false + } + + defer { + Task { + try await AsyncFileManager.default.removeItem(at: importsPath) + } + } + + if await Unzip.unzip(nativePath, toDirectory: nativeImportsPath) { + let historyFileURL = importsPath.appending(path: "History").appendingPathExtension("json") + guard + let nativeHistoryPath = HistoryImportExportUtility.nativeURLPathFromURL(historyFileURL) + else { + Logger.module.error("History Import - Invalid FileSystem Path") + return false + } + + // Each call to startAccessingSecurityScopedResource must be balanced with a call to stopAccessingSecurityScopedResource + // (Note: this is not reference counted) + defer { path.stopAccessingSecurityScopedResource() } + return await doImport(historyFileURL, nativeHistoryPath) + } + + return false + } + + // Each call to startAccessingSecurityScopedResource must be balanced with a call to stopAccessingSecurityScopedResource + // (Note: this is not reference counted) + defer { path.stopAccessingSecurityScopedResource() } + return await doImport(path, nativePath) + } + + // MARK: - Private + private var state: State = .none + private let importer = BraveHistoryImporter() + + private enum State { + case importing + case exporting + case none + } +} + +// MARK: - Parsing +extension HistoryImportExportUtility { + static func nativeURLPathFromURL(_ url: URL) -> String? { + return url.withUnsafeFileSystemRepresentation { bytes -> String? in + guard let bytes = bytes else { return nil } + return String(cString: bytes) + } + } + + func uniqueFileName(_ filename: String, folder: URL) async throws -> URL { + let basePath = folder.appending(path: filename) + let fileExtension = basePath.pathExtension + let filenameWithoutExtension = + !fileExtension.isEmpty ? String(filename.dropLast(fileExtension.count + 1)) : filename + + var proposedPath = basePath + var count = 0 + + while await AsyncFileManager.default.fileExists(atPath: proposedPath.path) { + count += 1 + + let proposedFilenameWithoutExtension = "\(filenameWithoutExtension) (\(count))" + proposedPath = folder.appending(path: proposedFilenameWithoutExtension) + .appending(path: fileExtension) + } + + return proposedPath + } +} diff --git a/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/Password/PasswordsImportExportUtility.swift b/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/Password/PasswordsImportExportUtility.swift new file mode 100644 index 000000000000..4fd4df341ecc --- /dev/null +++ b/ios/brave-ios/Sources/Brave/Frontend/Sync/BraveCore/Password/PasswordsImportExportUtility.swift @@ -0,0 +1,150 @@ +// Copyright 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import BraveCore +import BraveShared +import Foundation +import os.log + +class PasswordsImportExportUtility { + + @MainActor + func importPasswords(from path: URL) async -> Bool { + precondition( + state == .none, + "Passwords Import - Error Importing while an Import/Export operation is in progress" + ) + + let doImport = { (path: URL, nativePath: String) async -> Bool in + await withCheckedContinuation { continuation in + self.importer.importPasswords(nativePath) { [weak self] in + guard let self else { + // Each call to startAccessingSecurityScopedResource must be balanced with a call to stopAccessingSecurityScopedResource + // (Note: this is not reference counted) + continuation.resume(returning: false) + return + } + + self.state = .none + + Logger.module.debug("Passwords Import - Import Completed") + continuation.resume(returning: true) + } + } + } + + guard let nativePath = PasswordsImportExportUtility.nativeURLPathFromURL(path) else { + Logger.module.error("Passwords Import - Invalid FileSystem Path") + return false + } + + // While accessing document URL from UIDocumentPickerViewController to access the file + // startAccessingSecurityScopedResource should be called for that URL + // Reference: https://stackoverflow.com/a/73912499/2239348 + guard path.startAccessingSecurityScopedResource() else { + return false + } + + state = .importing + defer { state = .none } + + if path.pathExtension.lowercased() == "zip" { + guard + let importsPath = try? await uniqueFileName( + "SafariImports", + folder: AsyncFileManager.default.temporaryDirectory + ) + else { + return false + } + + guard let nativeImportsPath = PasswordsImportExportUtility.nativeURLPathFromURL(importsPath) + else { + return false + } + + do { + try await AsyncFileManager.default.createDirectory( + at: importsPath, + withIntermediateDirectories: true + ) + } catch { + return false + } + + defer { + Task { + try await AsyncFileManager.default.removeItem(at: importsPath) + } + } + + if await Unzip.unzip(nativePath, toDirectory: nativeImportsPath) { + let passwordsFileURL = importsPath.appending(path: "Passwords").appendingPathExtension( + "csv" + ) + guard + let nativePasswordsPath = PasswordsImportExportUtility.nativeURLPathFromURL( + passwordsFileURL + ) + else { + Logger.module.error("Passwords Import - Invalid FileSystem Path") + return false + } + + // Each call to startAccessingSecurityScopedResource must be balanced with a call to stopAccessingSecurityScopedResource + // (Note: this is not reference counted) + defer { path.stopAccessingSecurityScopedResource() } + return await doImport(passwordsFileURL, nativePasswordsPath) + } + + return false + } + + // Each call to startAccessingSecurityScopedResource must be balanced with a call to stopAccessingSecurityScopedResource + // (Note: this is not reference counted) + defer { path.stopAccessingSecurityScopedResource() } + return await doImport(path, nativePath) + } + + // MARK: - Private + private var state: State = .none + private let importer = BravePasswordImporter() + + private enum State { + case importing + case exporting + case none + } +} + +// MARK: - Parsing +extension PasswordsImportExportUtility { + static func nativeURLPathFromURL(_ url: URL) -> String? { + return url.withUnsafeFileSystemRepresentation { bytes -> String? in + guard let bytes = bytes else { return nil } + return String(cString: bytes) + } + } + + func uniqueFileName(_ filename: String, folder: URL) async throws -> URL { + let basePath = folder.appending(path: filename) + let fileExtension = basePath.pathExtension + let filenameWithoutExtension = + !fileExtension.isEmpty ? String(filename.dropLast(fileExtension.count + 1)) : filename + + var proposedPath = basePath + var count = 0 + + while await AsyncFileManager.default.fileExists(atPath: proposedPath.path) { + count += 1 + + let proposedFilenameWithoutExtension = "\(filenameWithoutExtension) (\(count))" + proposedPath = folder.appending(path: proposedFilenameWithoutExtension) + .appending(path: fileExtension) + } + + return proposedPath + } +} diff --git a/ios/brave-ios/Sources/BraveShared/AsyncFileManager.swift b/ios/brave-ios/Sources/BraveShared/AsyncFileManager.swift index c7dc0f965e8c..72bd695a2f60 100644 --- a/ios/brave-ios/Sources/BraveShared/AsyncFileManager.swift +++ b/ios/brave-ios/Sources/BraveShared/AsyncFileManager.swift @@ -332,4 +332,9 @@ extension AsyncFileManager { public func downloadsPath() async throws -> URL { try await url(for: .documentDirectory, appending: "Downloads", create: true) } + + /// URL where temporary files are stored. + public var temporaryDirectory: URL { + fileManager.temporaryDirectory + } } diff --git a/ios/browser/BUILD.gn b/ios/browser/BUILD.gn index 09a14bbe525d..b8907a0eb347 100644 --- a/ios/browser/BUILD.gn +++ b/ios/browser/BUILD.gn @@ -29,6 +29,7 @@ source_set("browser") { "api/favicon", "api/features", "api/history", + "api/history/importer", "api/https_upgrades", "api/ipfs", "api/net", @@ -41,6 +42,7 @@ source_set("browser") { "api/storekit_receipt", "api/sync", "api/unicode", + "api/unzip", "api/url", "api/version_info", "api/web", diff --git a/ios/browser/api/history/importer/BUILD.gn b/ios/browser/api/history/importer/BUILD.gn new file mode 100644 index 000000000000..b08a296578fb --- /dev/null +++ b/ios/browser/api/history/importer/BUILD.gn @@ -0,0 +1,32 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//build/config/ios/rules.gni") +import("//ios/build/config.gni") + +source_set("importer") { + sources = [ + "brave_history_importer.h", + "brave_history_importer.mm", + "history_json_reader.cc", + "history_json_reader.h", + ] + + deps = [ + "//base", + "//components/history/core/browser", + "//components/keyed_service/core:core", + "//ios/chrome/browser/history/model", + "//ios/chrome/browser/shared/model/application_context", + "//ios/chrome/browser/shared/model/browser_state", + "//ios/chrome/browser/shared/model/profile", + "//ios/web/public/thread", + "//net", + "//ui/base:base", + "//url", + ] + + frameworks = [ "Foundation.framework" ] +} diff --git a/ios/browser/api/history/importer/brave_history_importer.h b/ios/browser/api/history/importer/brave_history_importer.h new file mode 100644 index 000000000000..2456fbdb633d --- /dev/null +++ b/ios/browser/api/history/importer/brave_history_importer.h @@ -0,0 +1,46 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_IOS_BROWSER_API_HISTORY_IMPORTER_BRAVE_HISTORY_IMPORTER_H_ +#define BRAVE_IOS_BROWSER_API_HISTORY_IMPORTER_BRAVE_HISTORY_IMPORTER_H_ + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, BraveHistoryImporterState) { + BraveHistoryImporterStateCompleted, + BraveHistoryImporterStateAutoCompleted, + BraveHistoryImporterStateStarted, + BraveHistoryImporterStateCancelled +}; + +OBJC_EXPORT +@interface BraveImportedHistory : NSObject +@property(nonatomic, readonly) NSURL* url; +@property(nonatomic, readonly) NSString* title; +@property(nonatomic, readonly) NSInteger visitCount; +@property(nonatomic, readonly) NSDate* lastVisitDate; +@end + +OBJC_EXPORT +@interface BraveHistoryImporter : NSObject +- (instancetype)init; + +- (void)cancel; + +- (void)importFromFile:(NSString*)filePath + automaticImport:(bool)automaticImport + withListener: + (void (^)(BraveHistoryImporterState, + NSArray* _Nullable))listener; + +- (void)importFromArray:(NSArray*)historyItems + withListener:(void (^)(BraveHistoryImporterState))listener; +@end + +NS_ASSUME_NONNULL_END + +#endif // BRAVE_IOS_BROWSER_API_HISTORY_IMPORTER_BRAVE_HISTORY_IMPORTER_H_ diff --git a/ios/browser/api/history/importer/brave_history_importer.mm b/ios/browser/api/history/importer/brave_history_importer.mm new file mode 100644 index 000000000000..efc1123ce8ce --- /dev/null +++ b/ios/browser/api/history/importer/brave_history_importer.mm @@ -0,0 +1,234 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/ios/browser/api/history/importer/brave_history_importer.h" + +#include + +#include "base/apple/foundation_util.h" +#include "base/base_paths.h" +#include "base/compiler_specific.h" +#include "base/files/file_path.h" +#include "base/functional/bind.h" +#include "base/functional/callback_helpers.h" +#include "base/path_service.h" +#include "base/stl_util.h" +#include "base/strings/sys_string_conversions.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/thread_pool.h" +#include "brave/ios/browser/api/history/importer/history_json_reader.h" +#include "components/history/core/browser/history_service.h" +#include "components/history/core/browser/history_types.h" +#include "components/keyed_service/core/service_access_type.h" +#include "ios/chrome/browser/history/model/history_service_factory.h" +#include "ios/chrome/browser/shared/model/application_context/application_context.h" +#include "ios/chrome/browser/shared/model/profile/profile_ios.h" +#include "ios/chrome/browser/shared/model/profile/profile_manager_ios.h" +#include "ios/web/public/thread/web_task_traits.h" +#include "ios/web/public/thread/web_thread.h" +#include "net/base/apple/url_conversions.h" +#include "url/gurl.h" + +namespace history { +void AddHistoryItems(const std::vector& history_items) { + std::vector profiles = + GetApplicationContext()->GetProfileManager()->GetLoadedProfiles(); + ProfileIOS* last_used_profile = profiles.at(0); + ios::HistoryServiceFactory::GetForProfile(last_used_profile, + ServiceAccessType::EXPLICIT_ACCESS) + ->AddPagesWithDetails(history_items, VisitSource::SOURCE_SAFARI_IMPORTED); +} +} // namespace history + +@implementation BraveImportedHistory +- (instancetype)initFromChromiumImportedHistory:(const history::URLRow&)entry { + if ((self = [super init])) { + _url = net::NSURLWithGURL(entry.url()); + _title = base::SysUTF16ToNSString(entry.title()); + _visitCount = entry.visit_count(); + _lastVisitDate = + [NSDate dateWithTimeIntervalSince1970:entry.last_visit() + .InSecondsFSinceUnixEpoch()]; + } + return self; +} + +- (history::URLRow)toChromiumImportedHistory { + history::URLRow entry = history::URLRow(net::GURLWithNSURL(self.url)); + entry.set_title(base::SysNSStringToUTF16(self.title)); + entry.set_visit_count(self.visitCount); + entry.set_last_visit(base::Time::FromSecondsSinceUnixEpoch( + [self.lastVisitDate timeIntervalSince1970])); + return entry; +} +@end + +@interface BraveHistoryImporter () { + scoped_refptr import_thread_; +} +@property(atomic) bool cancelled; // atomic +@end + +@implementation BraveHistoryImporter +- (instancetype)init { + if ((self = [super init])) { + self.cancelled = false; + + import_thread_ = base::ThreadPool::CreateSequencedTaskRunner( + {base::MayBlock(), base::TaskPriority::USER_VISIBLE, + base::TaskShutdownBehavior::BLOCK_SHUTDOWN}); + } + return self; +} + +- (void)dealloc { + [self cancel]; +} + +- (void)cancel { + self.cancelled = true; +} + +- (void)importFromFile:(NSString*)filePath + automaticImport:(bool)automaticImport + withListener: + (void (^)(BraveHistoryImporterState, + NSArray* _Nullable))listener { + base::FilePath source_file_path = base::apple::NSStringToFilePath(filePath); + + auto start_import = [](BraveHistoryImporter* weak_importer, + const base::FilePath& source_file_path, + bool automaticImport, + base::RepeatingCallback*)> listener) { + // Import cancelled as the importer has been deallocated + __strong BraveHistoryImporter* importer = weak_importer; + if (!importer) { + listener.Run(BraveHistoryImporterStateStarted, nullptr); + listener.Run(BraveHistoryImporterStateCancelled, nullptr); + return; + } + + listener.Run(BraveHistoryImporterStateStarted, nullptr); + std::vector history_items; + history_json_reader::ImportHistoryFile( + base::BindRepeating( + [](BraveHistoryImporter* importer) -> bool { + return [importer isImporterCancelled]; + }, + base::Unretained(importer)), + base::BindRepeating([](BraveHistoryImporter* importer, const GURL& url) + -> bool { return [importer canImportURL:url]; }, + base::Unretained(importer)), + source_file_path, &history_items); + + if (!history_items.empty() && ![importer isImporterCancelled]) { + if (automaticImport) { + auto complete_import = + [](std::vector history_items, + base::RepeatingCallback*)> + listener) { + history::AddHistoryItems(history_items); + listener.Run(BraveHistoryImporterStateAutoCompleted, nullptr); + }; + + // Import into the Profile/ProfileIOS on the main-thread. + web::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(complete_import, std::move(history_items), + base::BindRepeating(listener))); + } else { + listener.Run(BraveHistoryImporterStateCompleted, + [importer convertToIOSImportedHistory:history_items]); + } + } else { + listener.Run(BraveHistoryImporterStateCancelled, nullptr); + } + }; + + // Run the importer on the sequenced task runner. + __weak BraveHistoryImporter* weakSelf = self; + import_thread_->PostTask( + FROM_HERE, + base::BindOnce(start_import, weakSelf, source_file_path, automaticImport, + base::BindRepeating(listener))); +} + +- (void)importFromArray:(NSArray*)historyItems + withListener:(void (^)(BraveHistoryImporterState))listener { + auto start_import = + [](BraveHistoryImporter* weak_importer, + NSArray* history_items, + base::RepeatingCallback listener) { + // Import cancelled as the importer has been deallocated + __strong BraveHistoryImporter* importer = weak_importer; + if (!importer) { + listener.Run(BraveHistoryImporterStateStarted); + listener.Run(BraveHistoryImporterStateCancelled); + return; + } + + listener.Run(BraveHistoryImporterStateStarted); + history::AddHistoryItems( + [importer convertToChromiumImportedHistory:history_items]); + listener.Run(BraveHistoryImporterStateCompleted); + }; + + // Import into the Profile/ProfileIOS on the main-thread. + __weak BraveHistoryImporter* weakSelf = self; + import_thread_->PostTask(FROM_HERE, + base::BindOnce(start_import, weakSelf, historyItems, + base::BindRepeating(listener))); +} + +// MARK: - Private + +- (bool)isImporterCancelled { + return self.cancelled; +} + +// Returns true if |url| has a valid scheme that we allow to import. We +// filter out the URL with a unsupported scheme. +- (bool)canImportURL:(const GURL&)url { + // The URL is not valid. + if (!url.is_valid()) { + return false; + } + + // Filter out the URLs with unsupported schemes. + const char* const kInvalidSchemes[] = {"wyciwyg", "place", "about", "chrome"}; + for (const char* scheme : kInvalidSchemes) { + if (url.SchemeIs(scheme)) { + return false; + } + } + + return true; +} + +// Converts an array of Chromium imported history to iOS imported history. +- (NSArray*)convertToIOSImportedHistory: + (const std::vector&)historyItems { + NSMutableArray* results = + [[NSMutableArray alloc] init]; + for (const auto& historyItem : historyItems) { + BraveImportedHistory* imported_history_item = [[BraveImportedHistory alloc] + initFromChromiumImportedHistory:historyItem]; + [results addObject:imported_history_item]; + } + return results; +} + +// Converts an array of iOS imported history to Chromium imported history. +- (std::vector)convertToChromiumImportedHistory: + (NSArray*)historyItems { + std::vector results; + for (BraveImportedHistory* historyItem in historyItems) { + results.push_back([historyItem toChromiumImportedHistory]); + } + return results; +} +@end diff --git a/ios/browser/api/history/importer/history_json_reader.cc b/ios/browser/api/history/importer/history_json_reader.cc new file mode 100644 index 000000000000..6ccc5fe43367 --- /dev/null +++ b/ios/browser/api/history/importer/history_json_reader.cc @@ -0,0 +1,121 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/ios/browser/api/history/importer/history_json_reader.h" + +#include +#include + +#include "base/files/file_util.h" +#include "base/functional/callback.h" +#include "base/i18n/icu_string_conversions.h" +#include "base/json/json_reader.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "base/values.h" +#include "net/base/data_url.h" +#include "url/gurl.h" + +namespace history_json_reader { + +void ImportHistoryFile( + base::RepeatingCallback cancellation_callback, + base::RepeatingCallback valid_url_callback, + const base::FilePath& file_path, + std::vector* history_items) { + CHECK(history_items); + + std::string file_data; + CHECK(base::ReadFileToString(file_path, &file_data)); + + if (internal::ParseHistoryItems(file_data, history_items, + std::move(cancellation_callback), + std::move(valid_url_callback))) { + // Handle additional processing here if necessary. + } +} + +namespace internal { + +bool ParseHistoryItems( + const std::string& json_data, + std::vector* history_items, + base::RepeatingCallback cancellation_callback, + base::RepeatingCallback valid_url_callback) { + CHECK(!json_data.empty()); + CHECK(history_items); + + std::optional parsed_json = base::JSONReader::Read(json_data); + if (!parsed_json || !parsed_json->is_dict()) { + return false; // History file format is incorrect. Expected + // Structure/Dictionary (meta-data). + } + + const base::Value::Dict& meta_data = parsed_json->GetDict(); + auto* items = meta_data.FindList("history"); + if (!items) { + return false; // History file format is incorrect. Expected Array/List + // (history). + } + + for (const base::Value& item : *items) { + // Handle Import cancelled + if (!cancellation_callback.is_null() && cancellation_callback.Run()) { + return false; + } + + // History file format is incorrect. Expected Dictionary for each item. + if (!item.is_dict()) { + continue; + } + + history::URLRow url_row; + const base::Value::Dict& dict = item.GetDict(); + + // URL is non-optional + auto* url_string = dict.FindString("url"); + if (!url_string || url_string->empty()) { + return false; + } + + GURL url = GURL(*url_string); + if (valid_url_callback.is_null() || valid_url_callback.Run(url)) { + url_row.set_url(url); + } else { + continue; // Ignore this item + } + + // Title is optional + auto* title = dict.FindString("title"); + if (title && !title->empty()) { + url_row.set_title(base::UTF8ToUTF16(*title)); + } + + // time_usec_value is non-optional + auto time_usec = dict.FindDouble("time_usec"); + if (!time_usec.has_value()) { + continue; + } + + url_row.set_last_visit(base::Time::UnixEpoch() + + base::Microseconds(*time_usec)); + + // visit_count is non-optional + auto visit_count = dict.FindInt("visit_count"); + if (!visit_count.has_value()) { + continue; + } + + url_row.set_visit_count(*visit_count); + history_items->push_back(url_row); + } + + return true; +} + +} // namespace internal + +} // namespace history_json_reader diff --git a/ios/browser/api/history/importer/history_json_reader.h b/ios/browser/api/history/importer/history_json_reader.h new file mode 100644 index 000000000000..d906a948419f --- /dev/null +++ b/ios/browser/api/history/importer/history_json_reader.h @@ -0,0 +1,77 @@ +/* Copyright (c) 2024 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_IOS_BROWSER_API_HISTORY_IMPORTER_HISTORY_JSON_READER_H_ +#define BRAVE_IOS_BROWSER_API_HISTORY_IMPORTER_HISTORY_JSON_READER_H_ + +#include +#include + +#include "base/functional/callback_forward.h" +#include "components/history/core/browser/history_types.h" + +class GURL; + +namespace base { +class FilePath; +} // namespace base + +namespace history_json_reader { + +// Imports the history from the specified file. +// +// |cancellation_callback| is polled to query if the import should be cancelled; +// if it returns |true| at any time the import will be cancelled. If +// |cancellation_callback| is a null callback the import will run to completion. +// +// |valid_url_callback| is called to determine if a specified URL is valid for +// import; it returns |true| if it is. If |valid_url_callback| is a null +// callback, all URLs are considered to be valid. +// +// |file_path| is the path of the file on disk to import. +// +// |history_items| is a pointer to a vector, which is filled with the imported +// history. It may not be NULL. +void ImportHistoryFile( + base::RepeatingCallback cancellation_callback, + base::RepeatingCallback valid_url_callback, + const base::FilePath& file_path, + std::vector* history_items); + +namespace internal { + +// The file format that History parses +// [url] - A string that’s the URL of the history item. +// [title] - An optional string that, if present, is the title of the history +// item. [time_usec] - An integer that’s the UNIX timestamp in microseconds of +// the latest visit to the item. [destination_url] - An optional string that, if +// present, is the URL of the next item in the redirect chain. +// [destination_time_usec] - An optional integer that’s present if +// destination_url is also present and is the UNIX timestamp (the number of +// microseconds since midnight UTC, January 1, 1970) of the next navigation in +// the redirect chain. [source_url] - An optional string that, if present, is +// the URL of the previous item in the redirect chain. [source_time_usec] - An +// optional integer that’s present if source_url is also present and is the UNIX +// timestamp in microseconds of the previous navigation in the redirect chain. +// [visits_count] - An integer that’s the number of visits the browser made to +// this item, and is always greater than or equal to 1. +// [latest_visit_was_load_failure] - An optional Boolean that’s true if Safari +// failed to load the site when someone most recently tried to access it; +// otherwise, it’s false. [latest_visit_was_http_get] - An optional Boolean +// that’s true if the last visit to this item used the HTTP GET method; +// otherwise, it’s false. Reference: +// https://developer.apple.com/documentation/safariservices/importing-data-exported-from-safari?language=objc + +bool ParseHistoryItems( + const std::string& json_data, + std::vector* history_items, + base::RepeatingCallback cancellation_callback, + base::RepeatingCallback valid_url_callback); + +} // namespace internal + +} // namespace history_json_reader + +#endif // BRAVE_IOS_BROWSER_API_HISTORY_IMPORTER_HISTORY_JSON_READER_H_ diff --git a/ios/browser/api/password/BUILD.gn b/ios/browser/api/password/BUILD.gn index bb5edca82ccf..e334d812e1f6 100644 --- a/ios/browser/api/password/BUILD.gn +++ b/ios/browser/api/password/BUILD.gn @@ -18,6 +18,7 @@ source_set("password") { deps = [ "//base", + "//brave/ios/browser/api/password/importer", "//components/keyed_service/ios:ios", "//components/password_manager/core/browser/", "//ios/chrome/browser/passwords/model", diff --git a/ios/browser/api/password/importer/BUILD.gn b/ios/browser/api/password/importer/BUILD.gn new file mode 100644 index 000000000000..feef54db21d3 --- /dev/null +++ b/ios/browser/api/password/importer/BUILD.gn @@ -0,0 +1,34 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//build/config/ios/rules.gni") +import("//ios/build/config.gni") + +source_set("importer") { + sources = [ + "brave_password_importer.h", + "brave_password_importer.mm", + ] + + deps = [ + "//base", + "//brave/components/password_manager/core/browser/import:importer", + "//brave/components/password_manager/services/csv_password:lib", + "//components/password_manager/core/browser/password_store:password_store_interface", + "//components/password_manager/core/browser/ui", + "//ios/chrome/browser/affiliations/model:model", + "//ios/chrome/browser/passwords/model:store_factory", + "//ios/chrome/browser/shared/model/application_context", + "//ios/chrome/browser/shared/model/browser_state", + "//ios/chrome/browser/shared/model/profile", + "//ios/chrome/browser/webauthn/model", + "//ios/web/public/thread", + "//net", + "//ui/base:base", + "//url", + ] + + frameworks = [ "Foundation.framework" ] +} diff --git a/ios/browser/api/password/importer/brave_password_importer.h b/ios/browser/api/password/importer/brave_password_importer.h new file mode 100644 index 000000000000..c55ccf87f7e6 --- /dev/null +++ b/ios/browser/api/password/importer/brave_password_importer.h @@ -0,0 +1,16 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_IOS_BROWSER_API_PASSWORD_IMPORTER_BRAVE_PASSWORD_IMPORTER_H_ +#define BRAVE_IOS_BROWSER_API_PASSWORD_IMPORTER_BRAVE_PASSWORD_IMPORTER_H_ + +#include + +OBJC_EXPORT +@interface BravePasswordImporter : NSObject +- (void)importPasswords:(NSString*)fileName completion:(void (^)())completion; +@end + +#endif // BRAVE_IOS_BROWSER_API_PASSWORD_IMPORTER_BRAVE_PASSWORD_IMPORTER_H_ diff --git a/ios/browser/api/password/importer/brave_password_importer.mm b/ios/browser/api/password/importer/brave_password_importer.mm new file mode 100644 index 000000000000..be65a2deb3a9 --- /dev/null +++ b/ios/browser/api/password/importer/brave_password_importer.mm @@ -0,0 +1,74 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/ios/browser/api/password/importer/brave_password_importer.h" + +#include "base/apple/foundation_util.h" +#include "base/base_paths.h" +#include "base/files/file_path.h" +#include "base/functional/bind.h" +#include "base/functional/callback_helpers.h" +#include "base/strings/sys_string_conversions.h" +#include "brave/components/password_manager/core/browser/import/safari_password_importer.h" +#include "components/keyed_service/core/service_access_type.h" +#include "components/password_manager/core/browser/password_store/password_store_interface.h" +#include "components/password_manager/core/browser/ui/saved_passwords_presenter.h" +#include "ios/chrome/browser/affiliations/model/ios_chrome_affiliation_service_factory.h" +#include "ios/chrome/browser/passwords/model/ios_chrome_account_password_store_factory.h" +#include "ios/chrome/browser/passwords/model/ios_chrome_profile_password_store_factory.h" +#include "ios/chrome/browser/shared/model/application_context/application_context.h" +#include "ios/chrome/browser/shared/model/profile/profile_ios.h" +#include "ios/chrome/browser/shared/model/profile/profile_manager_ios.h" +#include "ios/chrome/browser/webauthn/model/ios_passkey_model_factory.h" + +namespace { +// Returns a passkey model instance if the feature is enabled. +webauthn::PasskeyModel* MaybeGetPasskeyModel(ProfileIOS* profile) { + return IOSPasskeyModelFactory::GetInstance()->GetForProfile(profile); +} + +} // namespace + +@interface BravePasswordImporter () { + std::unique_ptr _presenter; + std::unique_ptr _importer; +} +@end + +@implementation BravePasswordImporter + +- (void)importPasswords:(NSString*)filePath completion:(void (^)())completion { + std::vector profiles = + GetApplicationContext()->GetProfileManager()->GetLoadedProfiles(); + ProfileIOS* last_used_profile = profiles.at(0); + + _presenter = std::make_unique( + IOSChromeAffiliationServiceFactory::GetForProfile(last_used_profile), + IOSChromeProfilePasswordStoreFactory::GetForProfile( + last_used_profile, ServiceAccessType::EXPLICIT_ACCESS), + IOSChromeAccountPasswordStoreFactory::GetForProfile( + last_used_profile, ServiceAccessType::EXPLICIT_ACCESS), + MaybeGetPasskeyModel(last_used_profile)); + + base::FilePath path = base::apple::NSStringToFilePath(filePath); + _importer = std::make_unique( + _presenter.get()); + + auto password_store = password_manager::PasswordForm::Store::kProfileStore; + + _importer->Import( + path, password_store, + base::BindOnce( + [](void (^completion)(), + const password_manager::SafariImportResults& results) { + completion(); + }, + completion)); + + // _presenter.AddObserver(this); + _presenter->Init(); +} + +@end diff --git a/ios/browser/api/unzip/BUILD.gn b/ios/browser/api/unzip/BUILD.gn new file mode 100644 index 000000000000..035033b69d93 --- /dev/null +++ b/ios/browser/api/unzip/BUILD.gn @@ -0,0 +1,21 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +source_set("unzip") { + sources = [ + "unzip.h", + "unzip.mm", + ] + + deps = [ + "//base", + "//components/services/unzip:in_process", + "//components/services/unzip/public/cpp", + "//net", + "//url", + ] + + frameworks = [ "Foundation.framework" ] +} diff --git a/ios/browser/api/unzip/headers.gni b/ios/browser/api/unzip/headers.gni new file mode 100644 index 000000000000..00be04b1db29 --- /dev/null +++ b/ios/browser/api/unzip/headers.gni @@ -0,0 +1,6 @@ +# Copyright (c) 2024 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +browser_api_unzip_public_headers = [ "//brave/ios/browser/api/unzip/unzip.h" ] diff --git a/ios/browser/api/unzip/unzip.h b/ios/browser/api/unzip/unzip.h new file mode 100644 index 000000000000..8bb67e76c6c9 --- /dev/null +++ b/ios/browser/api/unzip/unzip.h @@ -0,0 +1,23 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include + +#ifndef BRAVE_IOS_BROWSER_API_UNZIP_UNZIP_H_ +#define BRAVE_IOS_BROWSER_API_UNZIP_UNZIP_H_ + +NS_ASSUME_NONNULL_BEGIN + +OBJC_EXPORT +NS_SWIFT_NAME(Unzip) +@interface BraveUnzip : NSObject ++ (void)unzip:(NSString*)zipFile + toDirectory:(NSString*)directory + completion:(void (^)(bool))completion; +@end + +NS_ASSUME_NONNULL_END + +#endif // BRAVE_IOS_BROWSER_API_UNZIP_UNZIP_H_ diff --git a/ios/browser/api/unzip/unzip.mm b/ios/browser/api/unzip/unzip.mm new file mode 100644 index 000000000000..667f944704ec --- /dev/null +++ b/ios/browser/api/unzip/unzip.mm @@ -0,0 +1,39 @@ +// Copyright (c) 2024 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/ios/browser/api/unzip/unzip.h" + +#include "base/apple/foundation_util.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/functional/callback_helpers.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/sys_string_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "base/task/sequenced_task_runner.h" +#include "base/task/thread_pool.h" +#include "components/services/unzip/in_process_unzipper.h" // nogncheck +#include "components/services/unzip/public/cpp/unzip.h" +#include "net/base/apple/url_conversions.h" + +@implementation BraveUnzip + ++ (void)unzip:(NSString*)zipFile + toDirectory:(NSString*)directory + completion:(void (^)(bool))completion { + base::FilePath file_to_unzip = base::apple::NSStringToFilePath(zipFile); + base::FilePath output_directory = base::apple::NSStringToFilePath(directory); + + unzip::Unzip(unzip::LaunchInProcessUnzipper(), file_to_unzip, + output_directory, unzip::mojom::UnzipOptions::New(), + unzip::AllContents(), base::DoNothing(), + base::BindOnce([](const base::FilePath& output_directory, + void (^completion)(bool), + bool success) { completion(success); }, + output_directory, completion)); +} + +@end