gecko-dev/toolkit/components/extensions/MatchPattern.cpp

745 строки
20 KiB
C++

/* -*- 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("*.")) {
mDomain = NS_ConvertUTF16toUTF8(Substring(host, 2));
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.
mDomain = NS_ConvertUTF16toUTF8(Substring(host, 1, host.Length() - 2));
} else {
mDomain = NS_ConvertUTF16toUTF8(host);
}
}
/***************************************************************************
* 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.
NS_NAMED_LITERAL_CSTRING(metaChars, ".+*?^${}()|[]\\");
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