diff options
author | rogerta@chromium.org <rogerta@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-12-15 14:45:05 +0000 |
---|---|---|
committer | rogerta@chromium.org <rogerta@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-12-15 14:45:05 +0000 |
commit | 0753db59d038d9a7feaf11f02e4df7587a2b4d1f (patch) | |
tree | 338094b1771c1a9d82e74e5c743d924fb01b44ec /chrome_frame | |
parent | 74ca044ddeb27bca294a88f397e85429ce28e2b6 (diff) | |
download | chromium_src-0753db59d038d9a7feaf11f02e4df7587a2b4d1f.zip chromium_src-0753db59d038d9a7feaf11f02e4df7587a2b4d1f.tar.gz chromium_src-0753db59d038d9a7feaf11f02e4df7587a2b4d1f.tar.bz2 |
Fixing a regression introduced with r69101, which now prevents Chrome Frame
from loading chrome extension URL in privileged mode using the NPAPI plugin.
The behaviour that was implemented only for the ActiveX control has been
moved into the base class NavigationConstraintsImpl, which both the ActieX
and NPAPI plugin derive from.
TEST=Added new unit tests for this case
BUG=0
Review URL: http://codereview.chromium.org/5814004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@69257 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome_frame')
-rw-r--r-- | chrome_frame/chrome_frame_activex.cc | 14 | ||||
-rw-r--r-- | chrome_frame/chrome_frame_activex_base.h | 38 | ||||
-rw-r--r-- | chrome_frame/chrome_frame_npapi.cc | 34 | ||||
-rw-r--r-- | chrome_frame/chrome_frame_npapi.h | 4 | ||||
-rw-r--r-- | chrome_frame/chrome_frame_plugin.h | 22 | ||||
-rw-r--r-- | chrome_frame/navigation_constraints.cc | 17 | ||||
-rw-r--r-- | chrome_frame/navigation_constraints.h | 14 | ||||
-rw-r--r-- | chrome_frame/test/util_unittests.cc | 37 |
8 files changed, 96 insertions, 84 deletions
diff --git a/chrome_frame/chrome_frame_activex.cc b/chrome_frame/chrome_frame_activex.cc index 286f351..fe3a36a 100644 --- a/chrome_frame/chrome_frame_activex.cc +++ b/chrome_frame/chrome_frame_activex.cc @@ -210,7 +210,7 @@ void ChromeFrameActivex::OnMessageFromChromeFrame(int tab_handle, if (target.compare("*") != 0) { bool drop = true; - if (is_privileged_) { + if (is_privileged()) { // Forward messages if the control is in privileged mode. ScopedComPtr<IDispatch> message_event; if (SUCCEEDED(CreateDomEvent("message", message, origin, @@ -272,7 +272,7 @@ void ChromeFrameActivex::OnAutomationServerLaunchFailed( Base::OnAutomationServerLaunchFailed(reason, server_version); if (reason == AUTOMATION_VERSION_MISMATCH && - ShouldShowVersionMismatchDialog(is_privileged_, m_spClientSite)) { + ShouldShowVersionMismatchDialog(is_privileged(), m_spClientSite)) { THREAD_SAFE_UMA_HISTOGRAM_COUNTS( "ChromeFrame.VersionMismatchDisplayed", 1); DisplayVersionMismatchWarning(m_hWnd, server_version); @@ -427,7 +427,7 @@ HRESULT ChromeFrameActivex::IOleObject_SetClientSite( handlers[i]->clear(); // Drop privileged mode on uninitialization. - is_privileged_ = false; + set_is_privileged(false); } else { ScopedComPtr<IHTMLDocument2> document; GetContainingDocument(document.Receive()); @@ -448,13 +448,13 @@ HRESULT ChromeFrameActivex::IOleObject_SetClientSite( service_hr = service->GetWantsPrivileged(&wants_privileged); if (SUCCEEDED(service_hr) && wants_privileged) - is_privileged_ = true; + set_is_privileged(true); - url_fetcher_->set_privileged_mode(is_privileged_); + url_fetcher_->set_privileged_mode(is_privileged()); } std::wstring profile_name(GetHostProcessName(false)); - if (is_privileged_) { + if (is_privileged()) { base::win::ScopedBstr automated_functions_arg; service_hr = service->GetExtensionApisToAutomate( @@ -491,7 +491,7 @@ HRESULT ChromeFrameActivex::IOleObject_SetClientSite( chrome_extra_arguments.append( ASCIIToWide(switches::kEnableExperimentalExtensionApis)); - url_fetcher_->set_frame_busting(!is_privileged_); + url_fetcher_->set_frame_busting(!is_privileged()); automation_client_->SetUrlFetcher(url_fetcher_.get()); if (!InitializeAutomation(profile_name, chrome_extra_arguments, IsIEInPrivate(), true, GURL(utf8_url), diff --git a/chrome_frame/chrome_frame_activex_base.h b/chrome_frame/chrome_frame_activex_base.h index 7d39a7e5..dfc3da0 100644 --- a/chrome_frame/chrome_frame_activex_base.h +++ b/chrome_frame/chrome_frame_activex_base.h @@ -29,7 +29,6 @@ #include "chrome_frame/chrome_frame_plugin.h" #include "chrome_frame/com_message_event.h" #include "chrome_frame/com_type_info_holder.h" -#include "chrome_frame/navigation_constraints.h" #include "chrome_frame/simple_resource_loader.h" #include "chrome_frame/urlmon_url_request.h" #include "chrome_frame/urlmon_url_request_private.h" @@ -170,8 +169,7 @@ class ATL_NO_VTABLE ChromeFrameActivexBase : // NOLINT public IPropertyNotifySinkCP<T>, public CComCoClass<T, &class_id>, public CComControl<T>, - public ChromeFramePlugin<T>, - public NavigationConstraintsImpl { + public ChromeFramePlugin<T> { protected: typedef std::set<base::win::ScopedComPtr<IDispatch> > EventHandlers; typedef ChromeFrameActivexBase<T, class_id> BasePlugin; @@ -388,7 +386,7 @@ END_MSG_MAP() // The base implementation returns true unless we are in privileged // mode, in which case we always trust our container so we return false. bool is_frame_busting_enabled() const { - return !is_privileged_; + return !is_privileged(); } // Needed to support PostTask. @@ -494,7 +492,7 @@ END_MSG_MAP() // passing mechanism between this CF instance, and the BHO that will // be constructed in the new IE tab. if (parsed_url.SchemeIs("chrome-extension") && - is_privileged_) { + is_privileged()) { const char kScheme[] = "http"; const char kHost[] = "local_host"; @@ -573,20 +571,6 @@ END_MSG_MAP() Fire_onclose(); } - // NavigationConstraints overrides. - virtual bool IsSchemeAllowed(const GURL& url) { - bool allowed = NavigationConstraintsImpl::IsSchemeAllowed(url); - if (allowed) - return true; - - if (is_privileged_ && - (url.SchemeIs(chrome::kDataScheme) || - url.SchemeIs(chrome::kExtensionScheme))) { - return true; - } - return false; - } - // Overridden to take advantage of readystate prop changes and send those // to potential listeners. HRESULT FireOnChanged(DISPID dispid) { @@ -733,7 +717,7 @@ END_MSG_MAP() } STDMETHOD(put_useChromeNetwork)(VARIANT_BOOL use_chrome_network) { - if (!is_privileged_) { + if (!is_privileged()) { DLOG(ERROR) << "Attempt to set useChromeNetwork in non-privileged mode"; return E_ACCESSDENIED; } @@ -830,7 +814,7 @@ END_MSG_MAP() if (NULL == message) return E_INVALIDARG; - if (!is_privileged_) { + if (!is_privileged()) { DLOG(ERROR) << "Attempt to postPrivateMessage in non-privileged mode"; return E_ACCESSDENIED; } @@ -859,7 +843,7 @@ END_MSG_MAP() return E_INVALIDARG; } - if (!is_privileged_) { + if (!is_privileged()) { DLOG(ERROR) << "Attempt to installExtension in non-privileged mode"; return E_ACCESSDENIED; } @@ -879,7 +863,7 @@ END_MSG_MAP() return E_INVALIDARG; } - if (!is_privileged_) { + if (!is_privileged()) { DLOG(ERROR) << "Attempt to loadExtension in non-privileged mode"; return E_ACCESSDENIED; } @@ -894,7 +878,7 @@ END_MSG_MAP() STDMETHOD(getEnabledExtensions)() { DCHECK(automation_client_.get()); - if (!is_privileged_) { + if (!is_privileged()) { DLOG(ERROR) << "Attempt to getEnabledExtensions in non-privileged mode"; return E_ACCESSDENIED; } @@ -907,7 +891,7 @@ END_MSG_MAP() DCHECK(automation_client_.get()); DCHECK(session_id); - if (!is_privileged_) { + if (!is_privileged()) { DLOG(ERROR) << "Attempt to getSessionId in non-privileged mode"; return E_ACCESSDENIED; } @@ -941,7 +925,7 @@ END_MSG_MAP() } else if (LowerCaseEqualsASCII(event_type, event_type_end, "privatemessage")) { // This event handler is only available in privileged mode. - if (is_privileged_) { + if (is_privileged()) { *handlers = &onprivatemessage_; } else { Error("Event type 'privatemessage' is privileged"); @@ -950,7 +934,7 @@ END_MSG_MAP() } else if (LowerCaseEqualsASCII(event_type, event_type_end, "extensionready")) { // This event handler is only available in privileged mode. - if (is_privileged_) { + if (is_privileged()) { *handlers = &onextensionready_; } else { Error("Event type 'extensionready' is privileged"); diff --git a/chrome_frame/chrome_frame_npapi.cc b/chrome_frame/chrome_frame_npapi.cc index 639a40a..c36c1b7 100644 --- a/chrome_frame/chrome_frame_npapi.cc +++ b/chrome_frame/chrome_frame_npapi.cc @@ -230,15 +230,15 @@ bool ChromeFrameNPAPI::Initialize(NPMIMEType mime_type, NPP instance, // Is the privileged mode requested? if (wants_privileged) { - is_privileged_ = IsFireFoxPrivilegedInvocation(instance); - if (!is_privileged_) { + set_is_privileged(IsFireFoxPrivilegedInvocation(instance)); + if (!is_privileged()) { DLOG(WARNING) << "Privileged mode requested in non-privileged context"; } } std::wstring extra_arguments; std::wstring profile_name(GetHostProcessName(false)); - if (is_privileged_) { + if (is_privileged()) { // Process any privileged mode-only arguments we were handed. if (onprivatemessage_arg) onprivatemessage_handler_ = JavascriptToNPObject(onprivatemessage_arg); @@ -260,14 +260,14 @@ bool ChromeFrameNPAPI::Initialize(NPMIMEType mime_type, NPP instance, // Setup Url fetcher. url_fetcher_.set_NPPInstance(instance_); - url_fetcher_.set_frame_busting(!is_privileged_); + url_fetcher_.set_frame_busting(!is_privileged()); automation_client_->SetUrlFetcher(&url_fetcher_); // TODO(joshia): Initialize navigation here and send proxy config as // part of LaunchSettings /* if (!src_.empty()) - automation_client_->InitiateNavigation(src_, is_privileged_); + automation_client_->InitiateNavigation(src_, is_privileged()); std::string proxy_settings; bool has_prefs = pref_service_->Initialize(instance_, @@ -439,7 +439,7 @@ void ChromeFrameNPAPI::OnAcceleratorPressed(int tab_handle, // WM_KEYUP, etc, which will result in messages like WM_CHAR, WM_SYSCHAR, etc // being posted to the message queue. We don't post these messages here to // avoid these messages from getting handled twice. - if (!is_privileged_ && + if (!is_privileged() && accel_message.message != WM_CHAR && accel_message.message != WM_DEADCHAR && accel_message.message != WM_SYSCHAR && @@ -636,7 +636,7 @@ bool ChromeFrameNPAPI::GetProperty(NPIdentifier name, } } else if (name == plugin_property_identifiers_[PLUGIN_PROPERTY_ONPRIVATEMESSAGE]) { - if (!is_privileged_) { + if (!is_privileged()) { DLOG(WARNING) << "Attempt to read onprivatemessage property while not " "privileged"; } else { @@ -669,7 +669,7 @@ bool ChromeFrameNPAPI::GetProperty(NPIdentifier name, BOOLEAN_TO_NPVARIANT(automation_client_->use_chrome_network(), *variant); return true; } else if (name == plugin_property_identifiers_[PLUGIN_PROPERTY_SESSIONID]) { - if (!is_privileged_) { + if (!is_privileged()) { DLOG(WARNING) << "Attempt to read sessionid property while not " "privileged"; } else { @@ -711,7 +711,7 @@ bool ChromeFrameNPAPI::SetProperty(NPIdentifier name, return true; } else if (name == plugin_property_identifiers_[PLUGIN_PROPERTY_ONPRIVATEMESSAGE]) { - if (!is_privileged_) { + if (!is_privileged()) { DLOG(WARNING) << "Attempt to set onprivatemessage while not privileged"; } else { onprivatemessage_handler_.Free(); @@ -823,7 +823,7 @@ void ChromeFrameNPAPI::OnMessageFromChromeFrame(int tab_handle, const std::string& target) { bool private_message = false; if (target.compare("*") != 0) { - if (is_privileged_) { + if (is_privileged()) { private_message = true; } else { if (!HaveSameOrigin(target, document_url_)) { @@ -848,7 +848,7 @@ void ChromeFrameNPAPI::OnMessageFromChromeFrame(int tab_handle, OBJECT_TO_NPVARIANT(event, params[0]); bool invoke = false; if (private_message) { - DCHECK(is_privileged_); + DCHECK(is_privileged()); STRINGN_TO_NPVARIANT(target.c_str(), target.length(), params[1]); invoke = InvokeDefault(onprivatemessage_handler_, arraysize(params), @@ -1218,7 +1218,7 @@ bool ChromeFrameNPAPI::postPrivateMessage(NPObject* npobject, const NPVariant* args, uint32_t arg_count, NPVariant* result) { - if (!is_privileged_) { + if (!is_privileged()) { DLOG(WARNING) << "postPrivateMessage invoked in non-privileged mode"; return false; } @@ -1251,7 +1251,7 @@ bool ChromeFrameNPAPI::installExtension(NPObject* npobject, return false; } - if (!is_privileged_) { + if (!is_privileged()) { DLOG(WARNING) << "installExtension invoked in non-privileged mode"; return false; } @@ -1298,7 +1298,7 @@ bool ChromeFrameNPAPI::loadExtension(NPObject* npobject, return false; } - if (!is_privileged_) { + if (!is_privileged()) { DLOG(WARNING) << "loadExtension invoked in non-privileged mode"; return false; } @@ -1331,7 +1331,7 @@ bool ChromeFrameNPAPI::enableExtensionAutomation(NPObject* npobject, return false; } - if (!is_privileged_) { + if (!is_privileged()) { DLOG(WARNING) << "enableExtensionAutomation invoked in non-privileged mode"; return false; @@ -1379,7 +1379,7 @@ bool ChromeFrameNPAPI::getEnabledExtensions(NPObject* npobject, return false; } - if (!is_privileged_) { + if (!is_privileged()) { DLOG(WARNING) << "getEnabledExtensions invoked in non-privileged mode"; return false; } @@ -1480,7 +1480,7 @@ bool ChromeFrameNPAPI::GetBrowserIncognitoMode() { bool ChromeFrameNPAPI::PreProcessContextMenu(HMENU menu) { // TODO: Remove this overridden method once HandleContextMenuCommand // implements "About Chrome Frame" handling. - if (!is_privileged_) { + if (!is_privileged()) { // Call base class (adds 'About' item). return ChromeFramePlugin::PreProcessContextMenu(menu); } diff --git a/chrome_frame/chrome_frame_npapi.h b/chrome_frame/chrome_frame_npapi.h index 3a9e61e..f05185c 100644 --- a/chrome_frame/chrome_frame_npapi.h +++ b/chrome_frame/chrome_frame_npapi.h @@ -11,7 +11,6 @@ #include "chrome_frame/chrome_frame_automation.h" #include "chrome_frame/chrome_frame_plugin.h" -#include "chrome_frame/navigation_constraints.h" #include "chrome_frame/np_browser_functions.h" #include "chrome_frame/np_event_listener.h" #include "chrome_frame/np_proxy_service.h" @@ -28,8 +27,7 @@ class nsIURI; class ChromeFrameNPAPI : public CWindowImpl<ChromeFrameNPAPI>, public ChromeFramePlugin<ChromeFrameNPAPI>, - public NpEventDelegate, - public NavigationConstraintsImpl { + public NpEventDelegate { public: typedef ChromeFramePlugin<ChromeFrameNPAPI> Base; diff --git a/chrome_frame/chrome_frame_plugin.h b/chrome_frame/chrome_frame_plugin.h index 58a76a6..7acda3c 100644 --- a/chrome_frame/chrome_frame_plugin.h +++ b/chrome_frame/chrome_frame_plugin.h @@ -14,6 +14,7 @@ #include "chrome/common/chrome_paths.h" #include "chrome/common/chrome_paths_internal.h" #include "chrome_frame/simple_resource_loader.h" +#include "chrome_frame/navigation_constraints.h" #include "chrome_frame/utils.h" #include "grit/chromium_strings.h" @@ -22,11 +23,11 @@ // A class to implement common functionality for all types of // plugins: NPAPI. ActiveX and ActiveDoc template <typename T> -class ChromeFramePlugin : public ChromeFrameDelegateImpl { +class ChromeFramePlugin + : public ChromeFrameDelegateImpl, + public NavigationConstraintsImpl { public: - ChromeFramePlugin() - : ignore_setfocus_(false), - is_privileged_(false) { + ChromeFramePlugin() : ignore_setfocus_(false){ } ~ChromeFramePlugin() { Uninitialize(); @@ -67,7 +68,7 @@ END_MSG_MAP() DCHECK(launch_params_ == NULL); // We don't want to do incognito when privileged, since we're // running in browser chrome or some other privileged context. - bool incognito_mode = !is_privileged_ && incognito; + bool incognito_mode = !is_privileged() && incognito; FilePath profile_path; GetProfilePath(profile_name, &profile_path); // The profile name could change based on the browser version. For e.g. for @@ -99,7 +100,7 @@ END_MSG_MAP() virtual void OnAutomationServerReady() { // Issue the extension automation request if we're privileged to // allow this control to handle extension requests from Chrome. - if (is_privileged_ && IsValid()) + if (is_privileged() && IsValid()) automation_client_->SetEnableExtensionAutomation(functions_enabled_); } @@ -261,14 +262,6 @@ END_MSG_MAP() // When the flag is not set, we transfer the focus to chrome. bool ignore_setfocus_; - // The plugin is privileged if it is: - // * Invoked by a window running under the system principal in FireFox. - // * Being hosted by a custom host exposing the SID_ChromeFramePrivileged - // service. - // - // When privileged, additional interfaces are made available to the user. - bool is_privileged_; - // List of functions to enable for automation, or a single entry "*" to // enable all functions for automation. Ignored unless is_privileged_ is // true. Defaults to the empty list, meaning automation will not be @@ -277,4 +270,3 @@ END_MSG_MAP() }; #endif // CHROME_FRAME_CHROME_FRAME_PLUGIN_H_ - diff --git a/chrome_frame/navigation_constraints.cc b/chrome_frame/navigation_constraints.cc index 0845bb1..78bd8df 100644 --- a/chrome_frame/navigation_constraints.cc +++ b/chrome_frame/navigation_constraints.cc @@ -9,6 +9,9 @@ #include "chrome/common/url_constants.h" #include "chrome_frame/utils.h" +NavigationConstraintsImpl::NavigationConstraintsImpl() : is_privileged_(false) { +} + // NavigationConstraintsImpl method definitions. bool NavigationConstraintsImpl::AllowUnsafeUrls() { // No sanity checks if unsafe URLs are allowed @@ -42,6 +45,13 @@ bool NavigationConstraintsImpl::IsSchemeAllowed(const GURL& url) { return true; } } + + if (is_privileged_ && + (url.SchemeIs(chrome::kDataScheme) || + url.SchemeIs(chrome::kExtensionScheme))) { + return true; + } + return false; } @@ -67,3 +77,10 @@ bool NavigationConstraintsImpl::IsZoneAllowed(const GURL& url) { return true; } +bool NavigationConstraintsImpl::is_privileged() const { + return is_privileged_; +} + +void NavigationConstraintsImpl::set_is_privileged(bool is_privileged) { + is_privileged_ = is_privileged; +} diff --git a/chrome_frame/navigation_constraints.h b/chrome_frame/navigation_constraints.h index a66cdeb..fe24558 100644 --- a/chrome_frame/navigation_constraints.h +++ b/chrome_frame/navigation_constraints.h @@ -23,15 +23,27 @@ class NavigationConstraints { // Provides default implementation for the NavigationConstraints interface. class NavigationConstraintsImpl : public NavigationConstraints { public: + NavigationConstraintsImpl(); virtual ~NavigationConstraintsImpl() {} // NavigationConstraints method overrides. virtual bool AllowUnsafeUrls(); virtual bool IsSchemeAllowed(const GURL& url); virtual bool IsZoneAllowed(const GURL& url); + + bool is_privileged() const; + void set_is_privileged(bool is_privileged); + private: base::win::ScopedComPtr<IInternetSecurityManager> security_manager_; + + // The plugin is privileged if it is: + // * Invoked by a window running under the system principal in FireFox. + // * Being hosted by a custom host exposing the SID_ChromeFramePrivileged + // service. + // + // When privileged, additional interfaces are made available to the user. + bool is_privileged_; }; #endif // CHROME_FRAME_NAVIGATION_CONSTRAINTS_H_ - diff --git a/chrome_frame/test/util_unittests.cc b/chrome_frame/test/util_unittests.cc index 68173b9..ac4bccf 100644 --- a/chrome_frame/test/util_unittests.cc +++ b/chrome_frame/test/util_unittests.cc @@ -271,6 +271,7 @@ TEST_F(UtilTests, CanNavigateTest) { { L"about:", URLZONE_TRUSTED }, { L"view-source:", URLZONE_TRUSTED }, { L"chrome-extension:", URLZONE_TRUSTED }, + { L"data:", URLZONE_INTERNET }, { L"ftp:", URLZONE_UNTRUSTED }, { L"file:", URLZONE_LOCAL_MACHINE }, { L"sip:", URLZONE_UNTRUSTED }, @@ -286,31 +287,39 @@ TEST_F(UtilTests, CanNavigateTest) { const char* url; bool default_expected; bool unsafe_expected; + bool is_privileged; } test_cases[] = { // Invalid URL - { " ", false, false }, - { "foo bar", false, false }, + { " ", false, false, false }, + { "foo bar", false, false, false }, // non-privileged test cases { "http://blah/?attach_external_tab&10&1&0&0&100&100&iexplore", true, - true }, - { "http://untrusted/bar.html", false, true }, + true, false }, + { "http://untrusted/bar.html", false, true, false }, { "http://blah/?attach_external_tab&10&1&0&0&100&100&iexplore", true, + true, false }, + { "view-source:http://www.google.ca", true, true, false }, + { "view-source:javascript:alert('foo');", false, true, false }, + { "about:blank", true, true, false }, + { "About:Version", true, true, false }, + { "about:config", false, true, false }, + { "chrome-extension://aaaaaaaaaaaaaaaaaaa/toolstrip.html", false, true, + false }, + { "ftp://www.google.ca", false, true, false }, + { "file://www.google.ca", false, true, false }, + { "file://C:\boot.ini", false, true, false }, + { "SIP:someone@10.1.2.3", false, true, false }, + + // privileged test cases + { "chrome-extension://aaaaaaaaaaaaaaaaaaa/toolstrip.html", true, true, true }, - { "view-source:http://www.google.ca", true, true }, - { "view-source:javascript:alert('foo');", false, true }, - { "about:blank", true, true }, - { "About:Version", true, true }, - { "about:config", false, true }, - { "chrome-extension://aaaaaaaaaaaaaaaaaaa/toolstrip.html", false, true }, - { "ftp://www.google.ca", false, true }, - { "file://www.google.ca", false, true }, - { "file://C:\boot.ini", false, true }, - { "SIP:someone@10.1.2.3", false, true }, + { "data://aaaaaaaaaaaaaaaaaaa/toolstrip.html", true, true, true }, }; for (int i = 0; i < arraysize(test_cases); ++i) { const Cases& test = test_cases[i]; + mock.set_is_privileged(test.is_privileged); bool actual = CanNavigate(GURL(test.url), &mock); EXPECT_EQ(test.default_expected, actual) << "Failure url: " << test.url; } |