| /* |
| * 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 |