diff options
Diffstat (limited to 'toolkit/components/extensions/MatchPattern.cpp')
-rw-r--r-- | toolkit/components/extensions/MatchPattern.cpp | 744 |
1 files changed, 744 insertions, 0 deletions
diff --git a/toolkit/components/extensions/MatchPattern.cpp b/toolkit/components/extensions/MatchPattern.cpp new file mode 100644 index 0000000000..8e46752272 --- /dev/null +++ b/toolkit/components/extensions/MatchPattern.cpp @@ -0,0 +1,744 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#include "mozilla/extensions/MatchPattern.h" +#include "mozilla/extensions/MatchGlob.h" + +#include "js/RegExp.h" // JS::NewUCRegExpObject, JS::ExecuteRegExpNoStatics +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/Unused.h" + +#include "nsGkAtoms.h" +#include "nsIProtocolHandler.h" +#include "nsIURL.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace extensions { + +using namespace mozilla::dom; + +/***************************************************************************** + * AtomSet + *****************************************************************************/ + +AtomSet::AtomSet(const nsTArray<nsString>& aElems) { + mElems.SetCapacity(aElems.Length()); + + for (const auto& elem : aElems) { + mElems.AppendElement(NS_AtomizeMainThread(elem)); + } + + SortAndUniquify(); +} + +AtomSet::AtomSet(const char** aElems) { + for (const char** elemp = aElems; *elemp; elemp++) { + mElems.AppendElement(NS_Atomize(*elemp)); + } + + SortAndUniquify(); +} + +AtomSet::AtomSet(std::initializer_list<nsAtom*> aIL) { + mElems.SetCapacity(aIL.size()); + + for (const auto& elem : aIL) { + mElems.AppendElement(elem); + } + + SortAndUniquify(); +} + +void AtomSet::SortAndUniquify() { + mElems.Sort(); + + nsAtom* prev = nullptr; + mElems.RemoveElementsBy([&prev](const RefPtr<nsAtom>& aAtom) { + bool remove = aAtom == prev; + prev = aAtom; + return remove; + }); + + mElems.Compact(); +} + +bool AtomSet::Intersects(const AtomSet& aOther) const { + for (const auto& atom : *this) { + if (aOther.Contains(atom)) { + return true; + } + } + for (const auto& atom : aOther) { + if (Contains(atom)) { + return true; + } + } + return false; +} + +void AtomSet::Add(nsAtom* aAtom) { + auto index = mElems.IndexOfFirstElementGt(aAtom); + if (index == 0 || mElems[index - 1] != aAtom) { + mElems.InsertElementAt(index, aAtom); + } +} + +void AtomSet::Remove(nsAtom* aAtom) { + auto index = mElems.BinaryIndexOf(aAtom); + if (index != ArrayType::NoIndex) { + mElems.RemoveElementAt(index); + } +} + +/***************************************************************************** + * URLInfo + *****************************************************************************/ + +nsAtom* URLInfo::Scheme() const { + if (!mScheme) { + nsCString scheme; + if (NS_SUCCEEDED(mURI->GetScheme(scheme))) { + mScheme = NS_AtomizeMainThread(NS_ConvertASCIItoUTF16(scheme)); + } + } + return mScheme; +} + +const nsCString& URLInfo::Host() const { + if (mHost.IsVoid()) { + Unused << mURI->GetHost(mHost); + } + return mHost; +} + +const nsAtom* URLInfo::HostAtom() const { + if (!mHostAtom) { + mHostAtom = NS_Atomize(Host()); + } + return mHostAtom; +} + +const nsString& URLInfo::FilePath() const { + if (mFilePath.IsEmpty()) { + nsCString path; + nsCOMPtr<nsIURL> url = do_QueryInterface(mURI); + if (url && NS_SUCCEEDED(url->GetFilePath(path))) { + AppendUTF8toUTF16(path, mFilePath); + } else { + mFilePath = Path(); + } + } + return mFilePath; +} + +const nsString& URLInfo::Path() const { + if (mPath.IsEmpty()) { + nsCString path; + if (NS_SUCCEEDED(URINoRef()->GetPathQueryRef(path))) { + AppendUTF8toUTF16(path, mPath); + } + } + return mPath; +} + +const nsCString& URLInfo::CSpec() const { + if (mCSpec.IsEmpty()) { + Unused << URINoRef()->GetSpec(mCSpec); + } + return mCSpec; +} + +const nsString& URLInfo::Spec() const { + if (mSpec.IsEmpty()) { + AppendUTF8toUTF16(CSpec(), mSpec); + } + return mSpec; +} + +nsIURI* URLInfo::URINoRef() const { + if (!mURINoRef) { + if (NS_FAILED(NS_GetURIWithoutRef(mURI, getter_AddRefs(mURINoRef)))) { + mURINoRef = mURI; + } + } + return mURINoRef; +} + +bool URLInfo::InheritsPrincipal() const { + if (!mInheritsPrincipal.isSome()) { + // For our purposes, about:blank and about:srcdoc are treated as URIs that + // inherit principals. + bool inherits = Spec().EqualsLiteral("about:blank") || + Spec().EqualsLiteral("about:srcdoc"); + + if (!inherits) { + nsresult rv = NS_URIChainHasFlags( + mURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT, &inherits); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + + mInheritsPrincipal.emplace(inherits); + } + return mInheritsPrincipal.ref(); +} + +/***************************************************************************** + * CookieInfo + *****************************************************************************/ + +bool CookieInfo::IsDomain() const { + if (mIsDomain.isNothing()) { + mIsDomain.emplace(false); + MOZ_ALWAYS_SUCCEEDS(mCookie->GetIsDomain(mIsDomain.ptr())); + } + return mIsDomain.ref(); +} + +bool CookieInfo::IsSecure() const { + if (mIsSecure.isNothing()) { + mIsSecure.emplace(false); + MOZ_ALWAYS_SUCCEEDS(mCookie->GetIsSecure(mIsSecure.ptr())); + } + return mIsSecure.ref(); +} + +const nsCString& CookieInfo::Host() const { + if (mHost.IsEmpty()) { + MOZ_ALWAYS_SUCCEEDS(mCookie->GetHost(mHost)); + } + return mHost; +} + +const nsCString& CookieInfo::RawHost() const { + if (mRawHost.IsEmpty()) { + MOZ_ALWAYS_SUCCEEDS(mCookie->GetRawHost(mRawHost)); + } + return mRawHost; +} + +/***************************************************************************** + * MatchPattern + *****************************************************************************/ + +const char* PERMITTED_SCHEMES[] = {"http", "https", "ws", "wss", + "file", "ftp", "data", nullptr}; + +// Known schemes that are followed by "://" instead of ":". +const char* HOST_LOCATOR_SCHEMES[] = { + "http", "https", "ws", "wss", "file", "ftp", "moz-extension", + "chrome", "resource", "moz", "moz-icon", "moz-gio", nullptr}; + +const char* WILDCARD_SCHEMES[] = {"http", "https", "ws", "wss", nullptr}; + +/* static */ +already_AddRefed<MatchPattern> MatchPattern::Constructor( + dom::GlobalObject& aGlobal, const nsAString& aPattern, + const MatchPatternOptions& aOptions, ErrorResult& aRv) { + RefPtr<MatchPattern> pattern = new MatchPattern(aGlobal.GetAsSupports()); + pattern->Init(aGlobal.Context(), aPattern, aOptions.mIgnorePath, + aOptions.mRestrictSchemes, aRv); + if (aRv.Failed()) { + return nullptr; + } + return pattern.forget(); +} + +void MatchPattern::Init(JSContext* aCx, const nsAString& aPattern, + bool aIgnorePath, bool aRestrictSchemes, + ErrorResult& aRv) { + RefPtr<AtomSet> permittedSchemes = AtomSet::Get<PERMITTED_SCHEMES>(); + + mPattern = aPattern; + + if (aPattern.EqualsLiteral("<all_urls>")) { + mSchemes = permittedSchemes; + mMatchSubdomain = true; + return; + } + + // The portion of the URL we're currently examining. + uint32_t offset = 0; + auto tail = Substring(aPattern, offset); + + /*************************************************************************** + * Scheme + ***************************************************************************/ + int32_t index = aPattern.FindChar(':'); + if (index <= 0) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + RefPtr<nsAtom> scheme = NS_AtomizeMainThread(StringHead(aPattern, index)); + bool requireHostLocatorScheme = true; + if (scheme == nsGkAtoms::_asterisk) { + mSchemes = AtomSet::Get<WILDCARD_SCHEMES>(); + } else if (!aRestrictSchemes || permittedSchemes->Contains(scheme) || + scheme == nsGkAtoms::moz_extension) { + mSchemes = new AtomSet({scheme}); + RefPtr<AtomSet> hostLocatorSchemes = AtomSet::Get<HOST_LOCATOR_SCHEMES>(); + requireHostLocatorScheme = hostLocatorSchemes->Contains(scheme); + } else { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + /*************************************************************************** + * Host + ***************************************************************************/ + offset = index + 1; + tail.Rebind(aPattern, offset); + + if (!requireHostLocatorScheme) { + // Unrecognized schemes and some schemes such as about: and data: URIs + // don't have hosts, so just match on the path. + // And so, ignorePath doesn't make sense for these matchers. + aIgnorePath = false; + } else { + if (!StringHead(tail, 2).EqualsLiteral("//")) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + offset += 2; + tail.Rebind(aPattern, offset); + index = tail.FindChar('/'); + if (index < 0) { + index = tail.Length(); + } + + auto host = StringHead(tail, index); + if (host.IsEmpty() && scheme != nsGkAtoms::file) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + offset += index; + tail.Rebind(aPattern, offset); + + if (host.EqualsLiteral("*")) { + mMatchSubdomain = true; + } else if (StringHead(host, 2).EqualsLiteral("*.")) { + CopyUTF16toUTF8(Substring(host, 2), mDomain); + mMatchSubdomain = true; + } else if (host.Length() > 1 && host[0] == '[' && + host[host.Length() - 1] == ']') { + // This is an IPv6 literal, we drop the enclosing `[]` to be + // consistent with nsIURI. + CopyUTF16toUTF8(Substring(host, 1, host.Length() - 2), mDomain); + } else { + CopyUTF16toUTF8(host, mDomain); + } + } + + /*************************************************************************** + * Path + ***************************************************************************/ + if (aIgnorePath) { + mPattern.Truncate(offset); + mPattern.AppendLiteral("/*"); + return; + } + + auto path = tail; + if (path.IsEmpty()) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + mPath = new MatchGlob(this); + mPath->Init(aCx, path, false, aRv); +} + +bool MatchPattern::MatchesDomain(const nsACString& aDomain) const { + if (DomainIsWildcard() || mDomain == aDomain) { + return true; + } + + if (mMatchSubdomain) { + int64_t offset = (int64_t)aDomain.Length() - mDomain.Length(); + if (offset > 0 && aDomain[offset - 1] == '.' && + Substring(aDomain, offset) == mDomain) { + return true; + } + } + + return false; +} + +bool MatchPattern::Matches(const nsAString& aURL, bool aExplicit, + ErrorResult& aRv) const { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + + return Matches(uri.get(), aExplicit); +} + +bool MatchPattern::Matches(const URLInfo& aURL, bool aExplicit) const { + if (aExplicit && mMatchSubdomain) { + return false; + } + + if (!mSchemes->Contains(aURL.Scheme())) { + return false; + } + + if (!MatchesDomain(aURL.Host())) { + return false; + } + + if (mPath && !mPath->IsWildcard() && !mPath->Matches(aURL.Path())) { + return false; + } + + return true; +} + +bool MatchPattern::MatchesCookie(const CookieInfo& aCookie) const { + if (!mSchemes->Contains(nsGkAtoms::https) && + (aCookie.IsSecure() || !mSchemes->Contains(nsGkAtoms::http))) { + return false; + } + + if (MatchesDomain(aCookie.RawHost())) { + return true; + } + + if (!aCookie.IsDomain()) { + return false; + } + + // Things get tricker for domain cookies. The extension needs to be able + // to read any cookies that could be read by any host it has permissions + // for. This means that our normal host matching checks won't work, + // since the pattern "*://*.foo.example.com/" doesn't match ".example.com", + // but it does match "bar.foo.example.com", which can read cookies + // with the domain ".example.com". + // + // So, instead, we need to manually check our filters, and accept any + // with hosts that end with our cookie's host. + + auto& host = aCookie.Host(); + return StringTail(mDomain, host.Length()) == host; +} + +bool MatchPattern::SubsumesDomain(const MatchPattern& aPattern) const { + if (!mMatchSubdomain && aPattern.mMatchSubdomain && + aPattern.mDomain == mDomain) { + return false; + } + + return MatchesDomain(aPattern.mDomain); +} + +bool MatchPattern::Subsumes(const MatchPattern& aPattern) const { + for (auto& scheme : *aPattern.mSchemes) { + if (!mSchemes->Contains(scheme)) { + return false; + } + } + + return SubsumesDomain(aPattern); +} + +bool MatchPattern::Overlaps(const MatchPattern& aPattern) const { + if (!mSchemes->Intersects(*aPattern.mSchemes)) { + return false; + } + + return SubsumesDomain(aPattern) || aPattern.SubsumesDomain(*this); +} + +JSObject* MatchPattern::WrapObject(JSContext* aCx, + JS::HandleObject aGivenProto) { + return MatchPattern_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +bool MatchPattern::MatchesAllURLs(const URLInfo& aURL) { + RefPtr<AtomSet> permittedSchemes = AtomSet::Get<PERMITTED_SCHEMES>(); + return permittedSchemes->Contains(aURL.Scheme()); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MatchPattern, mPath, mParent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MatchPattern) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MatchPattern) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MatchPattern) + +/***************************************************************************** + * MatchPatternSet + *****************************************************************************/ + +/* static */ +already_AddRefed<MatchPatternSet> MatchPatternSet::Constructor( + dom::GlobalObject& aGlobal, + const nsTArray<dom::OwningStringOrMatchPattern>& aPatterns, + const MatchPatternOptions& aOptions, ErrorResult& aRv) { + ArrayType patterns; + + for (auto& elem : aPatterns) { + if (elem.IsMatchPattern()) { + patterns.AppendElement(elem.GetAsMatchPattern()); + } else { + RefPtr<MatchPattern> pattern = + MatchPattern::Constructor(aGlobal, elem.GetAsString(), aOptions, aRv); + + if (!pattern) { + return nullptr; + } + patterns.AppendElement(std::move(pattern)); + } + } + + RefPtr<MatchPatternSet> patternSet = + new MatchPatternSet(aGlobal.GetAsSupports(), std::move(patterns)); + return patternSet.forget(); +} + +bool MatchPatternSet::Matches(const nsAString& aURL, bool aExplicit, + ErrorResult& aRv) const { + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + + return Matches(uri.get(), aExplicit); +} + +bool MatchPatternSet::Matches(const URLInfo& aURL, bool aExplicit) const { + for (const auto& pattern : mPatterns) { + if (pattern->Matches(aURL, aExplicit)) { + return true; + } + } + return false; +} + +bool MatchPatternSet::MatchesCookie(const CookieInfo& aCookie) const { + for (const auto& pattern : mPatterns) { + if (pattern->MatchesCookie(aCookie)) { + return true; + } + } + return false; +} + +bool MatchPatternSet::Subsumes(const MatchPattern& aPattern) const { + for (const auto& pattern : mPatterns) { + if (pattern->Subsumes(aPattern)) { + return true; + } + } + return false; +} + +bool MatchPatternSet::SubsumesDomain(const MatchPattern& aPattern) const { + for (const auto& pattern : mPatterns) { + if (pattern->SubsumesDomain(aPattern)) { + return true; + } + } + return false; +} + +bool MatchPatternSet::Overlaps(const MatchPatternSet& aPatternSet) const { + for (const auto& pattern : aPatternSet.mPatterns) { + if (Overlaps(*pattern)) { + return true; + } + } + return false; +} + +bool MatchPatternSet::Overlaps(const MatchPattern& aPattern) const { + for (const auto& pattern : mPatterns) { + if (pattern->Overlaps(aPattern)) { + return true; + } + } + return false; +} + +bool MatchPatternSet::OverlapsAll(const MatchPatternSet& aPatternSet) const { + for (const auto& pattern : aPatternSet.mPatterns) { + if (!Overlaps(*pattern)) { + return false; + } + } + return aPatternSet.mPatterns.Length() > 0; +} + +JSObject* MatchPatternSet::WrapObject(JSContext* aCx, + JS::HandleObject aGivenProto) { + return MatchPatternSet_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MatchPatternSet, mPatterns, mParent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MatchPatternSet) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MatchPatternSet) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MatchPatternSet) + +/***************************************************************************** + * MatchGlob + *****************************************************************************/ + +MatchGlob::~MatchGlob() { mozilla::DropJSObjects(this); } + +/* static */ +already_AddRefed<MatchGlob> MatchGlob::Constructor(dom::GlobalObject& aGlobal, + const nsAString& aGlob, + bool aAllowQuestion, + ErrorResult& aRv) { + RefPtr<MatchGlob> glob = new MatchGlob(aGlobal.GetAsSupports()); + glob->Init(aGlobal.Context(), aGlob, aAllowQuestion, aRv); + if (aRv.Failed()) { + return nullptr; + } + return glob.forget(); +} + +void MatchGlob::Init(JSContext* aCx, const nsAString& aGlob, + bool aAllowQuestion, ErrorResult& aRv) { + mGlob = aGlob; + + // Check for a literal match with no glob metacharacters. + auto index = mGlob.FindCharInSet(aAllowQuestion ? "*?" : "*"); + if (index < 0) { + mPathLiteral = mGlob; + return; + } + + // Check for a prefix match, where the only glob metacharacter is a "*" + // at the end of the string. + if (index == (int32_t)mGlob.Length() - 1 && mGlob[index] == '*') { + mPathLiteral = StringHead(mGlob, index); + mIsPrefix = true; + return; + } + + // Fall back to the regexp slow path. + constexpr auto metaChars = ".+*?^${}()|[]\\"_ns; + + nsAutoString escaped; + escaped.Append('^'); + + for (uint32_t i = 0; i < mGlob.Length(); i++) { + auto c = mGlob[i]; + if (c == '*') { + escaped.AppendLiteral(".*"); + } else if (c == '?' && aAllowQuestion) { + escaped.Append('.'); + } else { + if (metaChars.Contains(c)) { + escaped.Append('\\'); + } + escaped.Append(c); + } + } + + escaped.Append('$'); + + // TODO: Switch to the Rust regexp crate, when Rust integration is easier. + // It uses a much more efficient, linear time matching algorithm, and + // doesn't require special casing for the literal and prefix cases. + mRegExp = JS::NewUCRegExpObject(aCx, escaped.get(), escaped.Length(), 0); + if (mRegExp) { + mozilla::HoldJSObjects(this); + } else { + aRv.NoteJSContextException(aCx); + } +} + +bool MatchGlob::Matches(const nsAString& aString) const { + if (mRegExp) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + JSAutoRealm ar(cx, mRegExp); + + JS::RootedObject regexp(cx, mRegExp); + JS::RootedValue result(cx); + + nsString input(aString); + + size_t index = 0; + if (!JS::ExecuteRegExpNoStatics(cx, regexp, input.BeginWriting(), + aString.Length(), &index, true, &result)) { + return false; + } + + return result.isBoolean() && result.toBoolean(); + } + + if (mIsPrefix) { + return mPathLiteral == StringHead(aString, mPathLiteral.Length()); + } + + return mPathLiteral == aString; +} + +JSObject* MatchGlob::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) { + return MatchGlob_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(MatchGlob) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MatchGlob) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) + tmp->mRegExp = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MatchGlob) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(MatchGlob) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mRegExp) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MatchGlob) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MatchGlob) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MatchGlob) + +/***************************************************************************** + * MatchGlobSet + *****************************************************************************/ + +bool MatchGlobSet::Matches(const nsAString& aValue) const { + for (auto& glob : *this) { + if (glob->Matches(aValue)) { + return true; + } + } + return false; +} + +} // namespace extensions +} // namespace mozilla |