blob: 5a02075e1fa037eda81c27a32f09b05ddd721454 [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
* Copyright (C) 2015 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:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
* OWNER OR 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 "InspectorDOMDebuggerAgent.h"
#include "Event.h"
#include "EventTarget.h"
#include "InspectorDOMAgent.h"
#include "InstrumentingAgents.h"
#include "JSEvent.h"
#include "RegisteredEventListener.h"
#include "ScriptExecutionContext.h"
#include <JavaScriptCore/ContentSearchUtilities.h>
#include <JavaScriptCore/InjectedScript.h>
#include <JavaScriptCore/InjectedScriptManager.h>
#include <JavaScriptCore/InspectorFrontendDispatchers.h>
#include <JavaScriptCore/RegularExpression.h>
#include <wtf/JSONValues.h>
namespace WebCore {
using namespace Inspector;
InspectorDOMDebuggerAgent::InspectorDOMDebuggerAgent(WebAgentContext& context, InspectorDebuggerAgent* debuggerAgent)
: InspectorAgentBase("DOMDebugger"_s, context)
, m_debuggerAgent(debuggerAgent)
, m_backendDispatcher(Inspector::DOMDebuggerBackendDispatcher::create(context.backendDispatcher, this))
, m_injectedScriptManager(context.injectedScriptManager)
{
m_debuggerAgent->addListener(*this);
}
InspectorDOMDebuggerAgent::~InspectorDOMDebuggerAgent() = default;
bool InspectorDOMDebuggerAgent::enabled() const
{
return m_instrumentingAgents.enabledDOMDebuggerAgent() == this;
}
void InspectorDOMDebuggerAgent::enable()
{
m_instrumentingAgents.setEnabledDOMDebuggerAgent(this);
}
void InspectorDOMDebuggerAgent::disable()
{
m_instrumentingAgents.setEnabledDOMDebuggerAgent(nullptr);
m_listenerBreakpoints.clear();
m_pauseOnAllIntervalsBreakpoint = nullptr;
m_pauseOnAllListenersBreakpoint = nullptr;
m_pauseOnAllTimeoutsBreakpoint = nullptr;
m_urlTextBreakpoints.clear();
m_urlRegexBreakpoints.clear();
m_pauseOnAllURLsBreakpoint = nullptr;
}
// Browser debugger agent enabled only when JS debugger is enabled.
void InspectorDOMDebuggerAgent::debuggerWasEnabled()
{
ASSERT(!enabled());
enable();
}
void InspectorDOMDebuggerAgent::debuggerWasDisabled()
{
ASSERT(enabled());
disable();
}
void InspectorDOMDebuggerAgent::didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*)
{
}
void InspectorDOMDebuggerAgent::willDestroyFrontendAndBackend(Inspector::DisconnectReason)
{
disable();
}
void InspectorDOMDebuggerAgent::discardAgent()
{
m_debuggerAgent->removeListener(*this);
m_debuggerAgent = nullptr;
}
void InspectorDOMDebuggerAgent::mainFrameNavigated()
{
for (auto& breakpoint : m_listenerBreakpoints.values())
breakpoint->resetHitCount();
if (m_pauseOnAllIntervalsBreakpoint)
m_pauseOnAllIntervalsBreakpoint->resetHitCount();
if (m_pauseOnAllListenersBreakpoint)
m_pauseOnAllListenersBreakpoint->resetHitCount();
if (m_pauseOnAllTimeoutsBreakpoint)
m_pauseOnAllTimeoutsBreakpoint->resetHitCount();
}
Protocol::ErrorStringOr<void> InspectorDOMDebuggerAgent::setEventBreakpoint(Protocol::DOMDebugger::EventBreakpointType breakpointType, const String& eventName, RefPtr<JSON::Object>&& options)
{
Protocol::ErrorString errorString;
auto breakpoint = InspectorDebuggerAgent::debuggerBreakpointFromPayload(errorString, WTFMove(options));
if (!breakpoint)
return makeUnexpected(errorString);
if (!eventName.isEmpty()) {
if (breakpointType == Protocol::DOMDebugger::EventBreakpointType::Listener) {
if (!m_listenerBreakpoints.add(eventName, breakpoint.releaseNonNull()))
return makeUnexpected("Breakpoint with eventName already exists"_s);
return { };
}
return makeUnexpected("Unexpected eventName"_s);
}
switch (breakpointType) {
case Protocol::DOMDebugger::EventBreakpointType::AnimationFrame:
if (!setAnimationFrameBreakpoint(errorString, WTFMove(breakpoint)))
return makeUnexpected(errorString);
return { };
case Protocol::DOMDebugger::EventBreakpointType::Interval:
if (m_pauseOnAllIntervalsBreakpoint)
return makeUnexpected("Breakpoint for Interval already exists"_s);
m_pauseOnAllIntervalsBreakpoint = WTFMove(breakpoint);
return { };
case Protocol::DOMDebugger::EventBreakpointType::Listener:
if (m_pauseOnAllListenersBreakpoint)
return makeUnexpected("Breakpoint for Listener already exists"_s);
m_pauseOnAllListenersBreakpoint = WTFMove(breakpoint);
return { };
case Protocol::DOMDebugger::EventBreakpointType::Timeout:
if (m_pauseOnAllTimeoutsBreakpoint)
return makeUnexpected("Breakpoint for Timeout already exists"_s);
m_pauseOnAllTimeoutsBreakpoint = WTFMove(breakpoint);
return { };
}
ASSERT_NOT_REACHED();
return makeUnexpected("Not supported"_s);
}
Protocol::ErrorStringOr<void> InspectorDOMDebuggerAgent::removeEventBreakpoint(Protocol::DOMDebugger::EventBreakpointType breakpointType, const String& eventName)
{
Protocol::ErrorString errorString;
if (!eventName.isEmpty()) {
if (breakpointType == Protocol::DOMDebugger::EventBreakpointType::Listener) {
if (!m_listenerBreakpoints.remove(eventName))
return makeUnexpected("Breakpoint for given eventName missing"_s);
return { };
}
return makeUnexpected("Unexpected eventName"_s);
}
switch (breakpointType) {
case Protocol::DOMDebugger::EventBreakpointType::AnimationFrame:
if (!setAnimationFrameBreakpoint(errorString, nullptr))
return makeUnexpected(errorString);
return { };
case Protocol::DOMDebugger::EventBreakpointType::Interval:
if (!m_pauseOnAllIntervalsBreakpoint)
return makeUnexpected("Breakpoint for Intervals missing"_s);
m_pauseOnAllIntervalsBreakpoint = nullptr;
return { };
case Protocol::DOMDebugger::EventBreakpointType::Listener:
if (!m_pauseOnAllListenersBreakpoint)
return makeUnexpected("Breakpoint for Listeners missing"_s);
m_pauseOnAllListenersBreakpoint = nullptr;
return { };
case Protocol::DOMDebugger::EventBreakpointType::Timeout:
if (!m_pauseOnAllTimeoutsBreakpoint)
return makeUnexpected("Breakpoint for Timeouts missing"_s);
m_pauseOnAllTimeoutsBreakpoint = nullptr;
return { };
}
ASSERT_NOT_REACHED();
return makeUnexpected("Not supported"_s);
}
void InspectorDOMDebuggerAgent::willHandleEvent(ScriptExecutionContext& scriptExecutionContext, Event& event, const RegisteredEventListener& registeredEventListener)
{
// `event.target()->scriptExecutionContext()` can change between `willHandleEvent` and `didHandleEvent`. The passed
// `scriptExecutionContext` parameter will always match in companion calls to `willHandleEvent` and
// `didHandleEvent`, and will not be null.
auto state = scriptExecutionContext.globalObject();
auto injectedScript = m_injectedScriptManager.injectedScriptFor(state);
if (injectedScript.hasNoValue())
return;
// Set Web Inspector console command line `$event` getter.
{
JSC::JSLockHolder lock(state);
injectedScript.setEventValue(toJS(state, deprecatedGlobalObjectForPrototype(state), event));
}
if (!m_debuggerAgent->breakpointsActive())
return;
auto* domAgent = m_instrumentingAgents.persistentDOMAgent();
auto breakpoint = m_pauseOnAllListenersBreakpoint;
if (!breakpoint) {
auto it = m_listenerBreakpoints.find(event.type());
if (it != m_listenerBreakpoints.end())
breakpoint = it->value.copyRef();
}
if (!breakpoint && domAgent)
breakpoint = domAgent->breakpointForEventListener(*event.currentTarget(), event.type(), registeredEventListener.callback(), registeredEventListener.useCapture());
if (!breakpoint)
return;
Ref<JSON::Object> eventData = JSON::Object::create();
eventData->setString("eventName"_s, event.type());
if (domAgent) {
int eventListenerId = domAgent->idForEventListener(*event.currentTarget(), event.type(), registeredEventListener.callback(), registeredEventListener.useCapture());
if (eventListenerId)
eventData->setInteger("eventListenerId"_s, eventListenerId);
}
m_debuggerAgent->schedulePauseForSpecialBreakpoint(*breakpoint, Inspector::DebuggerFrontendDispatcher::Reason::Listener, WTFMove(eventData));
}
void InspectorDOMDebuggerAgent::didHandleEvent(ScriptExecutionContext& scriptExecutionContext, Event& event, const RegisteredEventListener& registeredEventListener)
{
// `event.target()->scriptExecutionContext()` can change between `willHandleEvent` and `didHandleEvent`. Here it
// could also be nullptr. The passed `scriptExecutionContext` parameter here will always match in companion calls to
// `willHandleEvent` and `didHandleEvent`, and will not be null.
auto state = scriptExecutionContext.globalObject();
auto injectedScript = m_injectedScriptManager.injectedScriptFor(state);
if (injectedScript.hasNoValue())
return;
// Clear Web Inspector console command line `$event` getter.
{
JSC::JSLockHolder lock(state);
injectedScript.clearEventValue();
}
if (!m_debuggerAgent->breakpointsActive())
return;
auto breakpoint = m_pauseOnAllListenersBreakpoint;
if (!breakpoint)
breakpoint = m_listenerBreakpoints.get(event.type());
if (!breakpoint) {
if (auto* domAgent = m_instrumentingAgents.persistentDOMAgent())
breakpoint = domAgent->breakpointForEventListener(*event.currentTarget(), event.type(), registeredEventListener.callback(), registeredEventListener.useCapture());
}
if (!breakpoint)
return;
m_debuggerAgent->cancelPauseForSpecialBreakpoint(*breakpoint);
}
void InspectorDOMDebuggerAgent::willFireTimer(bool oneShot)
{
if (!m_debuggerAgent->breakpointsActive())
return;
auto breakpoint = oneShot ? m_pauseOnAllTimeoutsBreakpoint : m_pauseOnAllIntervalsBreakpoint;
if (!breakpoint)
return;
auto breakReason = oneShot ? Inspector::DebuggerFrontendDispatcher::Reason::Timeout : Inspector::DebuggerFrontendDispatcher::Reason::Interval;
m_debuggerAgent->schedulePauseForSpecialBreakpoint(*breakpoint, breakReason);
}
void InspectorDOMDebuggerAgent::didFireTimer(bool oneShot)
{
if (!m_debuggerAgent->breakpointsActive())
return;
auto breakpoint = oneShot ? m_pauseOnAllTimeoutsBreakpoint : m_pauseOnAllIntervalsBreakpoint;
if (!breakpoint)
return;
m_debuggerAgent->cancelPauseForSpecialBreakpoint(*breakpoint);
}
Protocol::ErrorStringOr<void> InspectorDOMDebuggerAgent::setURLBreakpoint(const String& url, std::optional<bool>&& isRegex, RefPtr<JSON::Object>&& options)
{
Protocol::ErrorString errorString;
auto breakpoint = InspectorDebuggerAgent::debuggerBreakpointFromPayload(errorString, WTFMove(options));
if (!breakpoint)
return makeUnexpected(errorString);
if (url.isEmpty()) {
if (m_pauseOnAllURLsBreakpoint)
return makeUnexpected("Breakpoint for all URLs already exists"_s);
m_pauseOnAllURLsBreakpoint = WTFMove(breakpoint);
return { };
}
if (isRegex && *isRegex) {
if (!m_urlRegexBreakpoints.add(url, breakpoint.releaseNonNull()))
return makeUnexpected("Breakpoint for given regex already exists"_s);
} else {
if (!m_urlTextBreakpoints.add(url, breakpoint.releaseNonNull()))
return makeUnexpected("Breakpoint for given URL already exists"_s);
}
return { };
}
Protocol::ErrorStringOr<void> InspectorDOMDebuggerAgent::removeURLBreakpoint(const String& url, std::optional<bool>&& isRegex)
{
if (url.isEmpty()) {
if (!m_pauseOnAllURLsBreakpoint)
return makeUnexpected("Breakpoint for all URLs missing"_s);
m_pauseOnAllURLsBreakpoint = nullptr;
return { };
}
if (isRegex && *isRegex) {
if (!m_urlRegexBreakpoints.remove(url))
return makeUnexpected("Missing breakpoint for given regex"_s);
} else {
if (!m_urlTextBreakpoints.remove(url))
return makeUnexpected("Missing breakpoint for given URL"_s);
}
return { };
}
void InspectorDOMDebuggerAgent::breakOnURLIfNeeded(const String& url, URLBreakpointSource source)
{
if (!m_debuggerAgent->breakpointsActive())
return;
constexpr bool caseSensitive = false;
auto breakpointURL = emptyString();
auto breakpoint = m_pauseOnAllURLsBreakpoint.copyRef();
if (!breakpoint) {
for (auto& [query, textBreakpoint] : m_urlTextBreakpoints) {
auto regex = ContentSearchUtilities::createRegularExpressionForSearchString(query, caseSensitive, ContentSearchUtilities::SearchStringType::ContainsString);
if (regex.match(url) != -1) {
breakpoint = textBreakpoint.copyRef();
breakpointURL = query;
break;
}
}
}
if (!breakpoint) {
for (auto& [query, regexBreakpoint] : m_urlRegexBreakpoints) {
auto regex = ContentSearchUtilities::createRegularExpressionForSearchString(query, caseSensitive, ContentSearchUtilities::SearchStringType::Regex);
if (regex.match(url) != -1) {
breakpoint = regexBreakpoint.copyRef();
breakpointURL = query;
break;
}
}
}
if (!breakpoint)
return;
auto breakReason = Inspector::DebuggerFrontendDispatcher::Reason::Other;
switch (source) {
case URLBreakpointSource::Fetch:
breakReason = Inspector::DebuggerFrontendDispatcher::Reason::Fetch;
break;
case URLBreakpointSource::XHR:
breakReason = Inspector::DebuggerFrontendDispatcher::Reason::XHR;
break;
}
Ref<JSON::Object> eventData = JSON::Object::create();
eventData->setString("breakpointURL"_s, breakpointURL);
eventData->setString("url"_s, url);
m_debuggerAgent->breakProgram(breakReason, WTFMove(eventData), WTFMove(breakpoint));
}
void InspectorDOMDebuggerAgent::willSendXMLHttpRequest(const String& url)
{
breakOnURLIfNeeded(url, URLBreakpointSource::XHR);
}
void InspectorDOMDebuggerAgent::willFetch(const String& url)
{
breakOnURLIfNeeded(url, URLBreakpointSource::Fetch);
}
} // namespace WebCore