blob: 95a3648a905414be9b1baf8f05a9df4a220d3d7d [file] [log] [blame]
// Copyright 2021 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/web_applications/url_handler_prefs.h"
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/files/file_path.h"
#include "base/json/values_util.h"
#include "base/ranges/algorithm.h"
#include "base/ranges/functional.h"
#include "base/strings/strcat.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/url_formatter/elide_url.h"
#include "components/url_formatter/url_formatter.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "url/gurl.h"
#include "url/url_constants.h"
namespace web_app {
namespace url_handler_prefs {
namespace {
constexpr const char kAppId[] = "app_id";
constexpr const char kProfilePath[] = "profile_path";
constexpr const char kIncludePaths[] = "include_paths";
constexpr const char kExcludePaths[] = "exclude_paths";
constexpr const char kHasOriginWildcard[] = "has_origin_wildcard";
constexpr const char kDefaultPath[] = "/*";
constexpr const char kPath[] = "path";
constexpr const char kChoice[] = "choice";
constexpr const char kTimestamp[] = "timestamp";
// Returns true if |url| has the same origin as origin_str. If
// |look_for_subdomains| is true, url must have an origin that extends
// |origin_str| by at least one sub-domain.
bool UrlMatchesOrigin(const GURL& url,
const std::string& origin_str,
const bool look_for_subdomains) {
url::Origin origin = url::Origin::Create(GURL(origin_str));
url::Origin url_origin = url::Origin::Create(url);
if (origin.scheme() != url_origin.scheme() ||
origin.port() != url_origin.port())
return false;
const std::string& origin_host = origin.host();
const std::string& url_origin_host = url_origin.host();
if (look_for_subdomains) {
size_t pos = url_origin_host.find(origin_host);
if (pos == std::string::npos || pos == 0)
return false;
return url_origin_host.substr(pos) == origin_host;
} else {
return origin_host == url_origin_host;
}
}
// Returns true if |url_path| matches |path_pattern|. A prefix match is used if
// |path_pattern| ends with a '*' wildcard character. An exact match is used
// otherwise. |url_path| is a URL path from a fully specified URL.
// |path_pattern| is a URL path that can contain a wildcard postfix.
bool PathMatchesPathPattern(const std::string& url_path,
base::StringPiece path_pattern) {
if (!path_pattern.empty() && path_pattern.back() == '*') {
// Remove the wildcard and check if it's the same as the first several
// characters of |url_path|.
path_pattern = path_pattern.substr(0, path_pattern.length() - 1);
if (base::StartsWith(url_path, path_pattern))
return true;
} else {
// |path_pattern| doesn't contain a wildcard, check for an exact match.
if (path_pattern == url_path)
return true;
}
return false;
}
// Return true if |url_path| matches any path in |include_paths|. A path in
// |include_paths| can contain one wildcard '*' at the end.
// If any path matches, returns the best UrlHandlerSavedChoice found and
// its associated timestamp through the |choice| and |time| output parameters.
// "Best" here is defined by this ordering: kInApp > kNone > kInBrowser.
// |url_path| always starts with a '/', as it's the result of GURL::path().
bool FindBestMatchingIncludePathChoice(const std::string& url_path,
const base::Value& include_paths,
UrlHandlerSavedChoice* choice,
base::Time* time) {
if (!include_paths.is_list())
return false;
UrlHandlerSavedChoice best_choice = UrlHandlerSavedChoice::kInBrowser;
base::Time most_recent_timestamp;
bool found_match = false;
for (const auto& include_path_dict : include_paths.GetListDeprecated()) {
if (!include_path_dict.is_dict())
continue;
const std::string* include_path = include_path_dict.FindStringKey(kPath);
if (!include_path)
continue;
const absl::optional<int> choice_opt =
include_path_dict.FindIntKey(kChoice);
if (!choice_opt)
continue;
// Check enum. bounds before casting.
if (*choice_opt < 0 ||
*choice_opt > static_cast<int>(UrlHandlerSavedChoice::kMax))
continue;
auto current_choice = static_cast<UrlHandlerSavedChoice>(*choice_opt);
absl::optional<base::Time> current_timestamp =
base::ValueToTime(include_path_dict.FindKey(kTimestamp));
if (!current_timestamp)
continue;
if (PathMatchesPathPattern(url_path, *include_path)) {
// If current_choice is better than best_choice, update best choice and
// timestamp.
bool update_best = current_choice > best_choice ||
// If current_choice and best_choice are equal, choose
// the one with the latest timestamp.
(best_choice == current_choice &&
current_timestamp > most_recent_timestamp);
if (update_best) {
best_choice = current_choice;
most_recent_timestamp = *current_timestamp;
}
found_match = true;
}
}
if (found_match) {
*choice = best_choice;
*time = most_recent_timestamp;
}
return found_match;
}
// Return true if |url_path| matches any path in |exclude_paths|. A path in
// |exclude_paths| can contain one wildcard '*' at the end.
bool ExcludePathMatches(const std::string& url_path,
const base::Value& exclude_paths) {
if (!exclude_paths.is_list())
return false;
for (const auto& exclude_path : exclude_paths.GetListDeprecated()) {
if (!exclude_path.is_string())
continue;
if (PathMatchesPathPattern(url_path, exclude_path.GetString()))
return true;
}
return false;
}
// Given a list of handlers that matched an origin, apply the rules in each
// handler against |url| and return only handlers that match |url| by appending
// to |matches|.
void FilterAndAddMatches(const base::Value& all_handlers,
const GURL& url,
bool origin_trimmed,
std::vector<UrlHandlerLaunchParams>& matches) {
if (!all_handlers.is_list())
return;
for (const base::Value& handler : all_handlers.GetListDeprecated()) {
absl::optional<const HandlerView> handler_view =
GetConstHandlerView(handler);
if (!handler_view)
continue;
// |origin_trimmed| indicates if the input URL's origin had to be shortened
// to find a matching key. If true, filter out any matches that did not
// allow an origin prefix wildcard in their manifest.
if (origin_trimmed && !handler_view->has_origin_wildcard)
continue;
const std::string& url_path = url.path();
bool include_paths_exist =
!handler_view->include_paths.GetListDeprecated().empty();
UrlHandlerSavedChoice best_choice = UrlHandlerSavedChoice::kNone;
base::Time latest_timestamp = base::Time::Min();
if (include_paths_exist && !FindBestMatchingIncludePathChoice(
url_path, handler_view->include_paths,
&best_choice, &latest_timestamp)) {
continue;
}
bool exclude_paths_exist =
!handler_view->exclude_paths.GetListDeprecated().empty();
if (exclude_paths_exist &&
ExcludePathMatches(url_path, handler_view->exclude_paths)) {
continue;
}
matches.emplace_back(handler_view->profile_path, handler_view->app_id, url,
best_choice, latest_timestamp);
}
}
// Find the most recent match. If it is saved as kInBrowser, preferred choice
// is the browser so no matches should be returned; If saved as kNone, all the
// matches should be returned so the user can make a new saved choice; If
// kInApp, only returned the app match as it is the saved choice.
void FilterBySavedChoice(std::vector<UrlHandlerLaunchParams>& matches) {
if (matches.empty())
return;
// Record the most recent match. If two matches have the same timestamp,
// prefer the one with a higher saved_choice value.
auto most_recent_match_iterator = base::ranges::max_element(
matches, [](const UrlHandlerLaunchParams& match1,
const UrlHandlerLaunchParams& match2) {
if (match1.saved_choice_timestamp > match2.saved_choice_timestamp)
return false;
if (match1.saved_choice_timestamp < match2.saved_choice_timestamp)
return true;
return match1.saved_choice < match2.saved_choice;
});
switch (most_recent_match_iterator->saved_choice) {
case UrlHandlerSavedChoice::kInApp:
matches = {std::move(*most_recent_match_iterator)};
break;
case UrlHandlerSavedChoice::kInBrowser:
matches = {};
break;
case UrlHandlerSavedChoice::kNone:
// `matches` already contain all matches. Do not modify.
break;
}
}
void FindMatchesImpl(const base::Value& pref_value,
const GURL& url,
std::vector<UrlHandlerLaunchParams>& matches,
const std::string& origin_str,
const bool origin_trimmed) {
const base::Value* const all_handlers = pref_value.FindListKey(origin_str);
if (all_handlers) {
DCHECK(UrlMatchesOrigin(url, origin_str, origin_trimmed));
FilterAndAddMatches(*all_handlers, url, origin_trimmed, matches);
FilterBySavedChoice(matches);
}
}
// Helper function that runs |op| repeatedly with shorter versions of
// |origin_str|. This helps match URLs to entries keyed by broader origins.
template <typename Operation>
void TryDifferentOriginSubstrings(std::string origin_str, Operation op) {
bool origin_trimmed = false;
while (true) {
op(origin_str, origin_trimmed);
// Try to shorten origin_str to the next origin suffix by removing 1
// sub-domain. This enables matching against origins that contain wildcard
// prefixes. As these origins with wildcard prefixes could be of different
// lengths and yet match the initial origin_str, every suffix is processed.
auto found = origin_str.find('.');
if (found != std::string::npos) {
// Trim origin to after next '.' character if there is one.
origin_str = base::StrCat({"https://", origin_str.substr(found + 1)});
origin_trimmed = true;
// Do not early return here. There could be other apps that match using
// origin wildcard.
} else {
// There is no more '.'. Stop looking.
break;
}
}
}
// Returns the URL handlers stored in |pref_value| that match |url|'s origin.
std::vector<UrlHandlerLaunchParams> FindMatches(const base::Value& pref_value,
const GURL& url) {
std::vector<UrlHandlerLaunchParams> matches;
if (!pref_value.is_dict())
return matches;
url::Origin origin = url::Origin::Create(url);
if (origin.opaque())
return matches;
if (origin.scheme() != url::kHttpsScheme)
return matches;
// FindMatchesImpl accumulates results to |matches|.
std::string origin_str = origin.Serialize();
TryDifferentOriginSubstrings(
origin_str, [&pref_value, &url, &matches](const std::string& origin_str,
bool origin_trimmed) {
FindMatchesImpl(pref_value, url, matches, origin_str, origin_trimmed);
});
return matches;
}
base::Value GetIncludePathsValue(const std::vector<std::string>& include_paths,
const base::Time& time) {
base::Value value(base::Value::Type::LIST);
// When no "paths" are specified in web-app-origin-association, all include
// paths are allowed.
for (const auto& include_path : include_paths.empty()
? std::vector<std::string>({kDefaultPath})
: include_paths) {
base::Value path_dict(base::Value::Type::DICTIONARY);
path_dict.SetStringKey(kPath, include_path);
path_dict.SetIntKey(kChoice,
static_cast<int>(UrlHandlerSavedChoice::kNone));
path_dict.SetKey(kTimestamp, base::TimeToValue(time));
value.Append(std::move(path_dict));
}
return value;
}
base::Value GetExcludePathsValue(
const std::vector<std::string>& exclude_paths) {
base::Value value(base::Value::Type::LIST);
for (const auto& exclude_path : exclude_paths) {
value.Append(exclude_path);
}
return value;
}
base::Value NewHandler(const AppId& app_id,
const base::FilePath& profile_path,
const apps::UrlHandlerInfo& info,
const base::Time& time) {
base::Value value(base::Value::Type::DICTIONARY);
value.SetStringKey(kAppId, app_id);
value.SetKey(kProfilePath, base::FilePathToValue(profile_path));
value.SetBoolKey(kHasOriginWildcard, info.has_origin_wildcard);
// Set include_paths and exclude paths from associated app.
value.SetKey(kIncludePaths, GetIncludePathsValue(info.paths, time));
value.SetKey(kExcludePaths, GetExcludePathsValue(info.exclude_paths));
return value;
}
// If |match_app_id| is true, returns true if |handler| has dict. values equal
// to |app_id| and |profile_path|. If |match_app_id| is false, only compare
// |profile_path|.
bool IsHandlerForApp(const AppId& app_id,
const base::FilePath& profile_path,
bool match_app_id,
const base::Value& handler) {
auto handler_view = GetConstHandlerView(handler);
if (!handler_view)
return false;
if (handler_view->profile_path != profile_path)
return false;
return !match_app_id || handler_view->app_id == app_id;
}
// Removes entries that match |profile_path| and |app_id|.
// |profile_path| is always compared while |app_id| is only compared when it is
// not empty.
void RemoveEntries(base::Value& pref_value,
const AppId& app_id,
const base::FilePath& profile_path) {
if (!pref_value.is_dict())
return;
std::vector<std::string> origins_to_remove;
for (auto origin_value : pref_value.DictItems()) {
base::Value::List handlers = std::move(origin_value.second.GetList());
handlers.EraseIf([&app_id, &profile_path](const base::Value& handler) {
return IsHandlerForApp(app_id, profile_path,
/*match_app_id=*/!app_id.empty(), handler);
});
// Replace list if any entries remain.
if (!handlers.empty()) {
origin_value.second = base::Value(std::move(handlers));
} else {
origins_to_remove.push_back(origin_value.first);
}
}
for (const auto& origin_to_remove : origins_to_remove)
pref_value.RemoveKey(origin_to_remove);
}
using PathSet = base::flat_set<std::string>;
// Sets |choice| on every include path in |all_include_paths| where the path
// exists in |updated_include_paths|.
void UpdateSavedChoiceInIncludePaths(const PathSet& updated_include_paths,
UrlHandlerSavedChoice choice,
const base::Time& time,
base::Value& all_include_paths) {
// |all_include_paths| is a list of include path dicts. Eg:
// [ {
// "choice": 0,
// "path": "/abc",
// "timestamp": "-9223372036854775808"
// } ]
auto& include_paths_list = all_include_paths.GetList();
for (base::Value& include_path_value : include_paths_list) {
if (!include_path_value.is_dict())
continue;
base::Value::Dict& include_path_dict = include_path_value.GetDict();
const std::string* path = include_path_dict.FindString(kPath);
if (!path)
continue;
if (updated_include_paths.contains(*path)) {
include_path_dict.Set(kChoice, static_cast<int>(choice));
include_path_dict.Set(kTimestamp, base::TimeToValue(time));
}
}
}
// Sets |choice| on every path in |include_paths| that matches |url|. Returns
// a set of paths that are updated.
PathSet UpdateSavedChoice(const GURL& url,
UrlHandlerSavedChoice choice,
const base::Time& time,
base::Value& include_paths) {
// |include_paths| is a list of include path dicts. Eg:
// [ {
// "choice": 0,
// "path": "/abc",
// "timestamp": "-9223372036854775808"
// } ]
auto& include_paths_list = std::move(include_paths).GetList();
std::vector<std::string> updated_include_paths;
for (base::Value& include_path_value : include_paths_list) {
if (!include_path_value.is_dict())
continue;
base::Value::Dict& include_path_dict = include_path_value.GetDict();
const std::string* path = include_path_dict.FindString(kPath);
if (!path)
continue;
// Any matching path dict. will be updated with the input choice and
// timestamp.
if (PathMatchesPathPattern(url.path(), *path)) {
include_path_dict.Set(kChoice, static_cast<int>(choice));
include_path_dict.Set(kTimestamp, base::TimeToValue(time));
updated_include_paths.push_back(*path);
}
}
return std::move(updated_include_paths);
}
// Update the save choice on every include path that matches the |url|.
void SaveChoiceToAllMatchingIncludePaths(const GURL& url,
const UrlHandlerSavedChoice choice,
const base::Time& time,
base::Value::List& handlers) {
for (auto& handler : handlers) {
auto handler_view = GetHandlerView(handler);
if (!handler_view)
continue;
UpdateSavedChoice(url, choice, time, handler_view->include_paths);
}
}
bool AppIdAndProfileMatch(const AppId* app_id,
const base::FilePath* profile_path,
const std::string& handler_app_id,
const base::FilePath& handler_profile_path) {
return (*app_id == handler_app_id) && (*profile_path == handler_profile_path);
}
// Update the matching include paths' saved choice where app id and profile
// path match |app_id| and |profile_path|. Return which include paths are
// updated.
PathSet SaveInAppChoiceToSelectedApp(const AppId* app_id,
const base::FilePath* profile_path,
const GURL& url,
const base::Time& time,
base::Value::List& handlers) {
PathSet updated_include_paths;
for (auto& handler : handlers) {
auto handler_view = GetHandlerView(handler);
if (!handler_view ||
!AppIdAndProfileMatch(app_id, profile_path, handler_view->app_id,
handler_view->profile_path)) {
continue;
}
PathSet updated_paths = UpdateSavedChoice(
url, UrlHandlerSavedChoice::kInApp, time, handler_view->include_paths);
updated_include_paths.insert(updated_paths.begin(), updated_paths.end());
}
return updated_include_paths;
}
// Find include paths in |updated_include_paths| from apps that don't match
// |app_id| and |profile_path|. Reset the saved choice of these to kNone so
// they don't conflict with the app choice that was just saved.
void ResetSavedChoiceInOtherApps(const AppId* app_id,
const base::FilePath* profile_path,
const base::Time& time,
PathSet updated_include_paths,
base::Value::List& handlers) {
for (auto& handler : handlers) {
auto handler_view = GetHandlerView(handler);
if (!handler_view ||
AppIdAndProfileMatch(app_id, profile_path, handler_view->app_id,
handler_view->profile_path)) {
continue;
}
UpdateSavedChoiceInIncludePaths(updated_include_paths,
UrlHandlerSavedChoice::kNone, time,
handler_view->include_paths);
}
}
void SaveAppChoice(const AppId* app_id,
const base::FilePath* profile_path,
const GURL& url,
const base::Time& time,
base::Value::List& handlers) {
PathSet updated_include_paths =
SaveInAppChoiceToSelectedApp(app_id, profile_path, url, time, handlers);
if (updated_include_paths.empty())
return;
ResetSavedChoiceInOtherApps(app_id, profile_path, time,
std::move(updated_include_paths), handlers);
}
void SaveChoiceImpl(const AppId* app_id,
const base::FilePath* profile_path,
const GURL& url,
const UrlHandlerSavedChoice choice,
const base::Time& time,
base::Value& pref_value,
const std::string& origin_str,
const bool origin_trimmed) {
base::Value::List* handlers = pref_value.GetDict().FindList(origin_str);
if (!handlers)
return;
DCHECK(UrlMatchesOrigin(url, origin_str, origin_trimmed));
if (choice == UrlHandlerSavedChoice::kInApp) {
SaveAppChoice(app_id, profile_path, url, time, *handlers);
} else {
SaveChoiceToAllMatchingIncludePaths(url, choice, time, *handlers);
}
}
// Saves |choice| and |time| to all handler include_paths that match |app_id|,
// |profile_path|, and |url|. |url| provides both origin and path for matching.
void SaveChoice(PrefService* local_state,
const AppId* app_id,
const base::FilePath* profile_path,
const GURL& url,
const UrlHandlerSavedChoice choice,
const base::Time& time) {
DCHECK(url.is_valid());
DCHECK(local_state);
DCHECK(choice != UrlHandlerSavedChoice::kNone);
// |app_id| and |profile_path| are not needed when choice == kInBrowser.
DCHECK(choice != UrlHandlerSavedChoice::kInBrowser ||
(app_id == nullptr && profile_path == nullptr));
DictionaryPrefUpdate update(local_state, prefs::kWebAppsUrlHandlerInfo);
base::Value* const pref_value = update.Get();
if (!pref_value || !pref_value->is_dict())
return;
url::Origin origin = url::Origin::Create(url);
if (origin.opaque())
return;
if (origin.scheme() != url::kHttpsScheme)
return;
std::string origin_str = origin.Serialize();
// SaveChoiceImpl modifies prefs but produces no output.
TryDifferentOriginSubstrings(
origin_str, [app_id, profile_path, &url, choice, &time, pref_value](
const std::string& origin_str, bool origin_trimmed) {
SaveChoiceImpl(app_id, profile_path, url, choice, time, *pref_value,
origin_str, origin_trimmed);
});
}
bool ShouldUpdateIncludePaths(const base::Value& current_handler,
const base::Value& new_handler) {
const base::Value* include_paths_lh =
current_handler.FindListKey(kIncludePaths);
const base::Value* include_paths_rh = new_handler.FindListKey(kIncludePaths);
if (!include_paths_lh || !include_paths_rh)
return true;
base::Value::ConstListView include_paths_list_lh =
include_paths_lh->GetListDeprecated();
base::Value::ConstListView include_paths_list_rh =
include_paths_rh->GetListDeprecated();
if (include_paths_list_lh.size() != include_paths_list_rh.size())
return true;
for (size_t i = 0; i < include_paths_list_lh.size(); i++) {
DCHECK(include_paths_list_lh[i].is_dict());
DCHECK(include_paths_list_rh[i].is_dict());
const std::string* path_lh = include_paths_list_lh[i].FindStringKey(kPath);
const std::string* path_rh = include_paths_list_rh[i].FindStringKey(kPath);
if (!path_lh || !path_rh)
return true;
if (*path_lh != *path_rh)
return true;
}
return false;
}
// Update 'include_paths' in 'current_handler' from 'include_paths' in
// 'new_handler'. Update does not happen if 'include_paths' in both are
// identical. 'choice' and 'timestamp' are not compared to determine
// equivalence. Both handler values follow the format:
// {
// "app_id": "qruhrugqrgjdsdfhjghjrghjhdfgaaamenww",
// "profile_path": "C:\\Users\\alias\\Profile\\Default",
// "has_origin_wildcard": true,
// "include_paths": [
// {
// "path": "/*",
// "choice": 2, // kInApp
// // "2000-01-01 00:00:00.000 UTC"
// "timestamp": "12591158400000000"
// }
// ],
// "exclude_paths": ["/abc"],
// }
void MaybeUpdateIncludePaths(base::Value& current_handler,
base::Value& new_handler) {
if (ShouldUpdateIncludePaths(current_handler, new_handler)) {
base::Value* new_include_paths = new_handler.FindListKey(kIncludePaths);
if (new_include_paths) {
current_handler.SetKey(kIncludePaths, std::move(*new_include_paths));
} else {
current_handler.SetKey(kIncludePaths,
base::Value(base::Value::Type::LIST));
}
}
}
// Updates 'exclude_paths' in 'current_handler' from 'exclude_paths' in
// 'new_handler'. 'exclude_paths' can be replaced directly because it stores no
// user preferences.
void UpdateExcludePaths(base::Value& current_handler,
base::Value& new_handler) {
base::Value* new_exclude_paths = new_handler.FindListKey(kExcludePaths);
if (new_exclude_paths) {
current_handler.SetKey(kExcludePaths, std::move(*new_exclude_paths));
} else {
current_handler.SetKey(kExcludePaths, base::Value(base::Value::Type::LIST));
}
}
// Returns true if 'handler_lh' and 'handler_rh' have identical app_id,
// profile_path, and has_origin_wildcard values.
bool HasExpectedIdenticalFields(const base::Value& handler_lh,
const base::Value& handler_rh) {
auto handler_view_lh = GetConstHandlerView(handler_lh);
auto handler_view_rh = GetConstHandlerView(handler_rh);
if (!handler_view_lh || !handler_view_rh)
return false;
if (handler_view_lh->app_id != handler_view_rh->app_id)
return false;
if (handler_view_lh->profile_path != handler_view_rh->profile_path)
return false;
if (handler_view_lh->has_origin_wildcard !=
handler_view_rh->has_origin_wildcard) {
return false;
}
return true;
}
} // namespace
void RegisterLocalStatePrefs(PrefRegistrySimple* registry) {
DCHECK(registry);
registry->RegisterDictionaryPref(prefs::kWebAppsUrlHandlerInfo);
}
void AddWebApp(PrefService* local_state,
const AppId& app_id,
const base::FilePath& profile_path,
const apps::UrlHandlers& url_handlers,
const base::Time& time) {
if (profile_path.empty() || url_handlers.empty())
return;
DictionaryPrefUpdate update(local_state, prefs::kWebAppsUrlHandlerInfo);
base::Value* const pref_value = update.Get();
if (!pref_value || !pref_value->is_dict())
return;
for (const apps::UrlHandlerInfo& handler_info : url_handlers) {
const url::Origin& origin = handler_info.origin;
if (origin.opaque())
continue;
base::Value new_handler(
NewHandler(app_id, profile_path, handler_info, time));
base::Value::List* const handlers =
pref_value->GetDict().FindList(origin.Serialize());
// One or more apps are already associated with this origin.
if (handlers) {
auto it =
std::find_if(handlers->begin(), handlers->end(),
[&app_id, &profile_path](const base::Value& handler) {
return IsHandlerForApp(app_id, profile_path,
/*match_app_id=*/true, handler);
});
// If there is already an entry with the same app_id and profile, replace
// it. Otherwise, add new entry to the end.
if (it != handlers->end()) {
*it = std::move(new_handler);
} else {
handlers->Append(std::move(new_handler));
}
} else {
base::Value::List new_handlers;
new_handlers.Append(std::move(new_handler));
pref_value->GetDict().Set(origin.Serialize(), std::move(new_handlers));
}
}
}
void UpdateWebApp(PrefService* local_state,
const AppId& app_id,
const base::FilePath& profile_path,
apps::UrlHandlers new_url_handlers,
const base::Time& time) {
DictionaryPrefUpdate update(local_state, prefs::kWebAppsUrlHandlerInfo);
base::Value* const pref_value = update.Get();
if (!pref_value || !pref_value->is_dict())
return;
// In order to update data in URL handler prefs relevant to 'app_id' and
// 'profile_path', perform an exhaustive search of all handler entries under
// all keys. The previous url_handlers data could have had entries under any
// origin key.
std::vector<std::string> origins_to_remove;
for (auto origin_value : pref_value->DictItems()) {
const std::string& origin_str = origin_value.first;
base::Value::List curent_handlers =
std::move(origin_value.second.GetList());
// Remove any existing handler values that were written previously for the
// same app_id and profile but are no longer found in 'new_url_handlers'.
curent_handlers
.EraseIf(
// Returns true if 'current_handler' should be removed because it
// was previously added for 'app_id' and 'profile_path' but is no
// longer found in 'new_url_handlers' from the update.
[&app_id, &profile_path, &new_url_handlers, &time,
&origin_str](base::Value& current_handler) {
if (!IsHandlerForApp(app_id, profile_path,
/*match_app_id=*/true, current_handler)) {
return false;
}
// Determine if 'current_handler' value has a corresponding
// UrlHandlerInfo in 'new_url_handlers'. If not, it is no longer
// relevant to the updated app and can be removed.
const auto same_origin_it = base::ranges::find_if(
new_url_handlers,
[&current_handler,
&origin_str](const apps::UrlHandlerInfo& new_handler) {
if (origin_str != new_handler.origin.Serialize())
return false;
absl::optional<bool> current_has_origin_wildcard =
current_handler.FindBoolKey(kHasOriginWildcard);
if (!current_has_origin_wildcard)
return false;
if (*current_has_origin_wildcard !=
new_handler.has_origin_wildcard) {
return false;
}
return true;
});
if (same_origin_it == new_url_handlers.end())
return true;
// If include_paths or exclude_paths have changed, replace the
// current handler value with the new handler value.
base::Value new_handler =
NewHandler(app_id, profile_path, *same_origin_it, time);
// 'exclude_paths' can be updated without invalidating the user
// preferences that are stored within include_paths.
DCHECK(HasExpectedIdenticalFields(current_handler, new_handler));
MaybeUpdateIncludePaths(current_handler, new_handler);
UpdateExcludePaths(current_handler, new_handler);
// Remove new handler from container now that it has been updated
// in prefs.
new_url_handlers.erase(same_origin_it);
return false;
});
// Replace list if it contains entries or remove its origin key from prefs.
if (!curent_handlers.empty()) {
origin_value.second = base::Value(std::move(curent_handlers));
} else {
origins_to_remove.push_back(origin_value.first);
}
}
// Remove any origin keys that have no more entries.
for (const auto& origin_to_remove : origins_to_remove)
pref_value->RemoveKey(origin_to_remove);
// Add the remaining items in 'new_url_handlers'.
AddWebApp(local_state, app_id, profile_path, new_url_handlers, time);
}
void RemoveWebApp(PrefService* local_state,
const AppId& app_id,
const base::FilePath& profile_path) {
if (app_id.empty() || profile_path.empty())
return;
DictionaryPrefUpdate update(local_state, prefs::kWebAppsUrlHandlerInfo);
base::Value* const pref_value = update.Get();
if (!pref_value || !pref_value->is_dict())
return;
RemoveEntries(*pref_value, app_id, profile_path);
}
void RemoveProfile(PrefService* local_state,
const base::FilePath& profile_path) {
if (profile_path.empty())
return;
DictionaryPrefUpdate update(local_state, prefs::kWebAppsUrlHandlerInfo);
base::Value* const pref_value = update.Get();
if (!pref_value || !pref_value->is_dict())
return;
RemoveEntries(*pref_value, /*app_id*/ "", profile_path);
}
bool IsHandlerForProfile(const base::Value& handler,
const base::FilePath& profile_path) {
auto handler_view = GetConstHandlerView(handler);
if (!handler_view)
return false;
return handler_view->profile_path == profile_path;
}
bool ProfileHasUrlHandlers(PrefService* local_state,
const base::FilePath& profile_path) {
const base::Value& pref_value =
local_state->GetValue(prefs::kWebAppsUrlHandlerInfo);
if (!pref_value.is_dict())
return false;
for (const auto origin_value : pref_value.DictItems()) {
for (const auto& handler : origin_value.second.GetListDeprecated()) {
if (IsHandlerForProfile(handler, profile_path))
return true;
}
}
return false;
}
void Clear(PrefService* local_state) {
DictionaryPrefUpdate update(local_state, prefs::kWebAppsUrlHandlerInfo);
base::Value* const pref_value = update.Get();
pref_value->DictClear();
}
std::vector<UrlHandlerLaunchParams> FindMatchingUrlHandlers(
PrefService* local_state,
const GURL& url) {
if (!url.is_valid())
return {};
const base::Value& pref_value =
local_state->GetValue(prefs::kWebAppsUrlHandlerInfo);
if (!pref_value.is_dict())
return {};
return FindMatches(pref_value, url);
}
void SaveOpenInApp(PrefService* local_state,
const AppId& app_id,
const base::FilePath& profile_path,
const GURL& url,
const base::Time& time) {
DCHECK(!profile_path.empty());
DCHECK(!app_id.empty());
SaveChoice(local_state, &app_id, &profile_path, url,
UrlHandlerSavedChoice::kInApp, time);
}
void SaveOpenInBrowser(PrefService* local_state,
const GURL& url,
const base::Time& time) {
SaveChoice(local_state, /*app_id=*/nullptr, /*profile_path=*/nullptr, url,
UrlHandlerSavedChoice::kInBrowser, time);
}
void ResetSavedChoice(PrefService* local_state,
const absl::optional<std::string>& app_id,
const base::FilePath& profile_path,
const std::string& origin,
bool has_origin_wildcard,
const std::string& url_path,
const base::Time& time) {
DictionaryPrefUpdate update(local_state, prefs::kWebAppsUrlHandlerInfo);
base::Value* const pref_value = update.Get();
if (!pref_value || !pref_value->is_dict())
return;
base::Value* const handlers_mutable = pref_value->FindListKey(origin);
if (!handlers_mutable)
return;
for (auto& handler : handlers_mutable->GetListDeprecated()) {
auto handler_view = GetHandlerView(handler);
if (!handler_view)
continue;
if (handler_view->profile_path != profile_path)
continue;
// Do not filter by app_id if no value is provided.
if (app_id && handler_view->app_id != *app_id)
continue;
if (handler_view->has_origin_wildcard != has_origin_wildcard)
continue;
// Reset the choice and timestamp in every include_paths dict. where the
// path member matches |url_path|.
UpdateSavedChoiceInIncludePaths(PathSet({url_path}),
UrlHandlerSavedChoice::kNone, time,
handler_view->include_paths);
}
}
absl::optional<const HandlerView> GetConstHandlerView(
const base::Value& handler) {
if (!handler.is_dict())
return absl::nullopt;
const std::string* const handler_app_id = handler.FindStringKey(kAppId);
if (!handler_app_id)
return absl::nullopt;
absl::optional<base::FilePath> handler_profile_path =
base::ValueToFilePath(handler.FindKey(kProfilePath));
if (!handler_profile_path)
return absl::nullopt;
absl::optional<bool> has_origin_wildcard =
handler.FindBoolKey(kHasOriginWildcard);
if (!has_origin_wildcard)
return absl::nullopt;
base::Value* include_paths =
const_cast<base::Value&>(handler).FindListKey(kIncludePaths);
if (!include_paths || !include_paths->is_list())
return absl::nullopt;
base::Value* exclude_paths =
const_cast<base::Value&>(handler).FindListKey(kExcludePaths);
if (!exclude_paths || !exclude_paths->is_list())
return absl::nullopt;
HandlerView handler_view = {
*handler_app_id, handler_profile_path.value(),
*has_origin_wildcard, *include_paths,
*exclude_paths,
};
return handler_view;
}
absl::optional<HandlerView> GetHandlerView(base::Value& handler) {
absl::optional<const HandlerView> handler_view = GetConstHandlerView(handler);
if (!handler_view)
return absl::nullopt;
return const_cast<HandlerView&>(*handler_view);
}
} // namespace url_handler_prefs
} // namespace web_app