tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 1f5d422cf8f67781d01f155a991a31a5fc6a0385
parent eb2770739d1ff17f0d82fc8271c0e7a938045a5e
Author: Benjamin VanderSloot <bvandersloot@mozilla.com>
Date:   Fri, 17 Oct 2025 12:43:30 +0000

Bug 1991135, part 1 - Add support for <meta name=rating> behind a pref - r=dom-core,firefox-desktop-core-reviewers ,fluent-reviewers,bolsson,smaug,frontend-codestyle-reviewers,Gijs

Differential Revision: https://phabricator.services.mozilla.com/D266422

Diffstat:
Mdocshell/base/BrowsingContext.cpp | 12++++++++++++
Mdocshell/base/BrowsingContext.h | 9++++++++-
Mdocshell/base/CanonicalBrowsingContext.cpp | 20++++++++++++++++++++
Mdocshell/base/CanonicalBrowsingContext.h | 2++
Mdocshell/base/nsAboutRedirector.cpp | 4++++
Mdocshell/base/nsDocShell.cpp | 17+++++++++++++++++
Mdocshell/base/nsDocShell.h | 5+++++
Mdocshell/build/components.conf | 1+
Mdom/base/Document.cpp | 29+++++++++++++++++++++++++++++
Mdom/base/Document.h | 3+++
Meslint-file-globals.config.mjs | 1+
Mmodules/libpref/init/StaticPrefList.yaml | 11+++++++++++
Atoolkit/actors/AboutRestrictedChild.sys.mjs | 13+++++++++++++
Atoolkit/actors/AboutRestrictedParent.sys.mjs | 19+++++++++++++++++++
Mtoolkit/actors/moz.build | 2++
Atoolkit/content/aboutRestricted/aboutRestricted.css | 26++++++++++++++++++++++++++
Atoolkit/content/aboutRestricted/aboutRestricted.html | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Atoolkit/content/aboutRestricted/aboutRestricted.mjs | 39+++++++++++++++++++++++++++++++++++++++
Mtoolkit/content/jar.mn | 3+++
Atoolkit/locales-preview/aboutRestricted.ftl | 20++++++++++++++++++++
Mtoolkit/locales/jar.mn | 1+
Mtoolkit/modules/ActorManagerParent.sys.mjs | 14++++++++++++++
Mtoolkit/modules/RemotePageAccessManager.sys.mjs | 4++++
Mxpcom/base/ErrorList.py | 1+
Mxpcom/ds/StaticAtoms.py | 5+++++
25 files changed, 309 insertions(+), 1 deletion(-)

diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp @@ -489,6 +489,18 @@ already_AddRefed<BrowsingContext> BrowsingContext::CreateDetached( ? inherit->GetIPAddressSpace() : nsILoadInfo::IPAddressSpace::Unknown; + bool parentalControlsEnabled; + if (inherit) { + parentalControlsEnabled = inherit->GetParentalControlsEnabled(); + } else if (XRE_IsParentProcess()) { + parentalControlsEnabled = + CanonicalBrowsingContext::ShouldEnforceParentalControls(); + } else { + parentalControlsEnabled = false; + } + + fields.Get<IDX_ParentalControlsEnabled>() = parentalControlsEnabled; + fields.Get<IDX_IsPopupRequested>() = aOptions.isPopupRequested; fields.Get<IDX_TopLevelCreatedByWebContent>() = diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h @@ -284,7 +284,10 @@ struct EmbedderColorSchemes { * protections */ \ FIELD(TopInnerSizeForRFP, CSSIntSize) \ /* Used to propagate document's IPAddressSpace */ \ - FIELD(IPAddressSpace, nsILoadInfo::IPAddressSpace) + FIELD(IPAddressSpace, nsILoadInfo::IPAddressSpace) \ + /* This is true if we should redirect to an error page when inserting * \ + * meta tags flagging adult content into our documents */ \ + FIELD(ParentalControlsEnabled, bool) // BrowsingContext, in this context, is the cross process replicated // environment in which information about documents is stored. In @@ -1420,6 +1423,10 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { return XRE_IsParentProcess(); } + bool CanSet(FieldIndex<IDX_ParentalControlsEnabled>, bool, ContentParent*) { + return XRE_IsParentProcess(); + } + // Overload `DidSet` to get notifications for a particular field being set. // // You can also overload the variant that gets the old value if you need it. diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp @@ -41,8 +41,10 @@ #include "mozilla/StaticPrefs_browser.h" #include "mozilla/StaticPrefs_docshell.h" #include "mozilla/StaticPrefs_fission.h" +#include "mozilla/StaticPrefs_security.h" #include "mozilla/glean/DomMetrics.h" #include "nsILayoutHistoryState.h" +#include "nsIParentalControlsService.h" #include "nsIPrintSettings.h" #include "nsIPrintSettingsService.h" #include "nsISupports.h" @@ -271,6 +273,7 @@ void CanonicalBrowsingContext::ReplacedBy( txn.SetForceOffline(GetForceOffline()); txn.SetTopInnerSizeForRFP(GetTopInnerSizeForRFP()); txn.SetIPAddressSpace(GetIPAddressSpace()); + txn.SetParentalControlsEnabled(GetParentalControlsEnabled()); if (!GetLanguageOverride().IsEmpty()) { // Reapply language override to update the corresponding realm. @@ -3693,6 +3696,23 @@ bool CanonicalBrowsingContext::CanOpenModalPicker() { return true; } +bool CanonicalBrowsingContext::ShouldEnforceParentalControls() { + if (StaticPrefs::security_restrict_to_adults_always()) { + return true; + } + if (StaticPrefs::security_restrict_to_adults_respect_platform()) { + bool enabled; + nsCOMPtr<nsIParentalControlsService> pcs = + do_CreateInstance("@mozilla.org/parental-controls-service;1"); + nsresult rv = pcs->GetParentalControlsEnabled(&enabled); + if (NS_FAILED(rv)) { + return false; + } + return enabled; + } + return false; +} + NS_IMPL_CYCLE_COLLECTION_CLASS(CanonicalBrowsingContext) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(CanonicalBrowsingContext, diff --git a/docshell/base/CanonicalBrowsingContext.h b/docshell/base/CanonicalBrowsingContext.h @@ -453,6 +453,8 @@ class CanonicalBrowsingContext final : public BrowsingContext { bool CanOpenModalPicker(); + static bool ShouldEnforceParentalControls(); + protected: // Called when the browsing context is being discarded. void CanonicalDiscard(); diff --git a/docshell/base/nsAboutRedirector.cpp b/docshell/base/nsAboutRedirector.cpp @@ -175,6 +175,10 @@ static const RedirEntry kRedirMap[] = { nsIAboutModule::HIDE_FROM_ABOUTABOUT}, {"processes", "chrome://global/content/aboutProcesses.html", nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}, + {"restricted", "chrome://global/content/aboutRestricted/aboutRestricted.html", + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT | + nsIAboutModule::HIDE_FROM_ABOUTABOUT}, // about:serviceworkers always wants to load in the parent process because // the only place nsIServiceWorkerManager has any data is in the parent // process. diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp @@ -3566,6 +3566,12 @@ nsDocShell::DisplayLoadError(nsresult aError, nsIURI* aURI, if (messageStr.IsEmpty()) { messageStr.AssignLiteral(u" "); } + } else if (aError == NS_ERROR_RESTRICTED_CONTENT) { + errorPage.AssignLiteral("restricted"); + error = "restrictedcontent"; + if (messageStr.IsEmpty()) { + messageStr.AssignLiteral(u" "); + } } else { // Errors requiring simple formatting switch (aError) { @@ -4117,6 +4123,17 @@ nsresult nsDocShell::ReloadNavigable( return ReloadDocument(this, doc, loadType, bc, currentURI, referrerInfo); } +void nsDocShell::DisplayRestrictedContentError() { + bool didDisplayLoadError = false; + RefPtr<mozilla::dom::Document> doc = GetDocument(); + if (!doc) { + return; + } + doc->TerminateParserAndDisableScripts(); + DisplayLoadError(NS_ERROR_RESTRICTED_CONTENT, doc->GetDocumentURI(), nullptr, nullptr, + &didDisplayLoadError); +} + /* static */ nsresult nsDocShell::ReloadDocument(nsDocShell* aDocShell, Document* aDocument, uint32_t aLoadType, diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h @@ -782,6 +782,11 @@ class nsDocShell final : public nsDocLoader, return didDisplayLoadError; } + // Called when a document is recognised as content the device owner doesn't + // want to be displayed. Stops parsing, stops scripts, and displays an + // error page with DisplayLoadError. + void DisplayRestrictedContentError(); + // // Uncategorized // diff --git a/docshell/build/components.conf b/docshell/build/components.conf @@ -26,6 +26,7 @@ about_pages = [ 'networking', 'performance', 'processes', + 'restricted', 'serviceworkers', 'srcdoc', 'support', diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp @@ -384,6 +384,7 @@ #include "nsINodeList.h" #include "nsIObjectLoadingContent.h" #include "nsIObserverService.h" +#include "nsIParentalControlsService.h" #include "nsIPermission.h" #include "nsIPrompt.h" #include "nsIPropertyBag2.h" @@ -11857,6 +11858,34 @@ void Document::ProcessMETATag(HTMLMetaElement* aMetaElement) { SetHeaderData(nsGkAtoms::handheldFriendly, result); } } + // Check for Restricted To Adults meta tag + if (aMetaElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, + nsGkAtoms::rating, eIgnoreCase)) { + if (aMetaElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::content, + nsGkAtoms::adult, eIgnoreCase) || + aMetaElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::content, + nsGkAtoms::restrictToAdults, eIgnoreCase)) { + BrowsingContext* bc = GetBrowsingContext(); + if (bc && bc->GetParentalControlsEnabled() && GetDocShell()) { + RefPtr<nsDocShell> docShell = nsDocShell::Cast(GetDocShell()); + nsCOMPtr<nsIRunnable> redirect = NewRunnableMethod( + "Document::ProcessMETATag::DisplayRestrictedContentError", docShell, + &nsDocShell::DisplayRestrictedContentError); + nsContentUtils::AddScriptRunner(redirect.forget()); + } + } + } +} + +void Document::TerminateParserAndDisableScripts() { + if (mParser) { + Unused << mParser->Terminate(); + MOZ_ASSERT(!mParser, "mParser should have been null'd out"); + } + + if (WindowContext* wc = GetWindowContext()) { + Unused << wc->SetAllowJavascript(false); + } } already_AddRefed<Element> Document::CreateElem(const nsAString& aName, diff --git a/dom/base/Document.h b/dom/base/Document.h @@ -2241,6 +2241,9 @@ class Document : public nsINode, } void ProcessMETATag(HTMLMetaElement* aMetaElement); + + void TerminateParserAndDisableScripts(); + /** * Create an element with the specified name, prefix and namespace ID. * Returns null if element name parsing failed. diff --git a/eslint-file-globals.config.mjs b/eslint-file-globals.config.mjs @@ -382,6 +382,7 @@ export default [ "toolkit/components/certviewer/content/components/about-certificate-items.mjs", "toolkit/components/certviewer/content/components/about-certificate-section.mjs", "toolkit/components/httpsonlyerror/content/errorpage.js", + "toolkit/content/aboutRestricted/aboutRestricted.mjs", "toolkit/content/aboutNetError.mjs", "toolkit/content/aboutNetErrorHelpers.mjs", "toolkit/content/net-error-card.mjs", diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -17574,6 +17574,17 @@ value: 1 mirror: always +- name: security.restrict_to_adults.respect_platform + type: RelaxedAtomicBool + value: false + mirror: always + +- name: security.restrict_to_adults.always + type: bool + value: false + mirror: always + + #--------------------------------------------------------------------------- # Prefs starting with "signon." #--------------------------------------------------------------------------- diff --git a/toolkit/actors/AboutRestrictedChild.sys.mjs b/toolkit/actors/AboutRestrictedChild.sys.mjs @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +export class AboutRestrictedChild extends RemotePageChild { + actorCreated() { + super.actorCreated(); + const exportableFunctions = ["RPMGetBoolPref"]; + this.exportFunctions(exportableFunctions); + } +} diff --git a/toolkit/actors/AboutRestrictedParent.sys.mjs b/toolkit/actors/AboutRestrictedParent.sys.mjs @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { EscapablePageParent } from "resource://gre/actors/NetErrorParent.sys.mjs"; + +export class AboutRestrictedParent extends EscapablePageParent { + get browser() { + return this.browsingContext.top.embedderElement; + } + + receiveMessage(aMessage) { + switch (aMessage.name) { + case "goBack": + this.leaveErrorPage(this.browser); + break; + } + } +} diff --git a/toolkit/actors/moz.build b/toolkit/actors/moz.build @@ -31,6 +31,8 @@ TESTING_JS_MODULES += [ FINAL_TARGET_FILES.actors += [ "AboutHttpsOnlyErrorChild.sys.mjs", "AboutHttpsOnlyErrorParent.sys.mjs", + "AboutRestrictedChild.sys.mjs", + "AboutRestrictedParent.sys.mjs", "AudioPlaybackChild.sys.mjs", "AudioPlaybackParent.sys.mjs", "AutoCompleteChild.sys.mjs", diff --git a/toolkit/content/aboutRestricted/aboutRestricted.css b/toolkit/content/aboutRestricted/aboutRestricted.css @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@import url("chrome://global/skin/error-pages.css"); + +body { + --warning-color: #ffa436; +} + +@media (prefers-color-scheme: dark) { + body { + --warning-color: #ffbd4f; + } +} + +@media (prefers-contrast) { + body { + --warning-color: var(--text-color); + } +} + +.title { + background-image: url("chrome://global/skin/icons/warning.svg"); + fill: var(--warning-color); +} diff --git a/toolkit/content/aboutRestricted/aboutRestricted.html b/toolkit/content/aboutRestricted/aboutRestricted.html @@ -0,0 +1,49 @@ +<!doctype html> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html data-l10n-sync="true"> + <head> + <meta + http-equiv="Content-Security-Policy" + content="default-src chrome:; object-src 'none'" + /> + <meta name="color-scheme" content="light dark" /> + <title data-l10n-id="restricted-page-title"></title> + <link + rel="stylesheet" + href="chrome://global/content/aboutRestricted/aboutRestricted.css" + type="text/css" + media="all" + /> + <link + rel="icon" + id="favicon" + href="chrome://global/skin/icons/warning.svg" + /> + <link rel="localization" href="branding/brand.ftl" /> + <link rel="localization" href="locales-preview/aboutRestricted.ftl" /> + </head> + <body> + <div class="container"> + <div id="text-container"> + <div class="title"> + <h1 class="title-text" data-l10n-id="restricted-page-heading"></h1> + </div> + <p id="errorShortDesc" data-l10n-id="restricted-page-explain-what"></p> + <p id="errorShortDesc2" data-l10n-id="restricted-page-explain-why"></p> + </div> + <div class="button-container"> + <button + id="goBack" + class="primary" + data-l10n-id="restricted-go-back-button" + ></button> + </div> + </div> + <script + type="module" + src="chrome://global/content/aboutRestricted/aboutRestricted.mjs" + ></script> + </body> +</html> diff --git a/toolkit/content/aboutRestricted/aboutRestricted.mjs b/toolkit/content/aboutRestricted/aboutRestricted.mjs @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function init() { + document + .getElementById("goBack") + .addEventListener("click", onReturnButtonClick); + + const voluntary = RPMGetBoolPref("security.restrict_to_adults.always", false); + if (voluntary) { + document + .getElementById("errorShortDesc2") + .setAttribute("data-l10n-id", "restricted-page-explain-why-always"); + } + try { + const outerURL = URL.parse(document.documentURI); + const innerURL = outerURL.searchParams.get("u"); + const url = URL.parse(innerURL); + const host = url.host; + if (host && (url.protocol == "http:" || url.protocol == "https:")) { + let description = document.getElementById("errorShortDesc"); + document.l10n.setAttributes( + description, + "restricted-page-explain-what-named", + { host } + ); + } + } catch (_) {} + document.dispatchEvent( + new CustomEvent("AboutRestrictedLoad", { bubbles: true }) + ); +} + +function onReturnButtonClick() { + RPMSendAsyncMessage("goBack"); +} + +init(); diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn @@ -32,6 +32,9 @@ toolkit.jar: content/global/aboutwebrtc/disclosure.mjs (aboutwebrtc/disclosure.mjs) content/global/aboutwebrtc/copyButton.mjs (aboutwebrtc/copyButton.mjs) content/global/aboutwebrtc/aboutWebrtc.html (aboutwebrtc/aboutWebrtc.html) + content/global/aboutRestricted/aboutRestricted.mjs (aboutRestricted/aboutRestricted.mjs) + content/global/aboutRestricted/aboutRestricted.html (aboutRestricted/aboutRestricted.html) + content/global/aboutRestricted/aboutRestricted.css (aboutRestricted/aboutRestricted.css) content/global/aboutSupport.js * content/global/aboutSupport.xhtml #ifndef MOZ_GLEAN_ANDROID diff --git a/toolkit/locales-preview/aboutRestricted.ftl b/toolkit/locales-preview/aboutRestricted.ftl @@ -0,0 +1,20 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Title for error page +restricted-page-title = Restricted Content +# Heading in content for the error page +restricted-page-heading = Error: Restricted Content +# An explanation of why the user is landing on a error page instead of viewing adult content +restricted-page-explain-what = The web page you tried to visit marked itself as adult content and { -brand-short-name } is configured not to show you that content. +# An explanation of why the user is landing on a error page instead of viewing adult content +# Variables: +# $host (String): The website that would be displaying adult content +restricted-page-explain-what-named = { -brand-short-name } is configured not to show you adult content and <strong>{ $host }</strong> marked the page you tried to visit as adult content. +# An explanation of what configuration causes this error page to show up +restricted-page-explain-why = We blocked this site because Parental Controls are enabled on your computer. You can disable them if you have administrator access. +# An explanation of what configuration causes this error page to show up +restricted-page-explain-why-always = We blocked this site because Parental Controls are enabled on { -brand-short-name }. +# Button label to go back to the previous page +restricted-go-back-button = Go Back diff --git a/toolkit/locales/jar.mn b/toolkit/locales/jar.mn @@ -8,6 +8,7 @@ services (%services/**/*.ftl) toolkit (%toolkit/**/*.ftl) locales-preview/aboutTranslations.ftl (../locales-preview/aboutTranslations.ftl) + locales-preview/aboutRestricted.ftl (../locales-preview/aboutRestricted.ftl) #ifdef MOZ_LAYOUT_DEBUGGER layoutdebug/layoutdebug.ftl (../../layout/tools/layout-debug/ui/content/layoutdebug.ftl) #endif diff --git a/toolkit/modules/ActorManagerParent.sys.mjs b/toolkit/modules/ActorManagerParent.sys.mjs @@ -119,6 +119,20 @@ let JSWINDOWACTORS = { allFrames: true, }, + AboutRestricted: { + parent: { + esModuleURI: "resource://gre/actors/AboutRestrictedParent.sys.mjs", + }, + child: { + esModuleURI: "resource://gre/actors/AboutRestrictedChild.sys.mjs", + events: { + DOMDocElementInserted: {}, + }, + }, + matches: ["about:restricted?*"], + allFrames: true, + }, + AudioPlayback: { parent: { esModuleURI: "resource://gre/actors/AudioPlaybackParent.sys.mjs", diff --git a/toolkit/modules/RemotePageAccessManager.sys.mjs b/toolkit/modules/RemotePageAccessManager.sys.mjs @@ -236,6 +236,10 @@ export let RemotePageAccessManager = { ], RPMRecordGleanEvent: ["securityUiProtections"], }, + "about:restricted": { + RPMSendAsyncMessage: ["goBack"], + RPMGetBoolPref: ["security.restrict_to_adults.always"], + }, "about:tabcrashed": { RPMSendAsyncMessage: ["Load", "closeTab", "restoreTab", "restoreAll"], RPMAddMessageListener: ["*"], diff --git a/xpcom/base/ErrorList.py b/xpcom/base/ErrorList.py @@ -955,6 +955,7 @@ with modules["URILOADER"]: errors["NS_ERROR_CRYPTOMINING_URI"] = FAILURE(42) errors["NS_ERROR_SOCIALTRACKING_URI"] = FAILURE(43) errors["NS_ERROR_EMAILTRACKING_URI"] = FAILURE(44) + errors["NS_ERROR_RESTRICTED_CONTENT"] = FAILURE(45) # Used when "Save Link As..." doesn't see the headers quickly enough to # choose a filename. See nsContextMenu.js. errors["NS_ERROR_SAVE_LINK_AS_TIMEOUT"] = FAILURE(32) diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py @@ -77,6 +77,7 @@ STATIC_ATOMS = [ Atom("actuate", "actuate"), Atom("address", "address"), Atom("adoptedsheetclones", "adoptedsheetclones"), + Atom("adult", "adult"), Atom("after", "after"), Atom("align", "align"), Atom("alink", "alink"), @@ -1071,6 +1072,7 @@ STATIC_ATOMS = [ Atom("radioLabel", "radio-label"), Atom("radiogroup", "radiogroup"), Atom("range", "range"), + Atom("rating", "rating"), Atom("readonly", "readonly"), Atom("rect", "rect"), Atom("rectangle", "rectangle"), @@ -1093,6 +1095,9 @@ STATIC_ATOMS = [ Atom("resizer", "resizer"), Atom("resolution", "resolution"), Atom("resources", "resources"), + # legacy string from an unknown ontology, but used by several sites and + # respected by Google Search, for <meta name="rating"> content attributes + Atom("restrictToAdults", "RTA-5042-1996-1400-1577-RTA"), Atom("result", "result"), Atom("resultPrefix", "result-prefix"), Atom("retargetdocumentfocus", "retargetdocumentfocus"),