blob: 8e0cc53edb0d7a4dcb7c74bbda37322d47300702 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// 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/components/url_handler_prefs.h"
#include <algorithm>
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/optional.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/util/values/values_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 "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* kExcludePaths = "exclude_paths";
constexpr const char* kHasOriginWildcard = "has_origin_wildcard";
constexpr const char* kPaths = "paths";
constexpr const char* kProfilePath = "profile_path";
// 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;
}
}
// Return if |url_path| matches any path in |paths|. A path in |paths| can
// contain one wildcard * at the end.
// |url_path| always starts with a '/', as it's the result of GURL::path().
bool UrlPathMatches(const std::string& url_path, const base::Value& paths) {
if (!paths.is_list())
return false;
for (const auto& path : paths.GetList()) {
std::string path_str = path.GetString();
if (path_str.back() == '*') {
// Remove the wildcard and check if it's the same as the first several
// characters of |url_path|.
path_str = path_str.substr(0, path_str.length() - 1);
if (base::StartsWith(url_path, path_str))
return true;
} else {
// |path_str| doesn't contain a wildcard, check for an exact match.
if (path_str == url_path)
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|.
// |origin_trimmed| indicates if the input URL's origin had to be shortened to
// find a matching key. If true, filter out and matches that did not allow an
// origin prefix wildcard in their manifest.
void FilterAndAddMatches(const base::Value& all_handlers,
const GURL& url,
bool origin_trimmed,
std::vector<UrlHandlerLaunchParams>& matches) {
if (!all_handlers.is_list())
return;
for (auto& handler : all_handlers.GetList()) {
if (!handler.is_dict())
continue;
const std::string* const app_id = handler.FindStringKey(kAppId);
if (!app_id || app_id->empty())
continue;
base::Optional<base::FilePath> profile_path =
util::ValueToFilePath(handler.FindKey(kProfilePath));
if (!profile_path || profile_path->empty())
continue;
if (origin_trimmed) {
base::Optional<bool> has_wildcard =
handler.FindBoolKey(kHasOriginWildcard);
if (!has_wildcard || !*has_wildcard)
continue;
}
const std::string& url_path = url.path();
bool path_matches = true;
const base::Value* const paths = handler.FindListKey(kPaths);
bool paths_exist = paths && paths->is_list() && !paths->GetList().empty();
if (paths_exist)
path_matches = UrlPathMatches(url_path, *paths);
const base::Value* const exclude_paths = handler.FindListKey(kExcludePaths);
bool exclude_paths_exist = exclude_paths && exclude_paths->is_list() &&
!exclude_paths->GetList().empty();
if (exclude_paths_exist) {
bool match_exclude_path = UrlPathMatches(url_path, *exclude_paths);
// If |paths| and |exclude_paths| are both not empty, only |url_path|
// that matches a path and does not match an exclude path is considered
// a match.
// If |paths| is empty and |exclude_paths| is not, |url_path| that does
// not match any exclude path is considered a match.
path_matches = paths_exist ? (path_matches && !match_exclude_path)
: !match_exclude_path;
}
if (path_matches)
matches.emplace_back(*profile_path, *app_id, url);
}
}
// 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;
std::string origin_str = origin.Serialize();
bool origin_trimmed(false);
while (true) {
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);
}
// If a key matching the input URL's origin is not found, shorten the origin
// by sub-domain and try again. This enables matching against manifest
// "url_handlers" origins that contain wildcard prefixes.
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;
} else {
// There is no more '.'. Stop looking.
break;
}
}
return matches;
}
base::Value GetPathsValue(const std::vector<std::string>& paths) {
base::Value paths_value(base::Value::Type::LIST);
for (const auto& path : paths)
paths_value.Append(path);
return paths_value;
}
base::Value NewHandler(const AppId& app_id,
const base::FilePath& profile_path,
const apps::UrlHandlerInfo& info) {
base::Value value(base::Value::Type::DICTIONARY);
value.SetStringKey(kAppId, app_id);
value.SetKey(kProfilePath, util::FilePathToValue(profile_path));
value.SetBoolKey(kHasOriginWildcard, info.has_origin_wildcard);
// Set paths and exclude paths from associated app.
value.SetKey(kPaths, GetPathsValue(info.paths));
value.SetKey(kExcludePaths, GetPathsValue(info.exclude_paths));
// TODO(crbug/1072058): Set "user_permission" field when implementing user
// settings in the chrome://settings page.
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) {
const std::string* const app_id_local = handler.FindStringKey(kAppId);
base::Optional<base::FilePath> profile_path_local =
util::ValueToFilePath(handler.FindKey(kProfilePath));
if (!app_id_local || !profile_path_local)
return false;
if (*profile_path_local != profile_path)
return false;
return !match_app_id || *app_id_local == 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::ListStorage handlers = origin_value.second.TakeList();
handlers.erase(
std::remove_if(handlers.begin(), handlers.end(),
[&app_id, &profile_path](const base::Value& handler) {
return IsHandlerForApp(
app_id, profile_path,
/*match_app_id=*/!app_id.empty(), handler);
}),
handlers.end());
// 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);
}
} // 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) {
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));
base::Value* const handlers_mutable =
pref_value->FindListKey(origin.Serialize());
// One or more apps are already associated with this origin.
if (handlers_mutable) {
base::Value::ListStorage handlers = handlers_mutable->TakeList();
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.push_back(std::move(new_handler));
}
*handlers_mutable = base::Value(std::move(handlers));
} else {
base::Value new_handlers(base::Value::Type::LIST);
new_handlers.Append(std::move(new_handler));
pref_value->SetKey(origin.Serialize(), std::move(new_handlers));
}
}
}
void UpdateWebApp(PrefService* local_state,
const AppId& app_id,
const base::FilePath& profile_path,
const apps::UrlHandlers& url_handlers) {
// TODO(crbug/1072058): Handle "user_permission" field when it is
// implemented.
RemoveWebApp(local_state, app_id, profile_path);
AddWebApp(local_state, app_id, profile_path, url_handlers);
}
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);
}
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* const pref_value =
local_state->Get(prefs::kWebAppsUrlHandlerInfo);
if (!pref_value || !pref_value->is_dict())
return {};
return FindMatches(*pref_value, url);
}
} // namespace url_handler_prefs
} // namespace web_app