| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/extensions/api/bookmarks/bookmarks_api.h" |
| |
| #include <stddef.h> |
| |
| #include <limits> |
| #include <memory> |
| #include <optional> |
| #include <ranges> |
| #include <string> |
| #include <utility> |
| |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/lazy_instance.h" |
| #include "base/notreached.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/bookmarks/bookmark_model_factory.h" |
| #include "chrome/browser/bookmarks/managed_bookmark_service_factory.h" |
| #include "chrome/browser/extensions/bookmarks/bookmarks_error_constants.h" |
| #include "chrome/browser/extensions/bookmarks/bookmarks_features.h" |
| #include "chrome/browser/extensions/bookmarks/bookmarks_helpers.h" |
| #include "chrome/browser/platform_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/chrome_select_file_policy.h" |
| #include "chrome/common/extensions/api/bookmarks.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/bookmarks/browser/bookmark_model.h" |
| #include "components/bookmarks/browser/bookmark_node.h" |
| #include "components/bookmarks/browser/bookmark_utils.h" |
| #include "components/bookmarks/common/bookmark_metrics.h" |
| #include "components/bookmarks/managed/managed_bookmark_service.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/event_router.h" |
| #include "extensions/browser/extension_function_dispatcher.h" |
| #include "extensions/buildflags/buildflags.h" |
| |
| static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); |
| |
| using bookmarks::BookmarkModel; |
| using bookmarks::BookmarkNode; |
| using bookmarks::BookmarkPermanentNode; |
| using bookmarks::ManagedBookmarkService; |
| |
| namespace extensions { |
| |
| using api::bookmarks::BookmarkTreeNode; |
| using api::bookmarks::CreateDetails; |
| using content::BrowserContext; |
| using content::BrowserThread; |
| using content::WebContents; |
| |
| BookmarkEventRouter::BookmarkEventRouter(Profile* profile) |
| : browser_context_(profile), |
| model_(BookmarkModelFactory::GetForBrowserContext(profile)), |
| managed_(ManagedBookmarkServiceFactory::GetForProfile(profile)) { |
| model_->AddObserver(this); |
| } |
| |
| BookmarkEventRouter::~BookmarkEventRouter() { |
| if (model_) { |
| model_->RemoveObserver(this); |
| } |
| } |
| |
| void BookmarkEventRouter::DispatchEvent(events::HistogramValue histogram_value, |
| const std::string& event_name, |
| base::Value::List event_args) { |
| EventRouter* event_router = EventRouter::Get(browser_context_); |
| if (event_router) { |
| event_router->BroadcastEvent(std::make_unique<extensions::Event>( |
| histogram_value, event_name, std::move(event_args))); |
| } |
| } |
| |
| void BookmarkEventRouter::BookmarkModelLoaded(bool ids_reassigned) { |
| // TODO(erikkay): Perhaps we should send this event down to the extension |
| // so they know when it's safe to use the API? |
| } |
| |
| void BookmarkEventRouter::BookmarkModelBeingDeleted() { |
| // This codepath is unexpected because `this` is owned by a KeyedService that |
| // depends on BookmarkModelFactory, which means BookmarkModel must outlive |
| // `this`. |
| NOTREACHED(base::NotFatalUntil::M138); |
| model_ = nullptr; |
| } |
| |
| void BookmarkEventRouter::BookmarkNodeMoved(const BookmarkNode* old_parent, |
| size_t old_index, |
| const BookmarkNode* new_parent, |
| size_t new_index) { |
| // Only the root node currently has non-visible children, and nodes cannot be |
| // moved from/to it. Validate this assumption here, as otherwise this method |
| // would need to recalculate child indices. This is a debug-only check since |
| // it is not constant-time. |
| DCHECK(std::ranges::all_of(old_parent->children(), [](const auto& child) { |
| return child->IsVisible(); |
| })); |
| DCHECK(std::ranges::all_of(new_parent->children(), [](const auto& child) { |
| return child->IsVisible(); |
| })); |
| |
| const BookmarkNode* node = new_parent->children()[new_index].get(); |
| api::bookmarks::OnMoved::MoveInfo move_info; |
| move_info.parent_id = base::NumberToString(new_parent->id()); |
| move_info.index = static_cast<int>(new_index); |
| move_info.old_parent_id = base::NumberToString(old_parent->id()); |
| move_info.old_index = static_cast<int>(old_index); |
| |
| DispatchEvent(events::BOOKMARKS_ON_MOVED, api::bookmarks::OnMoved::kEventName, |
| api::bookmarks::OnMoved::Create( |
| base::NumberToString(node->id()), move_info)); |
| } |
| |
| void BookmarkEventRouter::BookmarkNodeAdded(const BookmarkNode* parent, |
| size_t index, |
| bool added_by_user) { |
| const BookmarkNode* node = parent->children()[index].get(); |
| if (base::FeatureList::IsEnabled(kEnforceBookmarkVisibilityOnExtensionsAPI) && |
| !node->IsVisible()) { |
| return; |
| } |
| |
| BookmarkTreeNode tree_node = bookmarks_helpers::GetBookmarkTreeNode( |
| model_, managed_, node, /*recurse=*/false, /*only_folders=*/false); |
| DispatchEvent(events::BOOKMARKS_ON_CREATED, |
| api::bookmarks::OnCreated::kEventName, |
| api::bookmarks::OnCreated::Create( |
| base::NumberToString(node->id()), tree_node)); |
| } |
| |
| void BookmarkEventRouter::BookmarkNodeRemoved( |
| const BookmarkNode* parent, |
| size_t index, |
| const BookmarkNode* node, |
| const std::set<GURL>& removed_urls, |
| const base::Location& location) { |
| CHECK(parent); |
| if (base::FeatureList::IsEnabled(kEnforceBookmarkVisibilityOnExtensionsAPI) && |
| !node->IsVisible()) { |
| return; |
| } |
| |
| api::bookmarks::OnRemoved::RemoveInfo remove_info; |
| remove_info.parent_id = base::NumberToString(parent->id()); |
| |
| // Calculate the API index of this node, prior to it being removed. |
| remove_info.index = bookmarks_helpers::GetAPIIndexOf(*parent, index); |
| |
| bookmarks_helpers::PopulateBookmarkTreeNode( |
| model_, managed_, node, /*recurse=*/true, |
| /*only_folders=*/false, /*visible_index=*/std::nullopt, |
| &remove_info.node); |
| |
| // The syncing property is not accurately populated for non-permanent nodes, |
| // because the node has already been detached from its parent. |
| if (!node->is_permanent_node()) { |
| remove_info.node.syncing = !model_->IsLocalOnlyNode(*parent); |
| } |
| |
| DispatchEvent(events::BOOKMARKS_ON_REMOVED, |
| api::bookmarks::OnRemoved::kEventName, |
| api::bookmarks::OnRemoved::Create( |
| base::NumberToString(node->id()), remove_info)); |
| } |
| |
| void BookmarkEventRouter::BookmarkAllUserNodesRemoved( |
| const std::set<GURL>& removed_urls, |
| const base::Location& location) { |
| // TODO(crbug.com/40277078): This used to be used only on Android, but |
| // that's no longer the case. We need to implement a new event to handle |
| // this. |
| } |
| |
| void BookmarkEventRouter::BookmarkNodeChanged(const BookmarkNode* node) { |
| // TODO(erikkay) The only three things that BookmarkModel sends this |
| // notification for are title, url and favicon. Since we're currently |
| // ignoring favicon and since the notification doesn't say which one anyway, |
| // for now we only include title and url. The ideal thing would be to |
| // change BookmarkModel to indicate what changed. |
| api::bookmarks::OnChanged::ChangeInfo change_info; |
| change_info.title = base::UTF16ToUTF8(node->GetTitle()); |
| if (node->is_url()) { |
| change_info.url = node->url().spec(); |
| } |
| |
| DispatchEvent(events::BOOKMARKS_ON_CHANGED, |
| api::bookmarks::OnChanged::kEventName, |
| api::bookmarks::OnChanged::Create( |
| base::NumberToString(node->id()), change_info)); |
| } |
| |
| void BookmarkEventRouter::BookmarkNodeFaviconChanged(const BookmarkNode* node) { |
| // TODO(erikkay) anything we should do here? |
| } |
| |
| void BookmarkEventRouter::BookmarkNodeChildrenReordered( |
| const BookmarkNode* node) { |
| api::bookmarks::OnChildrenReordered::ReorderInfo reorder_info; |
| for (const auto& child : node->children()) { |
| reorder_info.child_ids.push_back(base::NumberToString(child->id())); |
| } |
| |
| DispatchEvent(events::BOOKMARKS_ON_CHILDREN_REORDERED, |
| api::bookmarks::OnChildrenReordered::kEventName, |
| api::bookmarks::OnChildrenReordered::Create( |
| base::NumberToString(node->id()), reorder_info)); |
| } |
| |
| void BookmarkEventRouter::BookmarkPermanentNodeVisibilityChanged( |
| const BookmarkPermanentNode* node) { |
| if (!base::FeatureList::IsEnabled( |
| kEnforceBookmarkVisibilityOnExtensionsAPI)) { |
| return; |
| } |
| |
| // Currently visibility can only change for permanent nodes. The |
| // implementation of this method assumes this for now, as a simplification. |
| CHECK(node->is_permanent_node()); |
| |
| if (node->IsVisible()) { |
| BookmarkTreeNode tree_node = bookmarks_helpers::GetBookmarkTreeNode( |
| model_, managed_, node, /*recurse=*/false, /*only_folders=*/false); |
| |
| // Convert the index to be the index as seen by extensions. |
| tree_node.index = bookmarks_helpers::GetAPIIndexOf(*node); |
| |
| DispatchEvent(events::BOOKMARKS_ON_CREATED, |
| api::bookmarks::OnCreated::kEventName, |
| api::bookmarks::OnCreated::Create( |
| base::NumberToString(node->id()), tree_node)); |
| } else { |
| // There are currently no scenarios where a node that has children is |
| // hidden. |
| CHECK(node->children().empty()); |
| |
| // Notify the API user that the node was removed. |
| api::bookmarks::OnRemoved::RemoveInfo remove_info; |
| remove_info.parent_id = base::NumberToString(node->parent()->id()); |
| remove_info.index = bookmarks_helpers::GetAPIIndexOf(*node); |
| |
| bookmarks_helpers::PopulateBookmarkTreeNode( |
| model_, managed_, node, /*recurse=*/true, |
| /*only_folders=*/false, /*visible_index=*/std::nullopt, |
| &remove_info.node); |
| |
| // For consistency with OnRemoved events triggered by an individual node |
| // removal, do not populate the index, or parent_id fields. |
| remove_info.node.index = std::nullopt; |
| remove_info.node.parent_id = std::nullopt; |
| |
| DispatchEvent(events::BOOKMARKS_ON_REMOVED, |
| api::bookmarks::OnRemoved::kEventName, |
| api::bookmarks::OnRemoved::Create( |
| base::NumberToString(node->id()), remove_info)); |
| } |
| } |
| |
| void BookmarkEventRouter::ExtensiveBookmarkChangesBeginning() { |
| DispatchEvent(events::BOOKMARKS_ON_IMPORT_BEGAN, |
| api::bookmarks::OnImportBegan::kEventName, |
| api::bookmarks::OnImportBegan::Create()); |
| } |
| |
| void BookmarkEventRouter::ExtensiveBookmarkChangesEnded() { |
| DispatchEvent(events::BOOKMARKS_ON_IMPORT_ENDED, |
| api::bookmarks::OnImportEnded::kEventName, |
| api::bookmarks::OnImportEnded::Create()); |
| } |
| |
| BookmarksAPI::BookmarksAPI(BrowserContext* context) |
| : browser_context_(context) { |
| EventRouter* event_router = EventRouter::Get(browser_context_); |
| event_router->RegisterObserver(this, api::bookmarks::OnCreated::kEventName); |
| event_router->RegisterObserver(this, api::bookmarks::OnRemoved::kEventName); |
| event_router->RegisterObserver(this, api::bookmarks::OnChanged::kEventName); |
| event_router->RegisterObserver(this, api::bookmarks::OnMoved::kEventName); |
| event_router->RegisterObserver( |
| this, api::bookmarks::OnChildrenReordered::kEventName); |
| event_router->RegisterObserver(this, |
| api::bookmarks::OnImportBegan::kEventName); |
| event_router->RegisterObserver(this, |
| api::bookmarks::OnImportEnded::kEventName); |
| } |
| |
| BookmarksAPI::~BookmarksAPI() = default; |
| |
| void BookmarksAPI::Shutdown() { |
| EventRouter::Get(browser_context_)->UnregisterObserver(this); |
| } |
| |
| static base::LazyInstance<BrowserContextKeyedAPIFactory<BookmarksAPI>>:: |
| DestructorAtExit g_bookmarks_api_factory = LAZY_INSTANCE_INITIALIZER; |
| |
| // static |
| BrowserContextKeyedAPIFactory<BookmarksAPI>* |
| BookmarksAPI::GetFactoryInstance() { |
| return g_bookmarks_api_factory.Pointer(); |
| } |
| |
| void BookmarksAPI::OnListenerAdded(const EventListenerInfo& details) { |
| bookmark_event_router_ = std::make_unique<BookmarkEventRouter>( |
| Profile::FromBrowserContext(browser_context_)); |
| EventRouter::Get(browser_context_)->UnregisterObserver(this); |
| } |
| |
| ExtensionFunction::ResponseValue BookmarksGetFunction::RunOnReady() { |
| std::optional<api::bookmarks::Get::Params> params = |
| api::bookmarks::Get::Params::Create(args()); |
| if (!params) { |
| return BadMessage(); |
| } |
| |
| std::vector<BookmarkTreeNode> nodes; |
| ManagedBookmarkService* managed = GetManagedBookmarkService(); |
| if (params->id_or_id_list.as_strings) { |
| std::vector<std::string>& ids = *params->id_or_id_list.as_strings; |
| size_t count = ids.size(); |
| if (count <= 0) { |
| return BadMessage(); |
| } |
| for (size_t i = 0; i < count; ++i) { |
| std::string error; |
| const BookmarkNode* node = GetBookmarkNodeFromId(ids[i], &error); |
| if (!node) { |
| return Error(error); |
| } |
| bookmarks_helpers::AddNode(GetBookmarkModel(), managed, node, &nodes, |
| false); |
| } |
| } else { |
| std::string error; |
| const BookmarkNode* node = |
| GetBookmarkNodeFromId(*params->id_or_id_list.as_string, &error); |
| if (!node) { |
| return Error(error); |
| } |
| bookmarks_helpers::AddNode(GetBookmarkModel(), managed, node, &nodes, |
| false); |
| } |
| |
| return ArgumentList(api::bookmarks::Get::Results::Create(nodes)); |
| } |
| |
| ExtensionFunction::ResponseValue BookmarksGetChildrenFunction::RunOnReady() { |
| std::optional<api::bookmarks::GetChildren::Params> params = |
| api::bookmarks::GetChildren::Params::Create(args()); |
| if (!params) { |
| return BadMessage(); |
| } |
| |
| std::string error; |
| const BookmarkNode* node = GetBookmarkNodeFromId(params->id, &error); |
| if (!node) { |
| return Error(error); |
| } |
| |
| std::vector<BookmarkTreeNode> nodes; |
| for (const auto& child : node->children()) { |
| bookmarks_helpers::AddNode(GetBookmarkModel(), GetManagedBookmarkService(), |
| child.get(), &nodes, false); |
| } |
| |
| return ArgumentList(api::bookmarks::GetChildren::Results::Create(nodes)); |
| } |
| |
| ExtensionFunction::ResponseValue BookmarksGetRecentFunction::RunOnReady() { |
| std::optional<api::bookmarks::GetRecent::Params> params = |
| api::bookmarks::GetRecent::Params::Create(args()); |
| if (!params) { |
| return BadMessage(); |
| } |
| if (params->number_of_items < 1) { |
| // TODO(lazyboy): This shouldn't be necessary as schema specifies |
| // "minimum: 1". |
| return Error("numberOfItems cannot be less than 1."); |
| } |
| |
| std::vector<const BookmarkNode*> nodes; |
| bookmarks::GetMostRecentlyAddedEntries( |
| BookmarkModelFactory::GetForBrowserContext(GetProfile()), |
| params->number_of_items, &nodes); |
| |
| std::vector<BookmarkTreeNode> tree_nodes; |
| for (const BookmarkNode* node : nodes) { |
| bookmarks_helpers::AddNode(GetBookmarkModel(), GetManagedBookmarkService(), |
| node, &tree_nodes, false); |
| } |
| |
| return ArgumentList(api::bookmarks::GetRecent::Results::Create(tree_nodes)); |
| } |
| |
| ExtensionFunction::ResponseValue BookmarksGetTreeFunction::RunOnReady() { |
| std::vector<BookmarkTreeNode> nodes; |
| const BookmarkNode* node = |
| BookmarkModelFactory::GetForBrowserContext(GetProfile())->root_node(); |
| bookmarks_helpers::AddNode(GetBookmarkModel(), GetManagedBookmarkService(), |
| node, &nodes, true); |
| return ArgumentList(api::bookmarks::GetTree::Results::Create(nodes)); |
| } |
| |
| ExtensionFunction::ResponseValue BookmarksGetSubTreeFunction::RunOnReady() { |
| std::optional<api::bookmarks::GetSubTree::Params> params = |
| api::bookmarks::GetSubTree::Params::Create(args()); |
| if (!params) { |
| return BadMessage(); |
| } |
| |
| std::string error; |
| const BookmarkNode* node = GetBookmarkNodeFromId(params->id, &error); |
| if (!node) { |
| return Error(error); |
| } |
| |
| std::vector<BookmarkTreeNode> nodes; |
| bookmarks_helpers::AddNode(GetBookmarkModel(), GetManagedBookmarkService(), |
| node, &nodes, true); |
| return ArgumentList(api::bookmarks::GetSubTree::Results::Create(nodes)); |
| } |
| |
| ExtensionFunction::ResponseValue BookmarksSearchFunction::RunOnReady() { |
| std::optional<api::bookmarks::Search::Params> params = |
| api::bookmarks::Search::Params::Create(args()); |
| if (!params) { |
| return BadMessage(); |
| } |
| |
| std::vector<const BookmarkNode*> nodes; |
| if (params->query.as_string) { |
| bookmarks::QueryFields query; |
| query.word_phrase_query = std::make_unique<std::u16string>( |
| base::UTF8ToUTF16(*params->query.as_string)); |
| nodes = bookmarks::GetBookmarksMatchingProperties( |
| BookmarkModelFactory::GetForBrowserContext(GetProfile()), query, |
| std::numeric_limits<int>::max()); |
| } else { |
| DCHECK(params->query.as_object); |
| const api::bookmarks::Search::Params::Query::Object& object = |
| *params->query.as_object; |
| bookmarks::QueryFields query; |
| if (object.query) { |
| query.word_phrase_query = |
| std::make_unique<std::u16string>(base::UTF8ToUTF16(*object.query)); |
| } |
| if (object.url) { |
| query.url = |
| std::make_unique<std::u16string>(base::UTF8ToUTF16(*object.url)); |
| } |
| if (object.title) { |
| query.title = |
| std::make_unique<std::u16string>(base::UTF8ToUTF16(*object.title)); |
| } |
| nodes = bookmarks::GetBookmarksMatchingProperties( |
| BookmarkModelFactory::GetForBrowserContext(GetProfile()), query, |
| std::numeric_limits<int>::max()); |
| } |
| |
| std::vector<BookmarkTreeNode> tree_nodes; |
| ManagedBookmarkService* managed = GetManagedBookmarkService(); |
| for (const BookmarkNode* node : nodes) { |
| bookmarks_helpers::AddNode(GetBookmarkModel(), managed, node, &tree_nodes, |
| false); |
| } |
| |
| return ArgumentList(api::bookmarks::Search::Results::Create(tree_nodes)); |
| } |
| |
| ExtensionFunction::ResponseValue BookmarksRemoveFunctionBase::RunOnReady() { |
| if (!EditBookmarksEnabled()) { |
| return Error(bookmarks_errors::kEditBookmarksDisabled); |
| } |
| |
| std::optional<api::bookmarks::Remove::Params> params = |
| api::bookmarks::Remove::Params::Create(args()); |
| if (!params) { |
| return BadMessage(); |
| } |
| |
| int64_t id; |
| if (!base::StringToInt64(params->id, &id)) { |
| return Error(bookmarks_errors::kInvalidIdError); |
| } |
| |
| std::string error; |
| BookmarkModel* model = GetBookmarkModel(); |
| ManagedBookmarkService* managed = GetManagedBookmarkService(); |
| if (!bookmarks_helpers::RemoveNode(model, managed, id, is_recursive(), |
| &error)) { |
| return Error(error); |
| } |
| |
| return NoArguments(); |
| } |
| |
| bool BookmarksRemoveFunction::is_recursive() const { |
| return false; |
| } |
| |
| bool BookmarksRemoveTreeFunction::is_recursive() const { |
| return true; |
| } |
| |
| ExtensionFunction::ResponseValue BookmarksCreateFunction::RunOnReady() { |
| if (!EditBookmarksEnabled()) { |
| return Error(bookmarks_errors::kEditBookmarksDisabled); |
| } |
| |
| std::optional<api::bookmarks::Create::Params> params = |
| api::bookmarks::Create::Params::Create(args()); |
| if (!params) { |
| return BadMessage(); |
| } |
| |
| std::string error; |
| BookmarkModel* model = |
| BookmarkModelFactory::GetForBrowserContext(GetProfile()); |
| const BookmarkNode* node = |
| CreateBookmarkNode(model, params->bookmark, &error); |
| if (!node) { |
| return Error(error); |
| } |
| |
| BookmarkTreeNode ret = bookmarks_helpers::GetBookmarkTreeNode( |
| GetBookmarkModel(), GetManagedBookmarkService(), node, |
| /*recurse=*/false, |
| /*only_folders=*/false); |
| return ArgumentList(api::bookmarks::Create::Results::Create(ret)); |
| } |
| |
| const BookmarkNode* BookmarksCreateFunction::CreateBookmarkNode( |
| BookmarkModel* model, |
| const CreateDetails& details, |
| std::string* error) { |
| int64_t parent_id; |
| |
| if (!details.parent_id) { |
| // Optional, default to "other bookmarks" as a parent ID on desktop, "mobile |
| // bookmarks" on desktop Android. |
| // TODO(crbug.com/414844449): Currently, desktop Android still saves |
| // bookmarks to the mobile bookmarks folder and the bookmarks bar/other |
| // bookmarks folder are not visible if they are empty. This behavior is |
| // subject to change. |
| #if BUILDFLAG(IS_ANDROID) |
| parent_id = model->account_mobile_node() |
| ? model->account_mobile_node()->id() |
| : model->mobile_node()->id(); |
| #else |
| parent_id = model->account_other_node() ? model->account_other_node()->id() |
| : model->other_node()->id(); |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| } else if (!base::StringToInt64(*details.parent_id, &parent_id)) { |
| *error = bookmarks_errors::kInvalidIdError; |
| return nullptr; |
| } |
| const BookmarkNode* parent = bookmarks::GetBookmarkNodeByID(model, parent_id); |
| if (!CanBeModified(parent, error)) { |
| return nullptr; |
| } |
| if (!parent->is_folder()) { |
| *error = bookmarks_errors::kInvalidParentError; |
| return nullptr; |
| } |
| |
| // `parent` is not the root node (since the root node cannot be modified). |
| CHECK(!model->is_root_node(parent)); |
| |
| // Only the root node currently has non-visible children. Validate this |
| // assumption here, as otherwise this method would need to recalculate child |
| // indices. This is a debug-only check since it is not constant-time. |
| DCHECK(std::ranges::all_of(parent->children(), [](const auto& child) { |
| return child->IsVisible(); |
| })); |
| |
| size_t index; |
| if (!details.index) { // Optional (defaults to end). |
| index = parent->children().size(); |
| } else { |
| if (*details.index < 0 || |
| static_cast<size_t>(*details.index) > parent->children().size()) { |
| *error = bookmarks_errors::kInvalidIndexError; |
| return nullptr; |
| } |
| index = static_cast<size_t>(*details.index); |
| } |
| |
| std::u16string title; // Optional. |
| if (details.title) { |
| title = base::UTF8ToUTF16(*details.title); |
| } |
| |
| std::string url_string; // Optional. |
| if (details.url) { |
| url_string = *details.url; |
| } |
| |
| GURL url(url_string); |
| if (!url_string.empty() && !url.is_valid()) { |
| *error = bookmarks_errors::kInvalidUrlError; |
| return nullptr; |
| } |
| |
| const BookmarkNode* node; |
| if (url_string.length()) { |
| node = model->AddNewURL(parent, index, title, url); |
| } else { |
| node = model->AddFolder(parent, index, title); |
| model->SetDateFolderModified(parent, base::Time::Now()); |
| } |
| |
| DCHECK(node); |
| |
| return node; |
| } |
| |
| ExtensionFunction::ResponseValue BookmarksMoveFunction::RunOnReady() { |
| if (!EditBookmarksEnabled()) { |
| return Error(bookmarks_errors::kEditBookmarksDisabled); |
| } |
| |
| std::optional<api::bookmarks::Move::Params> params = |
| api::bookmarks::Move::Params::Create(args()); |
| if (!params) { |
| return BadMessage(); |
| } |
| |
| std::string error; |
| const BookmarkNode* node = GetBookmarkNodeFromId(params->id, &error); |
| if (!node) { |
| return Error(error); |
| } |
| |
| BookmarkModel* model = |
| BookmarkModelFactory::GetForBrowserContext(GetProfile()); |
| if (model->is_permanent_node(node)) { |
| return Error(bookmarks_errors::kModifySpecialError); |
| } |
| |
| const BookmarkNode* parent = nullptr; |
| if (!params->destination.parent_id) { |
| // Optional, defaults to current parent. |
| parent = node->parent(); |
| } else { |
| int64_t parent_id; |
| if (!base::StringToInt64(*params->destination.parent_id, &parent_id)) { |
| return Error(bookmarks_errors::kInvalidIdError); |
| } |
| |
| parent = bookmarks::GetBookmarkNodeByID(model, parent_id); |
| } |
| |
| if (!CanBeModified(parent, &error) || !CanBeModified(node, &error)) { |
| return Error(error); |
| } |
| |
| if (!parent->is_folder()) { |
| return Error(bookmarks_errors::kInvalidParentError); |
| } |
| |
| if (parent->HasAncestor(node)) { |
| return Error(bookmarks_errors::kInvalidMoveDestinationError); |
| } |
| |
| // `parent` is not the root node (since the root node cannot be modified). |
| CHECK(!model->is_root_node(parent)); |
| |
| // Only the root node currently has non-visible children. Validate this |
| // assumption here, as otherwise this method would need to recalculate child |
| // indices. This is a debug-only check since it is not constant-time. |
| DCHECK(std::ranges::all_of(parent->children(), [](const auto& child) { |
| return child->IsVisible(); |
| })); |
| |
| size_t index; |
| if (params->destination.index) { // Optional (defaults to end). |
| if (*params->destination.index < 0 || |
| static_cast<size_t>(*params->destination.index) > |
| parent->children().size()) { |
| return Error(bookmarks_errors::kInvalidIndexError); |
| } |
| index = static_cast<size_t>(*params->destination.index); |
| } else { |
| index = parent->children().size(); |
| } |
| |
| model->Move(node, parent, index); |
| |
| BookmarkTreeNode tree_node = bookmarks_helpers::GetBookmarkTreeNode( |
| GetBookmarkModel(), GetManagedBookmarkService(), node, |
| /*recurse=*/false, |
| /*only_folders=*/false); |
| return ArgumentList(api::bookmarks::Move::Results::Create(tree_node)); |
| } |
| |
| ExtensionFunction::ResponseValue BookmarksUpdateFunction::RunOnReady() { |
| if (!EditBookmarksEnabled()) { |
| return Error(bookmarks_errors::kEditBookmarksDisabled); |
| } |
| |
| std::optional<api::bookmarks::Update::Params> params = |
| api::bookmarks::Update::Params::Create(args()); |
| if (!params) { |
| return BadMessage(); |
| } |
| |
| // Optional but we need to distinguish non present from an empty title. |
| std::u16string title; |
| bool has_title = false; |
| if (params->changes.title) { |
| title = base::UTF8ToUTF16(*params->changes.title); |
| has_title = true; |
| } |
| |
| // Optional. |
| std::string url_string; |
| if (params->changes.url) { |
| url_string = *params->changes.url; |
| } |
| GURL url(url_string); |
| if (!url_string.empty() && !url.is_valid()) { |
| return Error(bookmarks_errors::kInvalidUrlError); |
| } |
| |
| std::string error; |
| const BookmarkNode* node = GetBookmarkNodeFromId(params->id, &error); |
| if (!CanBeModified(node, &error)) { |
| return Error(error); |
| } |
| |
| BookmarkModel* model = |
| BookmarkModelFactory::GetForBrowserContext(GetProfile()); |
| if (model->is_permanent_node(node)) { |
| return Error(bookmarks_errors::kModifySpecialError); |
| } |
| |
| if (!url.is_empty() && node->is_folder()) { |
| return Error(bookmarks_errors::kCannotSetUrlOfFolderError); |
| } |
| |
| if (has_title) { |
| model->SetTitle(node, title, |
| bookmarks::metrics::BookmarkEditSource::kExtension); |
| } |
| if (!url.is_empty()) { |
| model->SetURL(node, url, |
| bookmarks::metrics::BookmarkEditSource::kExtension); |
| } |
| |
| BookmarkTreeNode tree_node = bookmarks_helpers::GetBookmarkTreeNode( |
| GetBookmarkModel(), GetManagedBookmarkService(), node, |
| /*recurse=*/false, |
| /*only_folders=*/false); |
| return ArgumentList(api::bookmarks::Update::Results::Create(tree_node)); |
| } |
| |
| } // namespace extensions |