chore: bump chromium to 138.0.7204.97 (37-x-y) (#47619)
* chore: bump chromium in DEPS to 138.0.7204.51 * chore: bump chromium in DEPS to 138.0.7204.97 * Revert "Reland "FSA: Only normalize the hardcoded rules once during initialization"" https://chromium-review.googlesource.com/c/chromium/src/+/6687843 --------- Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com> Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
This commit is contained in:
parent
65c062d3f9
commit
cc05e5f40e
4 changed files with 297 additions and 478 deletions
2
DEPS
2
DEPS
|
@ -2,7 +2,7 @@ gclient_gn_args_from = 'src'
|
|||
|
||||
vars = {
|
||||
'chromium_version':
|
||||
'138.0.7204.49',
|
||||
'138.0.7204.97',
|
||||
'node_version':
|
||||
'v22.17.0',
|
||||
'nan_version':
|
||||
|
|
|
@ -8,10 +8,18 @@ it in Electron and prevent drift from Chrome's blocklist. We should look for a w
|
|||
to upstream this change to Chrome.
|
||||
|
||||
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
|
||||
index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255cab529c646 100644
|
||||
index 3514864559de0d2f2f36fda9b0add0b7b88f3b2a..44318ce3bed67e6f83f3687d11500ddfecd4aef4 100644
|
||||
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
|
||||
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
|
||||
@@ -82,11 +82,13 @@
|
||||
@@ -45,7 +45,6 @@
|
||||
#include "chrome/browser/ui/file_system_access/file_system_access_dangerous_file_dialog.h"
|
||||
#include "chrome/browser/ui/file_system_access/file_system_access_dialogs.h"
|
||||
#include "chrome/browser/ui/file_system_access/file_system_access_restricted_directory_dialog.h"
|
||||
-#include "chrome/common/chrome_paths.h"
|
||||
#include "chrome/grit/generated_resources.h"
|
||||
#include "components/content_settings/core/browser/host_content_settings_map.h"
|
||||
#include "components/content_settings/core/common/content_settings.h"
|
||||
@@ -81,11 +80,13 @@
|
||||
#include "chrome/browser/ui/browser_window.h"
|
||||
#include "chrome/browser/ui/tabs/public/tab_features.h"
|
||||
#include "chrome/browser/ui/views/file_system_access/file_system_access_page_action_controller.h"
|
||||
|
@ -25,7 +33,7 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
#include "components/tabs/public/tab_interface.h"
|
||||
#if BUILDFLAG(ENABLE_PLATFORM_APPS)
|
||||
#include "extensions/browser/extension_registry.h" // nogncheck
|
||||
@@ -262,182 +264,10 @@ bool MaybeIsLocalUNCPath(const base::FilePath& path) {
|
||||
@@ -261,129 +262,10 @@ bool MaybeIsLocalUNCPath(const base::FilePath& path) {
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -33,52 +41,31 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
-// the struct below.
|
||||
-constexpr const int kNoBasePathKey = -1;
|
||||
-
|
||||
-// A wrapper around `base::NormalizeFilePath` that returns its result instead of
|
||||
-// using an out parameter.
|
||||
-base::FilePath NormalizeFilePath(const base::FilePath& path) {
|
||||
- CHECK(path.IsAbsolute());
|
||||
- // TODO(crbug.com/368130513O): On Windows, this call will fail if the target
|
||||
- // file path is greater than MAX_PATH. We should decide how to handle this
|
||||
- // scenario.
|
||||
- base::FilePath normalized_path;
|
||||
- if (!base::NormalizeFilePath(path, &normalized_path)) {
|
||||
- return path;
|
||||
- }
|
||||
- CHECK_EQ(path.empty(), normalized_path.empty());
|
||||
- return normalized_path;
|
||||
-}
|
||||
-
|
||||
-using BlockType = ChromeFileSystemAccessPermissionContext::BlockType;
|
||||
-
|
||||
-std::unique_ptr<ChromeFileSystemAccessPermissionContext::BlockPathRules>
|
||||
-GenerateBlockPaths(bool should_normalize_file_path) {
|
||||
- static constexpr ChromeFileSystemAccessPermissionContext::BlockPath
|
||||
- kBlockPaths[] = {
|
||||
- // Don't allow users to share their entire home directory, entire
|
||||
- // desktop or entire documents folder, but do allow sharing anything
|
||||
- // inside those directories not otherwise blocked.
|
||||
-std::vector<ChromeFileSystemAccessPermissionContext::BlockedPath>
|
||||
-GenerateBlockedPath() {
|
||||
- return {
|
||||
- // Don't allow users to share their entire home directory, entire desktop
|
||||
- // or entire documents folder, but do allow sharing anything inside those
|
||||
- // directories not otherwise blocked.
|
||||
- {base::DIR_HOME, nullptr, BlockType::kDontBlockChildren},
|
||||
- {base::DIR_USER_DESKTOP, nullptr, BlockType::kDontBlockChildren},
|
||||
- {chrome::DIR_USER_DOCUMENTS, nullptr, BlockType::kDontBlockChildren},
|
||||
- // Similar restrictions for the downloads directory.
|
||||
- {chrome::DIR_DEFAULT_DOWNLOADS, nullptr,
|
||||
- BlockType::kDontBlockChildren},
|
||||
- {chrome::DIR_DEFAULT_DOWNLOADS, nullptr, BlockType::kDontBlockChildren},
|
||||
- {chrome::DIR_DEFAULT_DOWNLOADS_SAFE, nullptr,
|
||||
- BlockType::kDontBlockChildren},
|
||||
- // The Chrome installation itself should not be modified by the web.
|
||||
- {base::DIR_EXE, nullptr, BlockType::kBlockAllChildren},
|
||||
- {base::DIR_MODULE, nullptr, BlockType::kBlockAllChildren},
|
||||
- {base::DIR_ASSETS, nullptr, BlockType::kBlockAllChildren},
|
||||
- // And neither should the configuration of at least the currently
|
||||
- // running
|
||||
- // Chrome instance (note that this does not take --user-data-dir
|
||||
- // command
|
||||
- // And neither should the configuration of at least the currently running
|
||||
- // Chrome instance (note that this does not take --user-data-dir command
|
||||
- // line overrides into account).
|
||||
- {chrome::DIR_USER_DATA, nullptr, BlockType::kBlockAllChildren},
|
||||
- // ~/.ssh is pretty sensitive on all platforms, so block access to
|
||||
- // that.
|
||||
- {base::DIR_HOME, FILE_PATH_LITERAL(".ssh"),
|
||||
- BlockType::kBlockAllChildren},
|
||||
- // ~/.ssh is pretty sensitive on all platforms, so block access to that.
|
||||
- {base::DIR_HOME, FILE_PATH_LITERAL(".ssh"), BlockType::kBlockAllChildren},
|
||||
- // And limit access to ~/.gnupg as well.
|
||||
- {base::DIR_HOME, FILE_PATH_LITERAL(".gnupg"),
|
||||
- BlockType::kBlockAllChildren},
|
||||
|
@ -92,8 +79,7 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
- {base::DIR_ROAMING_APP_DATA, nullptr, BlockType::kBlockAllChildren},
|
||||
- {base::DIR_LOCAL_APP_DATA, nullptr, BlockType::kBlockAllChildren},
|
||||
- {base::DIR_COMMON_APP_DATA, nullptr, BlockType::kBlockAllChildren},
|
||||
- // Opening a file from an MTP device, such as a smartphone or a
|
||||
- // camera, is
|
||||
- // Opening a file from an MTP device, such as a smartphone or a camera, is
|
||||
- // implemented by Windows as opening a file in the temporary internet
|
||||
- // files directory. To support that, allow opening files in that
|
||||
- // directory, but not whole directories.
|
||||
|
@ -116,10 +102,8 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
- // Allow access to other cloud files, such as Google Drive.
|
||||
- {base::DIR_HOME, FILE_PATH_LITERAL("Library/CloudStorage"),
|
||||
- BlockType::kDontBlockChildren},
|
||||
- // Allow the site to interact with data from its corresponding
|
||||
- // natively
|
||||
- // installed (sandboxed) application. It would be nice to limit a site
|
||||
- // to
|
||||
- // Allow the site to interact with data from its corresponding natively
|
||||
- // installed (sandboxed) application. It would be nice to limit a site to
|
||||
- // access only _its_ corresponding natively installed application, but
|
||||
- // unfortunately there's no straightforward way to do that. See
|
||||
- // https://crbug.com/984641#c22.
|
||||
|
@ -135,25 +119,20 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
-#endif
|
||||
-#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID)
|
||||
- // On Linux also block access to devices via /dev.
|
||||
- {kNoBasePathKey, FILE_PATH_LITERAL("/dev"),
|
||||
- BlockType::kBlockAllChildren},
|
||||
- {kNoBasePathKey, FILE_PATH_LITERAL("/dev"), BlockType::kBlockAllChildren},
|
||||
- // And security sensitive data in /proc and /sys.
|
||||
- {kNoBasePathKey, FILE_PATH_LITERAL("/proc"),
|
||||
- BlockType::kBlockAllChildren},
|
||||
- {kNoBasePathKey, FILE_PATH_LITERAL("/sys"),
|
||||
- BlockType::kBlockAllChildren},
|
||||
- {kNoBasePathKey, FILE_PATH_LITERAL("/sys"), BlockType::kBlockAllChildren},
|
||||
- // And system files in /boot and /etc.
|
||||
- {kNoBasePathKey, FILE_PATH_LITERAL("/boot"),
|
||||
- BlockType::kBlockAllChildren},
|
||||
- {kNoBasePathKey, FILE_PATH_LITERAL("/etc"),
|
||||
- BlockType::kBlockAllChildren},
|
||||
- // And block all of ~/.config, matching the similar restrictions on
|
||||
- // mac
|
||||
- {kNoBasePathKey, FILE_PATH_LITERAL("/etc"), BlockType::kBlockAllChildren},
|
||||
- // And block all of ~/.config, matching the similar restrictions on mac
|
||||
- // and windows.
|
||||
- {base::DIR_HOME, FILE_PATH_LITERAL(".config"),
|
||||
- BlockType::kBlockAllChildren},
|
||||
- // Block ~/.dbus as well, just in case, although there probably isn't
|
||||
- // much
|
||||
- // Block ~/.dbus as well, just in case, although there probably isn't much
|
||||
- // a website can do with access to that directory and its contents.
|
||||
- {base::DIR_HOME, FILE_PATH_LITERAL(".dbus"),
|
||||
- BlockType::kBlockAllChildren},
|
||||
|
@ -165,45 +144,21 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
- // TODO(crbug.com/40095723): Refine this list, for example add
|
||||
- // XDG_CONFIG_HOME when it is not set ~/.config?
|
||||
- };
|
||||
-}
|
||||
-
|
||||
- // ChromeOS supports multi-user sign-in. base::DIR_HOME only returns the
|
||||
- // profile path for the primary user, the first user to sign in. We want to
|
||||
- // use the `profile_path` instead since that's associated with user that
|
||||
- // initiated this blocklist check.
|
||||
- //
|
||||
- // TODO(crbug.com/375490221): Improve the ChromeOS blocklist logic.
|
||||
- constexpr bool kUseProfilePathForDirHome = BUILDFLAG(IS_CHROMEOS);
|
||||
- // Populate the hard-coded rules.
|
||||
- auto block_path_rules = std::make_unique<
|
||||
- ChromeFileSystemAccessPermissionContext::BlockPathRules>();
|
||||
-
|
||||
- for (const auto& blocked_path : kBlockPaths) {
|
||||
- base::FilePath path;
|
||||
- if (blocked_path.base_path_key != kNoBasePathKey) {
|
||||
- if (kUseProfilePathForDirHome &&
|
||||
- blocked_path.base_path_key == base::DIR_HOME) {
|
||||
- block_path_rules->profile_based_block_path_rules_.emplace_back(
|
||||
- blocked_path.path, blocked_path.type);
|
||||
- continue;
|
||||
-// A wrapper around `base::NormalizeFilePath` that returns its result instead of
|
||||
-// using an out parameter.
|
||||
-base::FilePath NormalizeFilePath(const base::FilePath& path) {
|
||||
- CHECK(path.IsAbsolute());
|
||||
- // TODO(crbug.com/368130513O): On Windows, this call will fail if the target
|
||||
- // file path is greater than MAX_PATH. We should decide how to handle this
|
||||
- // scenario.
|
||||
- base::FilePath normalized_path;
|
||||
- if (!base::NormalizeFilePath(path, &normalized_path)) {
|
||||
- return path;
|
||||
- }
|
||||
-
|
||||
- if (!base::PathService::Get(blocked_path.base_path_key, &path)) {
|
||||
- continue;
|
||||
- }
|
||||
-
|
||||
- if (blocked_path.path) {
|
||||
- path = path.Append(blocked_path.path);
|
||||
- }
|
||||
- } else {
|
||||
- DCHECK(blocked_path.path);
|
||||
- path = base::FilePath(blocked_path.path);
|
||||
- }
|
||||
- block_path_rules->block_path_rules_.emplace_back(
|
||||
- should_normalize_file_path ? NormalizeFilePath(path) : path,
|
||||
- blocked_path.type);
|
||||
- }
|
||||
-
|
||||
- return block_path_rules;
|
||||
- CHECK_EQ(path.empty(), normalized_path.empty());
|
||||
- return normalized_path;
|
||||
-}
|
||||
+// This patch moves the deleted content from this file over to
|
||||
+// chrome/browser/file_system_access/chrome_file_system_access_permission_context.h.
|
||||
|
@ -212,24 +167,7 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
|
||||
// Checks if `path` should be blocked by the `rules`.
|
||||
// The BlockType of the nearest ancestor of a path to check is what
|
||||
@@ -1261,16 +1091,6 @@ struct ChromeFileSystemAccessPermissionContext::OriginState {
|
||||
std::unique_ptr<base::RetainingOneShotTimer> cleanup_timer;
|
||||
};
|
||||
|
||||
-ChromeFileSystemAccessPermissionContext::BlockPathRules::BlockPathRules() =
|
||||
- default;
|
||||
-ChromeFileSystemAccessPermissionContext::BlockPathRules::~BlockPathRules() =
|
||||
- default;
|
||||
-ChromeFileSystemAccessPermissionContext::BlockPathRules::BlockPathRules(
|
||||
- const BlockPathRules& other) = default;
|
||||
-ChromeFileSystemAccessPermissionContext::BlockPathRules&
|
||||
-ChromeFileSystemAccessPermissionContext::BlockPathRules::operator=(
|
||||
- const BlockPathRules& other) = default;
|
||||
-
|
||||
ChromeFileSystemAccessPermissionContext::
|
||||
ChromeFileSystemAccessPermissionContext(content::BrowserContext* context,
|
||||
const base::Clock* clock)
|
||||
@@ -1289,7 +1109,7 @@ ChromeFileSystemAccessPermissionContext::
|
||||
@@ -1237,7 +1119,7 @@ ChromeFileSystemAccessPermissionContext::
|
||||
#if BUILDFLAG(IS_ANDROID)
|
||||
one_time_permissions_tracker_.Observe(
|
||||
OneTimePermissionsTrackerFactory::GetForBrowserContext(context));
|
||||
|
@ -238,7 +176,7 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
auto* provider = web_app::WebAppProvider::GetForWebApps(
|
||||
Profile::FromBrowserContext(profile_));
|
||||
if (provider) {
|
||||
@@ -2551,7 +2371,7 @@ void ChromeFileSystemAccessPermissionContext::OnShutdown() {
|
||||
@@ -2443,7 +2325,7 @@ void ChromeFileSystemAccessPermissionContext::OnShutdown() {
|
||||
one_time_permissions_tracker_.Reset();
|
||||
}
|
||||
|
||||
|
@ -247,7 +185,7 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
void ChromeFileSystemAccessPermissionContext::OnWebAppInstalled(
|
||||
const webapps::AppId& app_id) {
|
||||
if (!base::FeatureList::IsEnabled(
|
||||
@@ -3108,11 +2928,7 @@ bool ChromeFileSystemAccessPermissionContext::OriginHasExtendedPermission(
|
||||
@@ -3000,11 +2882,7 @@ bool ChromeFileSystemAccessPermissionContext::OriginHasExtendedPermission(
|
||||
const url::Origin& origin) {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
|
||||
|
@ -260,7 +198,7 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
if (!base::FeatureList::IsEnabled(
|
||||
features::kFileSystemAccessPersistentPermissions)) {
|
||||
return false;
|
||||
@@ -3156,6 +2972,7 @@ bool ChromeFileSystemAccessPermissionContext::OriginHasExtendedPermission(
|
||||
@@ -3048,6 +2926,7 @@ bool ChromeFileSystemAccessPermissionContext::OriginHasExtendedPermission(
|
||||
: WebAppInstallStatus::kUninstalled;
|
||||
return app_has_os_integration;
|
||||
#endif // BUILDFLAG(IS_ANDROID)
|
||||
|
@ -269,10 +207,10 @@ index 5b543e82abb17cbb91bf37fa6bac016b6053eb93..117b6b05b3a1c39183a45830b97255ca
|
|||
|
||||
void ChromeFileSystemAccessPermissionContext::SetOriginExtendedPermissionByUser(
|
||||
diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
|
||||
index f647100981fd98d8511d07a6d7e100910e38a0f2..14e1b3c8ec78429f5a845f54cc973e7c77ea8bc4 100644
|
||||
index 46a2019587b534add3c89f464cdf7261a67e7cce..57e3f7c966a45114b17701a851b191be88d72e7c 100644
|
||||
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
|
||||
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.h
|
||||
@@ -9,10 +9,13 @@
|
||||
@@ -9,9 +9,12 @@
|
||||
#include <vector>
|
||||
|
||||
#include "base/auto_reset.h"
|
||||
|
@ -281,73 +219,61 @@ index f647100981fd98d8511d07a6d7e100910e38a0f2..14e1b3c8ec78429f5a845f54cc973e7c
|
|||
#include "base/files/file_path.h"
|
||||
+#include "base/files/file_util.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
+#include "base/path_service.h"
|
||||
#include "base/scoped_observation.h"
|
||||
#include "base/sequence_checker.h"
|
||||
#include "base/time/clock.h"
|
||||
@@ -22,6 +25,7 @@
|
||||
@@ -21,7 +24,7 @@
|
||||
#include "chrome/browser/file_system_access/file_system_access_permission_request_manager.h"
|
||||
#include "chrome/browser/permissions/one_time_permissions_tracker.h"
|
||||
#include "chrome/browser/permissions/one_time_permissions_tracker_observer.h"
|
||||
-#include "components/enterprise/buildflags/buildflags.h"
|
||||
+#include "chrome/common/chrome_paths.h"
|
||||
#include "components/enterprise/buildflags/buildflags.h"
|
||||
#include "components/permissions/features.h"
|
||||
#include "components/permissions/object_permission_context_base.h"
|
||||
@@ -403,6 +407,183 @@ class ChromeFileSystemAccessPermissionContext
|
||||
return is_block_path_rules_init_complete_;
|
||||
}
|
||||
#include "content/public/browser/file_system_access_permission_context.h"
|
||||
@@ -31,7 +34,7 @@
|
||||
#include "chrome/browser/web_applications/web_app_install_manager_observer.h"
|
||||
#endif
|
||||
|
||||
-#if BUILDFLAG(ENTERPRISE_CLOUD_CONTENT_ANALYSIS)
|
||||
+#if 0
|
||||
#include "components/enterprise/common/files_scan_data.h"
|
||||
#endif
|
||||
|
||||
@@ -371,6 +374,130 @@ class ChromeFileSystemAccessPermissionContext
|
||||
// KeyedService:
|
||||
void Shutdown() override;
|
||||
|
||||
+ // Sentinel used to indicate that no PathService key is specified for a path in
|
||||
+ // the struct below.
|
||||
+ static constexpr const int kNoBasePathKey = -1;
|
||||
+
|
||||
+ // A wrapper around `base::NormalizeFilePath` that returns its result instead of
|
||||
+ // using an out parameter.
|
||||
+ static base::FilePath NormalizeFilePath(const base::FilePath& path) {
|
||||
+ CHECK(path.IsAbsolute());
|
||||
+ // TODO(crbug.com/368130513O): On Windows, this call will fail if the target
|
||||
+ // file path is greater than MAX_PATH. We should decide how to handle this
|
||||
+ // scenario.
|
||||
+ base::FilePath normalized_path;
|
||||
+ if (!base::NormalizeFilePath(path, &normalized_path)) {
|
||||
+ return path;
|
||||
+ }
|
||||
+ CHECK_EQ(path.empty(), normalized_path.empty());
|
||||
+ return normalized_path;
|
||||
+ }
|
||||
+
|
||||
+ using BlockType = ChromeFileSystemAccessPermissionContext::BlockType;
|
||||
+
|
||||
+ static std::unique_ptr<ChromeFileSystemAccessPermissionContext::BlockPathRules>
|
||||
+ GenerateBlockPaths(bool should_normalize_file_path) {
|
||||
+ static constexpr ChromeFileSystemAccessPermissionContext::BlockPath
|
||||
+ kBlockPaths[] = {
|
||||
+ // Don't allow users to share their entire home directory, entire
|
||||
+ // desktop or entire documents folder, but do allow sharing anything
|
||||
+ // inside those directories not otherwise blocked.
|
||||
+ static std::vector<ChromeFileSystemAccessPermissionContext::BlockedPath>
|
||||
+ GenerateBlockedPath() {
|
||||
+ return {
|
||||
+ // Don't allow users to share their entire home directory, entire desktop
|
||||
+ // or entire documents folder, but do allow sharing anything inside those
|
||||
+ // directories not otherwise blocked.
|
||||
+ {base::DIR_HOME, nullptr, BlockType::kDontBlockChildren},
|
||||
+ {base::DIR_USER_DESKTOP, nullptr, BlockType::kDontBlockChildren},
|
||||
+ {chrome::DIR_USER_DOCUMENTS, nullptr, BlockType::kDontBlockChildren},
|
||||
+ // Similar restrictions for the downloads directory.
|
||||
+ {chrome::DIR_DEFAULT_DOWNLOADS, nullptr,
|
||||
+ BlockType::kDontBlockChildren},
|
||||
+ {chrome::DIR_DEFAULT_DOWNLOADS, nullptr, BlockType::kDontBlockChildren},
|
||||
+ {chrome::DIR_DEFAULT_DOWNLOADS_SAFE, nullptr,
|
||||
+ BlockType::kDontBlockChildren},
|
||||
+ // The Chrome installation itself should not be modified by the web.
|
||||
+ {base::DIR_EXE, nullptr, BlockType::kBlockAllChildren},
|
||||
+ {base::DIR_MODULE, nullptr, BlockType::kBlockAllChildren},
|
||||
+ {base::DIR_ASSETS, nullptr, BlockType::kBlockAllChildren},
|
||||
+ // And neither should the configuration of at least the currently
|
||||
+ // running
|
||||
+ // Chrome instance (note that this does not take --user-data-dir
|
||||
+ // command
|
||||
+ // And neither should the configuration of at least the currently running
|
||||
+ // Chrome instance (note that this does not take --user-data-dir command
|
||||
+ // line overrides into account).
|
||||
+ {chrome::DIR_USER_DATA, nullptr, BlockType::kBlockAllChildren},
|
||||
+ // ~/.ssh is pretty sensitive on all platforms, so block access to
|
||||
+ // that.
|
||||
+ {base::DIR_HOME, FILE_PATH_LITERAL(".ssh"),
|
||||
+ BlockType::kBlockAllChildren},
|
||||
+ // ~/.ssh is pretty sensitive on all platforms, so block access to that.
|
||||
+ {base::DIR_HOME, FILE_PATH_LITERAL(".ssh"), BlockType::kBlockAllChildren},
|
||||
+ // And limit access to ~/.gnupg as well.
|
||||
+ {base::DIR_HOME, FILE_PATH_LITERAL(".gnupg"),
|
||||
+ BlockType::kBlockAllChildren},
|
||||
|
@ -361,8 +287,7 @@ index f647100981fd98d8511d07a6d7e100910e38a0f2..14e1b3c8ec78429f5a845f54cc973e7c
|
|||
+ {base::DIR_ROAMING_APP_DATA, nullptr, BlockType::kBlockAllChildren},
|
||||
+ {base::DIR_LOCAL_APP_DATA, nullptr, BlockType::kBlockAllChildren},
|
||||
+ {base::DIR_COMMON_APP_DATA, nullptr, BlockType::kBlockAllChildren},
|
||||
+ // Opening a file from an MTP device, such as a smartphone or a
|
||||
+ // camera, is
|
||||
+ // Opening a file from an MTP device, such as a smartphone or a camera, is
|
||||
+ // implemented by Windows as opening a file in the temporary internet
|
||||
+ // files directory. To support that, allow opening files in that
|
||||
+ // directory, but not whole directories.
|
||||
|
@ -385,10 +310,8 @@ index f647100981fd98d8511d07a6d7e100910e38a0f2..14e1b3c8ec78429f5a845f54cc973e7c
|
|||
+ // Allow access to other cloud files, such as Google Drive.
|
||||
+ {base::DIR_HOME, FILE_PATH_LITERAL("Library/CloudStorage"),
|
||||
+ BlockType::kDontBlockChildren},
|
||||
+ // Allow the site to interact with data from its corresponding
|
||||
+ // natively
|
||||
+ // installed (sandboxed) application. It would be nice to limit a site
|
||||
+ // to
|
||||
+ // Allow the site to interact with data from its corresponding natively
|
||||
+ // installed (sandboxed) application. It would be nice to limit a site to
|
||||
+ // access only _its_ corresponding natively installed application, but
|
||||
+ // unfortunately there's no straightforward way to do that. See
|
||||
+ // https://crbug.com/984641#c22.
|
||||
|
@ -404,25 +327,20 @@ index f647100981fd98d8511d07a6d7e100910e38a0f2..14e1b3c8ec78429f5a845f54cc973e7c
|
|||
+ #endif
|
||||
+ #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID)
|
||||
+ // On Linux also block access to devices via /dev.
|
||||
+ {kNoBasePathKey, FILE_PATH_LITERAL("/dev"),
|
||||
+ BlockType::kBlockAllChildren},
|
||||
+ {kNoBasePathKey, FILE_PATH_LITERAL("/dev"), BlockType::kBlockAllChildren},
|
||||
+ // And security sensitive data in /proc and /sys.
|
||||
+ {kNoBasePathKey, FILE_PATH_LITERAL("/proc"),
|
||||
+ BlockType::kBlockAllChildren},
|
||||
+ {kNoBasePathKey, FILE_PATH_LITERAL("/sys"),
|
||||
+ BlockType::kBlockAllChildren},
|
||||
+ {kNoBasePathKey, FILE_PATH_LITERAL("/sys"), BlockType::kBlockAllChildren},
|
||||
+ // And system files in /boot and /etc.
|
||||
+ {kNoBasePathKey, FILE_PATH_LITERAL("/boot"),
|
||||
+ BlockType::kBlockAllChildren},
|
||||
+ {kNoBasePathKey, FILE_PATH_LITERAL("/etc"),
|
||||
+ BlockType::kBlockAllChildren},
|
||||
+ // And block all of ~/.config, matching the similar restrictions on
|
||||
+ // mac
|
||||
+ {kNoBasePathKey, FILE_PATH_LITERAL("/etc"), BlockType::kBlockAllChildren},
|
||||
+ // And block all of ~/.config, matching the similar restrictions on mac
|
||||
+ // and windows.
|
||||
+ {base::DIR_HOME, FILE_PATH_LITERAL(".config"),
|
||||
+ BlockType::kBlockAllChildren},
|
||||
+ // Block ~/.dbus as well, just in case, although there probably isn't
|
||||
+ // much
|
||||
+ // Block ~/.dbus as well, just in case, although there probably isn't much
|
||||
+ // a website can do with access to that directory and its contents.
|
||||
+ {base::DIR_HOME, FILE_PATH_LITERAL(".dbus"),
|
||||
+ BlockType::kBlockAllChildren},
|
||||
|
@ -434,47 +352,32 @@ index f647100981fd98d8511d07a6d7e100910e38a0f2..14e1b3c8ec78429f5a845f54cc973e7c
|
|||
+ // TODO(crbug.com/40095723): Refine this list, for example add
|
||||
+ // XDG_CONFIG_HOME when it is not set ~/.config?
|
||||
+ };
|
||||
+
|
||||
+ // ChromeOS supports multi-user sign-in. base::DIR_HOME only returns the
|
||||
+ // profile path for the primary user, the first user to sign in. We want to
|
||||
+ // use the `profile_path` instead since that's associated with user that
|
||||
+ // initiated this blocklist check.
|
||||
+ //
|
||||
+ // TODO(crbug.com/375490221): Improve the ChromeOS blocklist logic.
|
||||
+ constexpr bool kUseProfilePathForDirHome = BUILDFLAG(IS_CHROMEOS);
|
||||
+ // Populate the hard-coded rules.
|
||||
+ auto block_path_rules = std::make_unique<
|
||||
+ ChromeFileSystemAccessPermissionContext::BlockPathRules>();
|
||||
+
|
||||
+ for (const auto& blocked_path : kBlockPaths) {
|
||||
+ base::FilePath path;
|
||||
+ if (blocked_path.base_path_key != kNoBasePathKey) {
|
||||
+ if (kUseProfilePathForDirHome &&
|
||||
+ blocked_path.base_path_key == base::DIR_HOME) {
|
||||
+ block_path_rules->profile_based_block_path_rules_.emplace_back(
|
||||
+ blocked_path.path, blocked_path.type);
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ if (!base::PathService::Get(blocked_path.base_path_key, &path)) {
|
||||
+ continue;
|
||||
+ // A wrapper around `base::NormalizeFilePath` that returns its result instead of
|
||||
+ // using an out parameter.
|
||||
+ base::FilePath NormalizeFilePath(const base::FilePath& path) {
|
||||
+ CHECK(path.IsAbsolute());
|
||||
+ // TODO(crbug.com/368130513O): On Windows, this call will fail if the target
|
||||
+ // file path is greater than MAX_PATH. We should decide how to handle this
|
||||
+ // scenario.
|
||||
+ base::FilePath normalized_path;
|
||||
+ if (!base::NormalizeFilePath(path, &normalized_path)) {
|
||||
+ return path;
|
||||
+ }
|
||||
+
|
||||
+ if (blocked_path.path) {
|
||||
+ path = path.Append(blocked_path.path);
|
||||
+ }
|
||||
+ } else {
|
||||
+ DCHECK(blocked_path.path);
|
||||
+ path = base::FilePath(blocked_path.path);
|
||||
+ }
|
||||
+ block_path_rules->block_path_rules_.emplace_back(
|
||||
+ should_normalize_file_path ? NormalizeFilePath(path) : path,
|
||||
+ blocked_path.type);
|
||||
+ }
|
||||
+
|
||||
+ return block_path_rules;
|
||||
+ CHECK_EQ(path.empty(), normalized_path.empty());
|
||||
+ return normalized_path;
|
||||
+ }
|
||||
+
|
||||
protected:
|
||||
SEQUENCE_CHECKER(sequence_checker_);
|
||||
|
||||
@@ -390,7 +517,7 @@ class ChromeFileSystemAccessPermissionContext
|
||||
|
||||
void PermissionGrantDestroyed(PermissionGrantImpl* grant);
|
||||
|
||||
-#if BUILDFLAG(ENTERPRISE_CLOUD_CONTENT_ANALYSIS)
|
||||
+#if 0
|
||||
void OnContentAnalysisComplete(
|
||||
std::vector<content::PathInfo> entries,
|
||||
EntriesAllowedByEnterprisePolicyCallback callback,
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
#include "base/base_paths.h"
|
||||
#include "base/files/file_path.h"
|
||||
#include "base/files/file_util.h"
|
||||
#include "base/json/values_util.h"
|
||||
#include "base/path_service.h"
|
||||
#include "base/task/bind_post_task.h"
|
||||
|
@ -40,16 +39,6 @@
|
|||
#include "ui/base/l10n/l10n_util.h"
|
||||
#include "url/origin.h"
|
||||
|
||||
ChromeFileSystemAccessPermissionContext::BlockPathRules::BlockPathRules() =
|
||||
default;
|
||||
ChromeFileSystemAccessPermissionContext::BlockPathRules::~BlockPathRules() =
|
||||
default;
|
||||
ChromeFileSystemAccessPermissionContext::BlockPathRules::BlockPathRules(
|
||||
const BlockPathRules& other) = default;
|
||||
ChromeFileSystemAccessPermissionContext::BlockPathRules&
|
||||
ChromeFileSystemAccessPermissionContext::BlockPathRules::operator=(
|
||||
const BlockPathRules& other) = default;
|
||||
|
||||
namespace gin {
|
||||
|
||||
template <>
|
||||
|
@ -152,53 +141,51 @@ bool MaybeIsLocalUNCPath(const base::FilePath& path) {
|
|||
}
|
||||
#endif // BUILDFLAG(IS_WIN)
|
||||
|
||||
bool ShouldBlockAccessToPath(
|
||||
base::FilePath path,
|
||||
// Describes a rule for blocking a directory, which can be constructed
|
||||
// dynamically (based on state) or statically (from kBlockedPaths).
|
||||
struct BlockPathRule {
|
||||
base::FilePath path;
|
||||
BlockType type;
|
||||
};
|
||||
|
||||
bool ShouldBlockAccessToPath(const base::FilePath& path,
|
||||
HandleType handle_type,
|
||||
std::vector<ChromeFileSystemAccessPermissionContext::BlockPathRule>
|
||||
extra_rules,
|
||||
ChromeFileSystemAccessPermissionContext::BlockPathRules block_path_rules) {
|
||||
std::vector<BlockPathRule> rules) {
|
||||
DCHECK(!path.empty());
|
||||
DCHECK(path.IsAbsolute());
|
||||
|
||||
path = ChromeFileSystemAccessPermissionContext::NormalizeFilePath(path);
|
||||
for (auto& rule : extra_rules) {
|
||||
rule.path =
|
||||
ChromeFileSystemAccessPermissionContext::NormalizeFilePath(rule.path);
|
||||
}
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
// On Windows, local UNC paths are rejected, as UNC path can be written in a
|
||||
// way that can bypass the blocklist.
|
||||
if (MaybeIsLocalUNCPath(path)) {
|
||||
if (MaybeIsLocalUNCPath(path))
|
||||
return true;
|
||||
#endif // BUILDFLAG(IS_WIN)
|
||||
|
||||
// Add the hard-coded rules to the dynamic rules.
|
||||
for (auto const& [key, rule_path, type] :
|
||||
ChromeFileSystemAccessPermissionContext::GenerateBlockedPath()) {
|
||||
if (key == ChromeFileSystemAccessPermissionContext::kNoBasePathKey) {
|
||||
rules.emplace_back(base::FilePath{rule_path}, type);
|
||||
} else if (base::FilePath block_path;
|
||||
base::PathService::Get(key, &block_path)) {
|
||||
rules.emplace_back(rule_path ? block_path.Append(rule_path) : block_path,
|
||||
type);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
base::FilePath nearest_ancestor;
|
||||
BlockType nearest_ancestor_block_type = BlockType::kDontBlockChildren;
|
||||
auto should_block_with_rule = [&](const base::FilePath& block_path,
|
||||
BlockType block_type) -> bool {
|
||||
if (path == block_path || path.IsParent(block_path)) {
|
||||
VLOG(1) << "Blocking access to " << path << " because it is a parent of "
|
||||
<< block_path;
|
||||
for (const auto& block : rules) {
|
||||
if (path == block.path || path.IsParent(block.path)) {
|
||||
DLOG(INFO) << "Blocking access to " << path
|
||||
<< " because it is a parent of " << block.path;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (block_path.IsParent(path) &&
|
||||
(nearest_ancestor.empty() || nearest_ancestor.IsParent(block_path))) {
|
||||
nearest_ancestor = block_path;
|
||||
nearest_ancestor_block_type = block_type;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const auto* block_rules_ptr :
|
||||
{&extra_rules, &block_path_rules.block_path_rules_}) {
|
||||
for (const auto& block : *block_rules_ptr) {
|
||||
if (should_block_with_rule(block.path, block.type)) {
|
||||
return true;
|
||||
}
|
||||
if (block.path.IsParent(path) &&
|
||||
(nearest_ancestor.empty() || nearest_ancestor.IsParent(block.path))) {
|
||||
nearest_ancestor = block.path;
|
||||
nearest_ancestor_block_type = block.type;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,8 +193,6 @@ bool ShouldBlockAccessToPath(
|
|||
// nearest ancestor does not block access to its children. Grant access.
|
||||
if (nearest_ancestor.empty() ||
|
||||
nearest_ancestor_block_type == BlockType::kDontBlockChildren) {
|
||||
VLOG(1) << "Not blocking access to " << path << " because it is inside "
|
||||
<< nearest_ancestor << " and it's kDontBlockChildren";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -215,14 +200,12 @@ bool ShouldBlockAccessToPath(
|
|||
// access to directories. Grant access.
|
||||
if (handle_type == HandleType::kFile &&
|
||||
nearest_ancestor_block_type == BlockType::kBlockNestedDirectories) {
|
||||
VLOG(1) << "Not blocking access to " << path << " because it is inside "
|
||||
<< nearest_ancestor << " and it's kBlockNestedDirectories";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The nearest ancestor blocks access to its children, so block access.
|
||||
VLOG(1) << "Blocking access to " << path << " because it is inside "
|
||||
<< nearest_ancestor << " and it's kBlockAllChildren";
|
||||
DLOG(INFO) << "Blocking access to " << path << " because it is inside "
|
||||
<< nearest_ancestor;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -463,30 +446,11 @@ FileSystemAccessPermissionContext::FileSystemAccessPermissionContext(
|
|||
const base::Clock* clock)
|
||||
: browser_context_(browser_context), clock_(clock) {
|
||||
DETACH_FROM_SEQUENCE(sequence_checker_);
|
||||
ResetBlockPaths();
|
||||
}
|
||||
|
||||
FileSystemAccessPermissionContext::~FileSystemAccessPermissionContext() =
|
||||
default;
|
||||
|
||||
void FileSystemAccessPermissionContext::ResetBlockPaths() {
|
||||
is_block_path_rules_init_complete_ = false;
|
||||
base::ThreadPool::PostTaskAndReplyWithResult(
|
||||
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
|
||||
base::BindOnce(
|
||||
&ChromeFileSystemAccessPermissionContext::GenerateBlockPaths, true),
|
||||
base::BindOnce(&FileSystemAccessPermissionContext::UpdateBlockPaths,
|
||||
weak_factory_.GetWeakPtr()));
|
||||
}
|
||||
|
||||
void FileSystemAccessPermissionContext::UpdateBlockPaths(
|
||||
std::unique_ptr<ChromeFileSystemAccessPermissionContext::BlockPathRules>
|
||||
block_path_rules) {
|
||||
block_path_rules_ = std::move(block_path_rules);
|
||||
is_block_path_rules_init_complete_ = true;
|
||||
block_rules_check_callbacks_.Notify(*block_path_rules_.get());
|
||||
}
|
||||
|
||||
scoped_refptr<content::FileSystemAccessPermissionGrant>
|
||||
FileSystemAccessPermissionContext::GetReadPermissionGrant(
|
||||
const url::Origin& origin,
|
||||
|
@ -640,26 +604,12 @@ void FileSystemAccessPermissionContext::ConfirmSensitiveEntryAccess(
|
|||
std::move(after_blocklist_check_callback));
|
||||
}
|
||||
|
||||
void FileSystemAccessPermissionContext::CheckShouldBlockAccessToPathAndReply(
|
||||
base::FilePath path,
|
||||
HandleType handle_type,
|
||||
std::vector<ChromeFileSystemAccessPermissionContext::BlockPathRule>
|
||||
extra_rules,
|
||||
base::OnceCallback<void(bool)> callback,
|
||||
ChromeFileSystemAccessPermissionContext::BlockPathRules block_path_rules) {
|
||||
base::ThreadPool::PostTaskAndReplyWithResult(
|
||||
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
|
||||
base::BindOnce(&ShouldBlockAccessToPath, path, handle_type, extra_rules,
|
||||
block_path_rules),
|
||||
std::move(callback));
|
||||
}
|
||||
|
||||
void FileSystemAccessPermissionContext::CheckPathAgainstBlocklist(
|
||||
const content::PathInfo& path_info,
|
||||
HandleType handle_type,
|
||||
base::OnceCallback<void(bool)> callback) {
|
||||
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
|
||||
// TODO(crbug.com/40101272): Figure out what external paths should be
|
||||
// TODO(https://crbug.com/1009970): Figure out what external paths should be
|
||||
// blocked. We could resolve the external path to a local path, and check for
|
||||
// blocked directories based on that, but that doesn't work well. Instead we
|
||||
// should have a separate Chrome OS only code path to block for example the
|
||||
|
@ -669,27 +619,15 @@ void FileSystemAccessPermissionContext::CheckPathAgainstBlocklist(
|
|||
return;
|
||||
}
|
||||
|
||||
// Unlike the DIR_USER_DATA check, this handles the --user-data-dir override.
|
||||
// We check for the user data dir in two different ways: directly, via the
|
||||
// profile manager, where it exists (it does not in unit tests), and via the
|
||||
// profile's directory, assuming the profile dir is a child of the user data
|
||||
// dir.
|
||||
std::vector<ChromeFileSystemAccessPermissionContext::BlockPathRule>
|
||||
extra_rules;
|
||||
if (is_block_path_rules_init_complete_) {
|
||||
// The rules initialization is completed, we can just post the task to a
|
||||
// anonymous blocking traits.
|
||||
CheckShouldBlockAccessToPathAndReply(path_info.path, handle_type,
|
||||
extra_rules, std::move(callback),
|
||||
*block_path_rules_.get());
|
||||
return;
|
||||
}
|
||||
// The check must be performed after the rules initialization is done.
|
||||
block_rules_check_subscription_.push_back(block_rules_check_callbacks_.Add(
|
||||
base::BindOnce(&FileSystemAccessPermissionContext::
|
||||
CheckShouldBlockAccessToPathAndReply,
|
||||
weak_factory_.GetWeakPtr(), path_info.path, handle_type,
|
||||
extra_rules, std::move(callback))));
|
||||
std::vector<BlockPathRule> extra_rules;
|
||||
extra_rules.emplace_back(browser_context_->GetPath().DirName(),
|
||||
BlockType::kBlockAllChildren);
|
||||
|
||||
base::ThreadPool::PostTaskAndReplyWithResult(
|
||||
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
|
||||
base::BindOnce(&ShouldBlockAccessToPath, path_info.path, handle_type,
|
||||
extra_rules),
|
||||
std::move(callback));
|
||||
}
|
||||
|
||||
void FileSystemAccessPermissionContext::PerformAfterWriteChecks(
|
||||
|
|
|
@ -12,14 +12,13 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/callback_list.h"
|
||||
#include "base/functional/callback_forward.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/time/clock.h"
|
||||
#include "base/time/default_clock.h"
|
||||
#include "base/values.h"
|
||||
#include "chrome/browser/file_system_access/chrome_file_system_access_permission_context.h" // nogncheck
|
||||
#include "components/keyed_service/core/keyed_service.h"
|
||||
#include "content/public/browser/file_system_access_permission_context.h"
|
||||
|
||||
class GURL;
|
||||
|
||||
|
@ -136,14 +135,6 @@ class FileSystemAccessPermissionContext
|
|||
|
||||
void PermissionGrantDestroyed(PermissionGrantImpl* grant);
|
||||
|
||||
void CheckShouldBlockAccessToPathAndReply(
|
||||
base::FilePath path,
|
||||
HandleType handle_type,
|
||||
std::vector<ChromeFileSystemAccessPermissionContext::BlockPathRule>
|
||||
extra_rules,
|
||||
base::OnceCallback<void(bool)> callback,
|
||||
ChromeFileSystemAccessPermissionContext::BlockPathRules block_path_rules);
|
||||
|
||||
void CheckPathAgainstBlocklist(const content::PathInfo& path,
|
||||
HandleType handle_type,
|
||||
base::OnceCallback<void(bool)> callback);
|
||||
|
@ -168,11 +159,6 @@ class FileSystemAccessPermissionContext
|
|||
const base::FilePath& path,
|
||||
GrantType grant_type) const;
|
||||
|
||||
void ResetBlockPaths();
|
||||
void UpdateBlockPaths(
|
||||
std::unique_ptr<ChromeFileSystemAccessPermissionContext::BlockPathRules>
|
||||
block_path_rules);
|
||||
|
||||
base::WeakPtr<FileSystemAccessPermissionContext> GetWeakPtr();
|
||||
|
||||
const raw_ptr<content::BrowserContext, DanglingUntriaged> browser_context_;
|
||||
|
@ -190,14 +176,6 @@ class FileSystemAccessPermissionContext
|
|||
std::map<base::FilePath, base::OnceCallback<void(SensitiveEntryResult)>>
|
||||
callback_map_;
|
||||
|
||||
std::unique_ptr<ChromeFileSystemAccessPermissionContext::BlockPathRules>
|
||||
block_path_rules_;
|
||||
bool is_block_path_rules_init_complete_ = false;
|
||||
std::vector<base::CallbackListSubscription> block_rules_check_subscription_;
|
||||
base::OnceCallbackList<void(
|
||||
ChromeFileSystemAccessPermissionContext::BlockPathRules)>
|
||||
block_rules_check_callbacks_;
|
||||
|
||||
base::WeakPtrFactory<FileSystemAccessPermissionContext> weak_factory_{this};
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue