345 lines
16 KiB
C++
345 lines
16 KiB
C++
/*
|
|
* Copyright (C) 2014-2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "config.h"
|
|
#include "ContentExtensionsBackend.h"
|
|
|
|
#if ENABLE(CONTENT_EXTENSIONS)
|
|
|
|
#include "Chrome.h"
|
|
#include "ChromeClient.h"
|
|
#include "CompiledContentExtension.h"
|
|
#include "ContentExtension.h"
|
|
#include "ContentExtensionsDebugging.h"
|
|
#include "ContentRuleListResults.h"
|
|
#include "DFABytecodeInterpreter.h"
|
|
#include "Document.h"
|
|
#include "DocumentLoader.h"
|
|
#include "ExtensionStyleSheets.h"
|
|
#include "Frame.h"
|
|
#include "FrameLoaderClient.h"
|
|
#include "Page.h"
|
|
#include "ResourceLoadInfo.h"
|
|
#include "ScriptController.h"
|
|
#include "ScriptSourceCode.h"
|
|
#include "Settings.h"
|
|
#include <wtf/URL.h>
|
|
#include "UserContentController.h"
|
|
#include <wtf/NeverDestroyed.h>
|
|
#include <wtf/text/CString.h>
|
|
|
|
namespace WebCore {
|
|
|
|
namespace ContentExtensions {
|
|
|
|
#if USE(APPLE_INTERNAL_SDK)
|
|
#import <WebKitAdditions/ContentRuleListAdditions.mm>
|
|
#else
|
|
static void makeSecureIfNecessary(ContentRuleListResults& results, const URL& url, const URL& redirectFrom = { })
|
|
{
|
|
if (redirectFrom.host() == url.host() && redirectFrom.protocolIs("https"))
|
|
return;
|
|
|
|
if (!url.protocolIs("http"))
|
|
return;
|
|
if (url.host() == "www.opengl.org"
|
|
|| url.host() == "webkit.org"
|
|
|| url.host() == "download")
|
|
results.summary.madeHTTPS = true;
|
|
}
|
|
#endif
|
|
|
|
bool ContentExtensionsBackend::shouldBeMadeSecure(const URL& url)
|
|
{
|
|
ContentRuleListResults results;
|
|
makeSecureIfNecessary(results, url);
|
|
return results.summary.madeHTTPS;
|
|
}
|
|
|
|
void ContentExtensionsBackend::addContentExtension(const String& identifier, Ref<CompiledContentExtension> compiledContentExtension, ContentExtension::ShouldCompileCSS shouldCompileCSS)
|
|
{
|
|
ASSERT(!identifier.isEmpty());
|
|
if (identifier.isEmpty())
|
|
return;
|
|
|
|
auto contentExtension = ContentExtension::create(identifier, WTFMove(compiledContentExtension), shouldCompileCSS);
|
|
m_contentExtensions.set(identifier, WTFMove(contentExtension));
|
|
}
|
|
|
|
void ContentExtensionsBackend::removeContentExtension(const String& identifier)
|
|
{
|
|
m_contentExtensions.remove(identifier);
|
|
}
|
|
|
|
void ContentExtensionsBackend::removeAllContentExtensions()
|
|
{
|
|
m_contentExtensions.clear();
|
|
}
|
|
|
|
auto ContentExtensionsBackend::actionsForResourceLoad(const ResourceLoadInfo& resourceLoadInfo) const -> Vector<ActionsFromContentRuleList>
|
|
{
|
|
#if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
|
|
MonotonicTime addedTimeStart = MonotonicTime::now();
|
|
#endif
|
|
if (m_contentExtensions.isEmpty()
|
|
|| !resourceLoadInfo.resourceURL.isValid()
|
|
|| resourceLoadInfo.resourceURL.protocolIsData())
|
|
return { };
|
|
|
|
const String& urlString = resourceLoadInfo.resourceURL.string();
|
|
ASSERT_WITH_MESSAGE(urlString.isAllASCII(), "A decoded URL should only contain ASCII characters. The matching algorithm assumes the input is ASCII.");
|
|
const auto urlCString = urlString.utf8();
|
|
|
|
Vector<ActionsFromContentRuleList> actionsVector;
|
|
actionsVector.reserveInitialCapacity(m_contentExtensions.size());
|
|
const ResourceFlags flags = resourceLoadInfo.getResourceFlags();
|
|
for (auto& contentExtension : m_contentExtensions.values()) {
|
|
ActionsFromContentRuleList actionsStruct;
|
|
actionsStruct.contentRuleListIdentifier = contentExtension->identifier();
|
|
|
|
const CompiledContentExtension& compiledExtension = contentExtension->compiledExtension();
|
|
|
|
DFABytecodeInterpreter withoutConditionsInterpreter(compiledExtension.filtersWithoutConditionsBytecode(), compiledExtension.filtersWithoutConditionsBytecodeLength());
|
|
DFABytecodeInterpreter::Actions withoutConditionsActions = withoutConditionsInterpreter.interpret(urlCString, flags);
|
|
|
|
URL topURL = resourceLoadInfo.mainDocumentURL;
|
|
DFABytecodeInterpreter withConditionsInterpreter(compiledExtension.filtersWithConditionsBytecode(), compiledExtension.filtersWithConditionsBytecodeLength());
|
|
DFABytecodeInterpreter::Actions withConditionsActions = withConditionsInterpreter.interpretWithConditions(urlCString, flags, contentExtension->topURLActions(topURL));
|
|
|
|
const SerializedActionByte* actions = compiledExtension.actions();
|
|
const unsigned actionsLength = compiledExtension.actionsLength();
|
|
|
|
const Vector<uint32_t>& universalWithConditions = contentExtension->universalActionsWithConditions(topURL);
|
|
const Vector<uint32_t>& universalWithoutConditions = contentExtension->universalActionsWithoutConditions();
|
|
if (!withoutConditionsActions.isEmpty() || !withConditionsActions.isEmpty() || !universalWithConditions.isEmpty() || !universalWithoutConditions.isEmpty()) {
|
|
Vector<uint32_t> actionLocations;
|
|
actionLocations.reserveInitialCapacity(withoutConditionsActions.size() + withConditionsActions.size() + universalWithoutConditions.size() + universalWithConditions.size());
|
|
for (uint64_t actionLocation : withoutConditionsActions)
|
|
actionLocations.uncheckedAppend(static_cast<uint32_t>(actionLocation));
|
|
for (uint64_t actionLocation : withConditionsActions)
|
|
actionLocations.uncheckedAppend(static_cast<uint32_t>(actionLocation));
|
|
for (uint32_t actionLocation : universalWithoutConditions)
|
|
actionLocations.uncheckedAppend(actionLocation);
|
|
for (uint32_t actionLocation : universalWithConditions)
|
|
actionLocations.uncheckedAppend(actionLocation);
|
|
std::sort(actionLocations.begin(), actionLocations.end());
|
|
|
|
// Add actions in reverse order to properly deal with IgnorePreviousRules.
|
|
for (unsigned i = actionLocations.size(); i; i--) {
|
|
Action action = Action::deserialize(actions, actionsLength, actionLocations[i - 1]);
|
|
if (action.type() == ActionType::IgnorePreviousRules) {
|
|
actionsStruct.sawIgnorePreviousRules = true;
|
|
break;
|
|
}
|
|
actionsStruct.actions.append(WTFMove(action));
|
|
}
|
|
}
|
|
actionsVector.uncheckedAppend(WTFMove(actionsStruct));
|
|
}
|
|
#if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
|
|
MonotonicTime addedTimeEnd = MonotonicTime::now();
|
|
dataLogF("Time added: %f microseconds %s \n", (addedTimeEnd - addedTimeStart).microseconds(), resourceLoadInfo.resourceURL.string().utf8().data());
|
|
#endif
|
|
return actionsVector;
|
|
}
|
|
|
|
void ContentExtensionsBackend::forEach(const WTF::Function<void(const String&, ContentExtension&)>& apply)
|
|
{
|
|
for (auto& pair : m_contentExtensions)
|
|
apply(pair.key, pair.value);
|
|
}
|
|
|
|
StyleSheetContents* ContentExtensionsBackend::globalDisplayNoneStyleSheet(const String& identifier) const
|
|
{
|
|
const auto& contentExtension = m_contentExtensions.get(identifier);
|
|
return contentExtension ? contentExtension->globalDisplayNoneStyleSheet() : nullptr;
|
|
}
|
|
|
|
ContentRuleListResults ContentExtensionsBackend::processContentRuleListsForLoad(Page& page, const URL& url, OptionSet<ResourceType> resourceType, DocumentLoader& initiatingDocumentLoader, const URL& redirectFrom)
|
|
{
|
|
Document* currentDocument = nullptr;
|
|
URL mainDocumentURL;
|
|
bool mainFrameContext = false;
|
|
|
|
if (auto* frame = initiatingDocumentLoader.frame()) {
|
|
mainFrameContext = frame->isMainFrame();
|
|
currentDocument = frame->document();
|
|
|
|
if (initiatingDocumentLoader.isLoadingMainResource()
|
|
&& frame->isMainFrame()
|
|
&& resourceType == ResourceType::Document)
|
|
mainDocumentURL = url;
|
|
else if (auto* mainDocument = frame->mainFrame().document())
|
|
mainDocumentURL = mainDocument->url();
|
|
}
|
|
|
|
ResourceLoadInfo resourceLoadInfo = { url, mainDocumentURL, resourceType, mainFrameContext };
|
|
auto actions = actionsForResourceLoad(resourceLoadInfo);
|
|
|
|
ContentRuleListResults results;
|
|
if (page.httpsUpgradeEnabled())
|
|
makeSecureIfNecessary(results, url, redirectFrom);
|
|
results.results.reserveInitialCapacity(actions.size());
|
|
for (const auto& actionsFromContentRuleList : actions) {
|
|
const String& contentRuleListIdentifier = actionsFromContentRuleList.contentRuleListIdentifier;
|
|
ContentRuleListResults::Result result;
|
|
for (const auto& action : actionsFromContentRuleList.actions) {
|
|
switch (action.type()) {
|
|
case ContentExtensions::ActionType::BlockLoad:
|
|
results.summary.blockedLoad = true;
|
|
result.blockedLoad = true;
|
|
break;
|
|
case ContentExtensions::ActionType::BlockCookies:
|
|
results.summary.blockedCookies = true;
|
|
result.blockedCookies = true;
|
|
break;
|
|
case ContentExtensions::ActionType::CSSDisplayNoneSelector:
|
|
if (resourceType == ResourceType::Document)
|
|
initiatingDocumentLoader.addPendingContentExtensionDisplayNoneSelector(contentRuleListIdentifier, action.stringArgument(), action.actionID());
|
|
else if (currentDocument)
|
|
currentDocument->extensionStyleSheets().addDisplayNoneSelector(contentRuleListIdentifier, action.stringArgument(), action.actionID());
|
|
break;
|
|
case ContentExtensions::ActionType::Notify:
|
|
results.summary.hasNotifications = true;
|
|
result.notifications.append(action.stringArgument());
|
|
break;
|
|
case ContentExtensions::ActionType::MakeHTTPS: {
|
|
if ((url.protocolIs("http") || url.protocolIs("ws"))
|
|
&& (!url.port() || WTF::isDefaultPortForProtocol(url.port().value(), url.protocol()))) {
|
|
results.summary.madeHTTPS = true;
|
|
result.madeHTTPS = true;
|
|
}
|
|
break;
|
|
}
|
|
case ContentExtensions::ActionType::IgnorePreviousRules:
|
|
RELEASE_ASSERT_NOT_REACHED();
|
|
}
|
|
}
|
|
|
|
if (!actionsFromContentRuleList.sawIgnorePreviousRules) {
|
|
if (auto* styleSheetContents = globalDisplayNoneStyleSheet(contentRuleListIdentifier)) {
|
|
if (resourceType == ResourceType::Document)
|
|
initiatingDocumentLoader.addPendingContentExtensionSheet(contentRuleListIdentifier, *styleSheetContents);
|
|
else if (currentDocument)
|
|
currentDocument->extensionStyleSheets().maybeAddContentExtensionSheet(contentRuleListIdentifier, *styleSheetContents);
|
|
}
|
|
}
|
|
|
|
results.results.uncheckedAppend({ contentRuleListIdentifier, WTFMove(result) });
|
|
}
|
|
|
|
if (currentDocument) {
|
|
if (results.summary.madeHTTPS) {
|
|
ASSERT(url.protocolIs("http") || url.protocolIs("ws"));
|
|
String newProtocol = url.protocolIs("http") ? "https"_s : "wss"_s;
|
|
currentDocument->addConsoleMessage(MessageSource::ContentBlocker, MessageLevel::Info, makeString("Promoted URL from ", url.string(), " to ", newProtocol));
|
|
}
|
|
if (results.summary.blockedLoad) {
|
|
currentDocument->addConsoleMessage(MessageSource::ContentBlocker, MessageLevel::Info, makeString("Content blocker prevented frame displaying ", mainDocumentURL.string(), " from loading a resource from ", url.string()));
|
|
|
|
// Quirk for content-blocker interference with Google's anti-flicker optimization (rdar://problem/45968770).
|
|
// https://developers.google.com/optimize/
|
|
if (currentDocument->settings().googleAntiFlickerOptimizationQuirkEnabled()
|
|
&& ((equalLettersIgnoringASCIICase(url.host(), "www.google-analytics.com") && equalLettersIgnoringASCIICase(url.path(), "/analytics.js"))
|
|
|| (equalLettersIgnoringASCIICase(url.host(), "www.googletagmanager.com") && equalLettersIgnoringASCIICase(url.path(), "/gtm.js")))) {
|
|
if (auto* frame = currentDocument->frame())
|
|
frame->script().evaluateIgnoringException(ScriptSourceCode { "try { window.dataLayer.hide.end(); console.log('Called window.dataLayer.hide.end() in frame ' + document.URL + ' because the content blocker blocked the load of the https://www.google-analytics.com/analytics.js script'); } catch (e) { }"_s });
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
ContentRuleListResults ContentExtensionsBackend::processContentRuleListsForPingLoad(const URL& url, const URL& mainDocumentURL)
|
|
{
|
|
ResourceLoadInfo resourceLoadInfo = { url, mainDocumentURL, ResourceType::Ping };
|
|
auto actions = actionsForResourceLoad(resourceLoadInfo);
|
|
|
|
ContentRuleListResults results;
|
|
makeSecureIfNecessary(results, url);
|
|
for (const auto& actionsFromContentRuleList : actions) {
|
|
for (const auto& action : actionsFromContentRuleList.actions) {
|
|
switch (action.type()) {
|
|
case ContentExtensions::ActionType::BlockLoad:
|
|
results.summary.blockedLoad = true;
|
|
break;
|
|
case ContentExtensions::ActionType::BlockCookies:
|
|
results.summary.blockedCookies = true;
|
|
break;
|
|
case ContentExtensions::ActionType::MakeHTTPS:
|
|
if ((url.protocolIs("http") || url.protocolIs("ws")) && (!url.port() || WTF::isDefaultPortForProtocol(url.port().value(), url.protocol())))
|
|
results.summary.madeHTTPS = true;
|
|
break;
|
|
case ContentExtensions::ActionType::CSSDisplayNoneSelector:
|
|
case ContentExtensions::ActionType::Notify:
|
|
// We currently have not implemented notifications from the NetworkProcess to the UIProcess.
|
|
break;
|
|
case ContentExtensions::ActionType::IgnorePreviousRules:
|
|
RELEASE_ASSERT_NOT_REACHED();
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
const String& ContentExtensionsBackend::displayNoneCSSRule()
|
|
{
|
|
static NeverDestroyed<const String> rule(MAKE_STATIC_STRING_IMPL("display:none !important;"));
|
|
return rule;
|
|
}
|
|
|
|
void applyResultsToRequest(ContentRuleListResults&& results, Page* page, ResourceRequest& request)
|
|
{
|
|
if (results.summary.blockedCookies)
|
|
request.setAllowCookies(false);
|
|
|
|
if (results.summary.madeHTTPS) {
|
|
const URL& originalURL = request.url();
|
|
ASSERT(originalURL.protocolIs("http"));
|
|
ASSERT(!originalURL.port() || WTF::isDefaultPortForProtocol(originalURL.port().value(), originalURL.protocol()));
|
|
|
|
URL newURL = originalURL;
|
|
newURL.setProtocol("https");
|
|
if (originalURL.port())
|
|
newURL.setPort(WTF::defaultPortForProtocol("https").value());
|
|
request.setURL(newURL);
|
|
}
|
|
|
|
if (page && results.shouldNotifyApplication()) {
|
|
results.results.removeAllMatching([](const auto& pair) {
|
|
return !pair.second.shouldNotifyApplication();
|
|
});
|
|
page->chrome().client().contentRuleListNotification(request.url(), results);
|
|
}
|
|
}
|
|
|
|
} // namespace ContentExtensions
|
|
|
|
} // namespace WebCore
|
|
|
|
#endif // ENABLE(CONTENT_EXTENSIONS)
|