diff options
Diffstat (limited to 'chromium/chrome/renderer/content_settings_observer.cc')
-rw-r--r-- | chromium/chrome/renderer/content_settings_observer.cc | 669 |
1 files changed, 669 insertions, 0 deletions
diff --git a/chromium/chrome/renderer/content_settings_observer.cc b/chromium/chrome/renderer/content_settings_observer.cc new file mode 100644 index 00000000000..e45e69f5164 --- /dev/null +++ b/chromium/chrome/renderer/content_settings_observer.cc @@ -0,0 +1,669 @@ +// Copyright (c) 2012 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/renderer/content_settings_observer.h" + +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/feature_list.h" +#include "base/metrics/histogram_macros.h" +#include "base/strings/string_number_conversions.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/client_hints.mojom.h" +#include "chrome/common/client_hints/client_hints.h" +#include "chrome/common/render_messages.h" +#include "chrome/common/ssl_insecure_content.h" +#include "components/content_settings/core/common/content_settings.h" +#include "components/content_settings/core/common/content_settings.mojom.h" +#include "components/content_settings/core/common/content_settings_pattern.h" +#include "components/content_settings/core/common/content_settings_utils.h" +#include "content/public/common/origin_util.h" +#include "content/public/common/previews_state.h" +#include "content/public/common/url_constants.h" +#include "content/public/renderer/document_state.h" +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_view.h" +#include "extensions/buildflags/buildflags.h" +#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" +#include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h" +#include "third_party/blink/public/platform/url_conversion.h" +#include "third_party/blink/public/platform/web_client_hints_type.h" +#include "third_party/blink/public/platform/web_security_origin.h" +#include "third_party/blink/public/platform/web_url.h" +#include "third_party/blink/public/web/web_document.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "third_party/blink/public/web/web_local_frame_client.h" +#include "third_party/blink/public/web/web_view.h" +#include "url/gurl.h" +#include "url/origin.h" +#include "url/url_constants.h" + +#if BUILDFLAG(ENABLE_EXTENSIONS) +#include "extensions/common/constants.h" +#include "extensions/common/extension.h" +#include "extensions/common/permissions/api_permission.h" +#include "extensions/common/permissions/permissions_data.h" +#include "extensions/renderer/dispatcher.h" +#include "extensions/renderer/renderer_extension_registry.h" +#endif + +using blink::WebDocument; +using blink::WebFrame; +using blink::WebLocalFrame; +using blink::WebSecurityOrigin; +using blink::WebString; +using blink::WebURL; +using blink::WebView; +using content::DocumentState; + +namespace { + +GURL GetOriginOrURL(const WebFrame* frame) { + url::Origin top_origin = url::Origin(frame->Top()->GetSecurityOrigin()); + // The |top_origin| is unique ("null") e.g., for file:// URLs. Use the + // document URL as the primary URL in those cases. + // TODO(alexmos): This is broken for --site-per-process, since top() can be a + // WebRemoteFrame which does not have a document(), and the WebRemoteFrame's + // URL is not replicated. See https://crbug.com/628759. + if (top_origin.opaque() && frame->Top()->IsWebLocalFrame()) + return frame->Top()->ToWebLocalFrame()->GetDocument().Url(); + return top_origin.GetURL(); +} + +// Allow passing both WebURL and GURL here, so that we can early return without +// allocating a new backing string if only the default rule matches. +template <typename URL> +ContentSetting GetContentSettingFromRules( + const ContentSettingsForOneType& rules, + const WebFrame* frame, + const URL& secondary_url) { + // If there is only one rule, it's the default rule and we don't need to match + // the patterns. + if (rules.size() == 1) { + DCHECK(rules[0].primary_pattern == ContentSettingsPattern::Wildcard()); + DCHECK(rules[0].secondary_pattern == ContentSettingsPattern::Wildcard()); + return rules[0].GetContentSetting(); + } + const GURL& primary_url = GetOriginOrURL(frame); + const GURL& secondary_gurl = secondary_url; + for (const auto& rule : rules) { + if (rule.primary_pattern.Matches(primary_url) && + rule.secondary_pattern.Matches(secondary_gurl)) { + return rule.GetContentSetting(); + } + } + NOTREACHED(); + return CONTENT_SETTING_DEFAULT; +} + +bool IsScriptDisabledForPreview(content::RenderFrame* render_frame) { + return render_frame->GetPreviewsState() & content::NOSCRIPT_ON; +} + +bool IsUniqueFrame(WebFrame* frame) { + return frame->GetSecurityOrigin().IsUnique() || + frame->Top()->GetSecurityOrigin().IsUnique(); +} + +} // namespace + +ContentSettingsObserver::ContentSettingsObserver( + content::RenderFrame* render_frame, + bool should_whitelist, + service_manager::BinderRegistry* registry) + : content::RenderFrameObserver(render_frame), + content::RenderFrameObserverTracker<ContentSettingsObserver>( + render_frame), + should_whitelist_(should_whitelist) { + ClearBlockedContentSettings(); + render_frame->GetWebFrame()->SetContentSettingsClient(this); + + render_frame->GetAssociatedInterfaceRegistry()->AddInterface( + base::Bind(&ContentSettingsObserver::OnContentSettingsRendererRequest, + base::Unretained(this))); + + content::RenderFrame* main_frame = + render_frame->GetRenderView()->GetMainRenderFrame(); + // TODO(nasko): The main frame is not guaranteed to be in the same process + // with this frame with --site-per-process. This code needs to be updated + // to handle this case. See https://crbug.com/496670. + if (main_frame && main_frame != render_frame) { + // Copy all the settings from the main render frame to avoid race conditions + // when initializing this data. See https://crbug.com/333308. + ContentSettingsObserver* parent = ContentSettingsObserver::Get(main_frame); + allow_running_insecure_content_ = parent->allow_running_insecure_content_; + temporarily_allowed_plugins_ = parent->temporarily_allowed_plugins_; + is_interstitial_page_ = parent->is_interstitial_page_; + } +} + +ContentSettingsObserver::~ContentSettingsObserver() { +} + +#if BUILDFLAG(ENABLE_EXTENSIONS) +void ContentSettingsObserver::SetExtensionDispatcher( + extensions::Dispatcher* extension_dispatcher) { + DCHECK(!extension_dispatcher_) + << "SetExtensionDispatcher() should only be called once."; + extension_dispatcher_ = extension_dispatcher; +} +#endif + +void ContentSettingsObserver::SetContentSettingRules( + const RendererContentSettingRules* content_setting_rules) { + content_setting_rules_ = content_setting_rules; + UMA_HISTOGRAM_COUNTS_1M("ClientHints.CountRulesReceived", + content_setting_rules_->client_hints_rules.size()); +} + +const RendererContentSettingRules* +ContentSettingsObserver::GetContentSettingRules() { + return content_setting_rules_; +} + +bool ContentSettingsObserver::IsPluginTemporarilyAllowed( + const std::string& identifier) { + // If the empty string is in here, it means all plugins are allowed. + // TODO(bauerb): Remove this once we only pass in explicit identifiers. + return base::Contains(temporarily_allowed_plugins_, identifier) || + base::Contains(temporarily_allowed_plugins_, std::string()); +} + +void ContentSettingsObserver::DidBlockContentType( + ContentSettingsType settings_type) { + DidBlockContentType(settings_type, base::string16()); +} + +void ContentSettingsObserver::DidBlockContentType( + ContentSettingsType settings_type, + const base::string16& details) { + // Send multiple ContentBlocked messages if details are provided. + bool newly_blocked = content_blocked_.insert(settings_type).second; + if (newly_blocked || !details.empty()) { + Send(new ChromeViewHostMsg_ContentBlocked(routing_id(), settings_type, + details)); + } +} + +bool ContentSettingsObserver::OnMessageReceived(const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(ContentSettingsObserver, message) + IPC_MESSAGE_HANDLER(ChromeViewMsg_RequestFileSystemAccessAsyncResponse, + OnRequestFileSystemAccessAsyncResponse) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + if (handled) + return true; + + // Don't swallow LoadBlockedPlugins messages, as they're sent to every + // blocked plugin. + IPC_BEGIN_MESSAGE_MAP(ContentSettingsObserver, message) + IPC_MESSAGE_HANDLER(ChromeViewMsg_LoadBlockedPlugins, OnLoadBlockedPlugins) + IPC_END_MESSAGE_MAP() + + return false; +} + +void ContentSettingsObserver::DidCommitProvisionalLoad( + bool is_same_document_navigation, + ui::PageTransition transition) { + blink::WebLocalFrame* frame = render_frame()->GetWebFrame(); + if (frame->Parent()) + return; // Not a top-level navigation. + + if (!is_same_document_navigation) { + // Clear "block" flags for the new page. This needs to happen before any of + // |allowScript()|, |allowScriptFromSource()|, |allowImage()|, or + // |allowPlugins()| is called for the new page so that these functions can + // correctly detect that a piece of content flipped from "not blocked" to + // "blocked". + ClearBlockedContentSettings(); + temporarily_allowed_plugins_.clear(); + } + + GURL url = frame->GetDocument().Url(); + // If we start failing this DCHECK, please makes sure we don't regress + // this bug: http://code.google.com/p/chromium/issues/detail?id=79304 + DCHECK(frame->GetDocument().GetSecurityOrigin().ToString() == "null" || + !url.SchemeIs(url::kDataScheme)); +} + +void ContentSettingsObserver::OnDestruct() { + delete this; +} + +void ContentSettingsObserver::SetAllowRunningInsecureContent() { + allow_running_insecure_content_ = true; + + // Reload if we are the main frame. + blink::WebLocalFrame* frame = render_frame()->GetWebFrame(); + if (!frame->Parent()) + frame->StartReload(blink::WebFrameLoadType::kReload); +} + +void ContentSettingsObserver::SetAsInterstitial() { + is_interstitial_page_ = true; +} + +void ContentSettingsObserver::OnContentSettingsRendererRequest( + mojo::PendingAssociatedReceiver<chrome::mojom::ContentSettingsRenderer> + receiver) { + receivers_.Add(this, std::move(receiver)); +} + +bool ContentSettingsObserver::AllowDatabase() { + WebLocalFrame* frame = render_frame()->GetWebFrame(); + if (IsUniqueFrame(frame)) + return false; + + bool result = false; + Send(new ChromeViewHostMsg_AllowDatabase( + routing_id(), frame->GetSecurityOrigin(), + frame->GetDocument().SiteForCookies(), + frame->GetDocument().TopFrameOrigin(), &result)); + return result; +} + +void ContentSettingsObserver::RequestFileSystemAccessAsync( + base::OnceCallback<void(bool)> callback) { + WebLocalFrame* frame = render_frame()->GetWebFrame(); + if (IsUniqueFrame(frame)) { + std::move(callback).Run(false); + return; + } + ++current_request_id_; + bool inserted = + permission_requests_ + .insert(std::make_pair(current_request_id_, std::move(callback))) + .second; + + // Verify there are no duplicate insertions. + DCHECK(inserted); + + Send(new ChromeViewHostMsg_RequestFileSystemAccessAsync( + routing_id(), current_request_id_, frame->GetSecurityOrigin(), + frame->GetDocument().SiteForCookies(), + frame->GetDocument().TopFrameOrigin())); +} + +bool ContentSettingsObserver::AllowImage(bool enabled_per_settings, + const WebURL& image_url) { + bool allow = enabled_per_settings; + if (enabled_per_settings) { + if (is_interstitial_page_) + return true; + + if (IsWhitelistedForContentSettings()) + return true; + + if (content_setting_rules_) { + allow = GetContentSettingFromRules(content_setting_rules_->image_rules, + render_frame()->GetWebFrame(), + image_url) != CONTENT_SETTING_BLOCK; + } + } + if (!allow) + DidBlockContentType(CONTENT_SETTINGS_TYPE_IMAGES); + return allow; +} + +bool ContentSettingsObserver::AllowIndexedDB(const WebSecurityOrigin& origin) { + WebLocalFrame* frame = render_frame()->GetWebFrame(); + if (IsUniqueFrame(frame)) + return false; + + bool result = false; + Send(new ChromeViewHostMsg_AllowIndexedDB( + routing_id(), frame->GetSecurityOrigin(), + frame->GetDocument().SiteForCookies(), + frame->GetDocument().TopFrameOrigin(), &result)); + return result; +} + +bool ContentSettingsObserver::AllowCacheStorage( + const blink::WebSecurityOrigin& origin) { + WebLocalFrame* frame = render_frame()->GetWebFrame(); + if (IsUniqueFrame(frame)) + return false; + + bool result = false; + Send(new ChromeViewHostMsg_AllowCacheStorage( + routing_id(), frame->GetSecurityOrigin(), + frame->GetDocument().SiteForCookies(), + frame->GetDocument().TopFrameOrigin(), &result)); + return result; +} + +bool ContentSettingsObserver::AllowScript(bool enabled_per_settings) { + if (!enabled_per_settings) + return false; + if (IsScriptDisabledForPreview(render_frame())) + return false; + if (is_interstitial_page_) + return true; + + blink::WebLocalFrame* frame = render_frame()->GetWebFrame(); + const auto it = cached_script_permissions_.find(frame); + if (it != cached_script_permissions_.end()) + return it->second; + + // Evaluate the content setting rules before + // IsWhitelistedForContentSettings(); if there is only the default rule + // allowing all scripts, it's quicker this way. + bool allow = true; + if (content_setting_rules_) { + ContentSetting setting = GetContentSettingFromRules( + content_setting_rules_->script_rules, frame, + url::Origin(frame->GetDocument().GetSecurityOrigin()).GetURL()); + allow = setting != CONTENT_SETTING_BLOCK; + } + allow = allow || IsWhitelistedForContentSettings(); + + cached_script_permissions_[frame] = allow; + return allow; +} + +bool ContentSettingsObserver::AllowScriptFromSource( + bool enabled_per_settings, + const blink::WebURL& script_url) { + if (!enabled_per_settings) + return false; + if (IsScriptDisabledForPreview(render_frame())) + return false; + if (is_interstitial_page_) + return true; + + bool allow = true; + if (content_setting_rules_) { + ContentSetting setting = + GetContentSettingFromRules(content_setting_rules_->script_rules, + render_frame()->GetWebFrame(), script_url); + allow = setting != CONTENT_SETTING_BLOCK; + } + return allow || IsWhitelistedForContentSettings(); +} + +bool ContentSettingsObserver::AllowStorage(bool local) { + WebLocalFrame* frame = render_frame()->GetWebFrame(); + if (IsUniqueFrame(frame)) + return false; + + StoragePermissionsKey key( + url::Origin(frame->GetDocument().GetSecurityOrigin()).GetURL(), local); + const auto permissions = cached_storage_permissions_.find(key); + if (permissions != cached_storage_permissions_.end()) + return permissions->second; + + bool result = false; + Send(new ChromeViewHostMsg_AllowDOMStorage( + routing_id(), frame->GetSecurityOrigin(), + frame->GetDocument().SiteForCookies(), + frame->GetDocument().TopFrameOrigin(), local, &result)); + cached_storage_permissions_[key] = result; + return result; +} + +bool ContentSettingsObserver::AllowReadFromClipboard(bool default_value) { + bool allowed = default_value; +#if BUILDFLAG(ENABLE_EXTENSIONS) + extensions::ScriptContext* current_context = + extension_dispatcher_->script_context_set().GetCurrent(); + if (current_context) { + allowed |= current_context->HasAPIPermission( + extensions::APIPermission::kClipboardRead); + } +#endif + return allowed; +} + +bool ContentSettingsObserver::AllowWriteToClipboard(bool default_value) { + bool allowed = default_value; +#if BUILDFLAG(ENABLE_EXTENSIONS) + // All blessed extension pages could historically write to the clipboard, so + // preserve that for compatibility. + extensions::ScriptContext* current_context = + extension_dispatcher_->script_context_set().GetCurrent(); + if (current_context) { + if (current_context->effective_context_type() == + extensions::Feature::BLESSED_EXTENSION_CONTEXT && + !current_context->IsForServiceWorker()) { + allowed = true; + } else { + allowed |= current_context->HasAPIPermission( + extensions::APIPermission::kClipboardWrite); + } + } +#endif + return allowed; +} + +bool ContentSettingsObserver::AllowMutationEvents(bool default_value) { + return IsPlatformApp() ? false : default_value; +} + +bool ContentSettingsObserver::AllowRunningInsecureContent( + bool allowed_per_settings, + const blink::WebSecurityOrigin& origin, + const blink::WebURL& resource_url) { + bool allow = allowed_per_settings; + + if (base::FeatureList::IsEnabled(features::kMixedContentSiteSetting)) { + if (content_setting_rules_) { + auto setting = GetContentSettingFromRules( + content_setting_rules_->mixed_content_rules, + render_frame()->GetWebFrame(), GURL()); + allow |= (setting == CONTENT_SETTING_ALLOW); + } + } else { + allow |= allow_running_insecure_content_; + if (!allow) { + DidBlockContentType(CONTENT_SETTINGS_TYPE_MIXEDSCRIPT); + } + } + + // Note: this implementation is a mirror of + // Browser::ShouldAllowRunningInsecureContent. + FilteredReportInsecureContentRan(GURL(resource_url)); + + return allow; +} + +bool ContentSettingsObserver::AllowAutoplay(bool default_value) { + if (!content_setting_rules_) + return default_value; + + blink::WebLocalFrame* frame = render_frame()->GetWebFrame(); + return GetContentSettingFromRules( + content_setting_rules_->autoplay_rules, frame, + url::Origin(frame->GetDocument().GetSecurityOrigin()).GetURL()) == + CONTENT_SETTING_ALLOW; +} + +bool ContentSettingsObserver::AllowPopupsAndRedirects(bool default_value) { + if (!content_setting_rules_) + return default_value; + blink::WebLocalFrame* frame = render_frame()->GetWebFrame(); + return GetContentSettingFromRules( + content_setting_rules_->popup_redirect_rules, frame, + url::Origin(frame->GetDocument().GetSecurityOrigin()).GetURL()) == + CONTENT_SETTING_ALLOW; +} + +void ContentSettingsObserver::PassiveInsecureContentFound( + const blink::WebURL& resource_url) { + // Note: this implementation is a mirror of + // Browser::PassiveInsecureContentFound. + ReportInsecureContent(SslInsecureContentType::DISPLAY); + FilteredReportInsecureContentDisplayed(GURL(resource_url)); +} + +void ContentSettingsObserver::PersistClientHints( + const blink::WebEnabledClientHints& enabled_client_hints, + base::TimeDelta duration, + const blink::WebURL& url) { + if (duration <= base::TimeDelta()) + return; + + const GURL primary_url(url); + const url::Origin primary_origin = url::Origin::Create(primary_url); + if (!content::IsOriginSecure(primary_url)) + return; + + // TODO(tbansal): crbug.com/735518. Determine if the value should be + // merged or overridden. Also, determine if the merger should happen on the + // browser side or the renderer. If the value needs to be overridden, + // this method should not return early if |update_count| is 0. + std::vector<::blink::mojom::WebClientHintsType> client_hints; + static constexpr size_t kWebClientHintsCount = + static_cast<size_t>(blink::mojom::WebClientHintsType::kMaxValue) + 1; + client_hints.reserve(kWebClientHintsCount); + + for (size_t i = 0; i < kWebClientHintsCount; ++i) { + if (enabled_client_hints.IsEnabled( + static_cast<blink::mojom::WebClientHintsType>(i))) { + client_hints.push_back(static_cast<blink::mojom::WebClientHintsType>(i)); + } + } + size_t update_count = client_hints.size(); + if (update_count == 0) + return; + + UMA_HISTOGRAM_CUSTOM_TIMES( + "ClientHints.PersistDuration", duration, base::TimeDelta::FromSeconds(1), + // TODO(crbug.com/949034): Rename and fix this histogram to have some + // intended max value. We throw away the 32 most-significant bits of the + // 64-bit time delta in milliseconds. Before it happened silently in + // histogram.cc, now it is explicit here. The previous value of 365 days + // effectively turns into roughly 17 days when getting cast to int. + base::TimeDelta::FromMilliseconds( + static_cast<int>(base::TimeDelta::FromDays(365).InMilliseconds())), + 100); + + UMA_HISTOGRAM_COUNTS_100("ClientHints.UpdateSize", update_count); + + // Notify the embedder. + mojo::AssociatedRemote<client_hints::mojom::ClientHints> host_observer; + render_frame()->GetRemoteAssociatedInterfaces()->GetInterface(&host_observer); + host_observer->PersistClientHints(primary_origin, std::move(client_hints), + duration); +} + +void ContentSettingsObserver::GetAllowedClientHintsFromSource( + const blink::WebURL& url, + blink::WebEnabledClientHints* client_hints) const { + if (!content_setting_rules_) + return; + + if (content_setting_rules_->client_hints_rules.empty()) + return; + + client_hints::GetAllowedClientHintsFromSource( + url, + content_setting_rules_->client_hints_rules, client_hints); +} + +void ContentSettingsObserver::DidNotAllowPlugins() { + DidBlockContentType(CONTENT_SETTINGS_TYPE_PLUGINS); +} + +void ContentSettingsObserver::DidNotAllowScript() { + DidBlockContentType(CONTENT_SETTINGS_TYPE_JAVASCRIPT); +} + +void ContentSettingsObserver::OnLoadBlockedPlugins( + const std::string& identifier) { + temporarily_allowed_plugins_.insert(identifier); +} + +void ContentSettingsObserver::OnRequestFileSystemAccessAsyncResponse( + int request_id, + bool allowed) { + auto it = permission_requests_.find(request_id); + if (it == permission_requests_.end()) + return; + + base::OnceCallback<void(bool)> callback = std::move(it->second); + permission_requests_.erase(it); + + std::move(callback).Run(allowed); +} + +void ContentSettingsObserver::ClearBlockedContentSettings() { + content_blocked_.clear(); + cached_storage_permissions_.clear(); + cached_script_permissions_.clear(); +} + +bool ContentSettingsObserver::IsPlatformApp() { +#if BUILDFLAG(ENABLE_EXTENSIONS) + blink::WebLocalFrame* frame = render_frame()->GetWebFrame(); + WebSecurityOrigin origin = frame->GetDocument().GetSecurityOrigin(); + const extensions::Extension* extension = GetExtension(origin); + return extension && extension->is_platform_app(); +#else + return false; +#endif +} + +#if BUILDFLAG(ENABLE_EXTENSIONS) +const extensions::Extension* ContentSettingsObserver::GetExtension( + const WebSecurityOrigin& origin) const { + if (origin.Protocol().Ascii() != extensions::kExtensionScheme) + return nullptr; + + const std::string extension_id = origin.Host().Utf8().data(); + if (!extension_dispatcher_->IsExtensionActive(extension_id)) + return nullptr; + + return extensions::RendererExtensionRegistry::Get()->GetByID(extension_id); +} +#endif + +// static +bool ContentSettingsObserver::IsWhitelistedForContentSettings() const { + if (should_whitelist_) + return true; + + // Whitelist ftp directory listings, as they require JavaScript to function + // properly. + if (render_frame()->IsFTPDirectoryListing()) + return true; + + const WebDocument& document = render_frame()->GetWebFrame()->GetDocument(); + return IsWhitelistedForContentSettings(document.GetSecurityOrigin(), + document.Url()); +} + +bool ContentSettingsObserver::IsWhitelistedForContentSettings( + const WebSecurityOrigin& origin, + const WebURL& document_url) { + if (document_url.GetString() == content::kUnreachableWebDataURL) + return true; + + if (origin.IsUnique()) + return false; // Uninitialized document? + + blink::WebString protocol = origin.Protocol(); + + if (protocol == content::kChromeUIScheme) + return true; // Browser UI elements should still work. + + if (protocol == content::kChromeDevToolsScheme) + return true; // DevTools UI elements should still work. + +#if BUILDFLAG(ENABLE_EXTENSIONS) + if (protocol == extensions::kExtensionScheme) + return true; +#endif + + // If the scheme is file:, an empty file name indicates a directory listing, + // which requires JavaScript to function properly. + if (protocol == url::kFileScheme && + document_url.ProtocolIs(url::kFileScheme)) { + return GURL(document_url).ExtractFileName().empty(); + } + return false; +} |