1/*
2 * Copyright (C) 2011 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1.  Redistributions of source code must retain the above copyright
8 *     notice, this list of conditions and the following disclaimer.
9 * 2.  Redistributions in binary form must reproduce the above copyright
10 *     notice, this list of conditions and the following disclaimer in the
11 *     documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
17 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
20 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 */
24
25
26#include "config.h"
27
28#if ENABLE(INSPECTOR)
29#include "InspectorConsoleAgent.h"
30
31#include "InstrumentingAgents.h"
32#include "Console.h"
33#include "ConsoleMessage.h"
34#include "DOMWindow.h"
35#include "InjectedScriptHost.h"
36#include "InjectedScriptManager.h"
37#include "InspectorFrontend.h"
38#include "InspectorState.h"
39#include "ResourceError.h"
40#include "ResourceResponse.h"
41#include "ScriptArguments.h"
42#include "ScriptCallFrame.h"
43#include "ScriptCallStack.h"
44#include "ScriptCallStackFactory.h"
45#include "ScriptController.h"
46#include "ScriptObject.h"
47#include "ScriptProfiler.h"
48#include <wtf/CurrentTime.h>
49#include <wtf/OwnPtr.h>
50#include <wtf/PassOwnPtr.h>
51#include <wtf/text/StringBuilder.h>
52#include <wtf/text/WTFString.h>
53
54namespace WebCore {
55
56static const unsigned maximumConsoleMessages = 1000;
57static const int expireConsoleMessagesStep = 100;
58
59namespace ConsoleAgentState {
60static const char monitoringXHR[] = "monitoringXHR";
61static const char consoleMessagesEnabled[] = "consoleMessagesEnabled";
62}
63
64int InspectorConsoleAgent::s_enabledAgentCount = 0;
65
66InspectorConsoleAgent::InspectorConsoleAgent(InstrumentingAgents* instrumentingAgents, InspectorCompositeState* state, InjectedScriptManager* injectedScriptManager)
67    : InspectorBaseAgent<InspectorConsoleAgent>("Console", instrumentingAgents, state)
68    , m_injectedScriptManager(injectedScriptManager)
69    , m_frontend(0)
70    , m_previousMessage(0)
71    , m_expiredConsoleMessageCount(0)
72    , m_enabled(false)
73{
74    m_instrumentingAgents->setInspectorConsoleAgent(this);
75}
76
77InspectorConsoleAgent::~InspectorConsoleAgent()
78{
79    m_instrumentingAgents->setInspectorConsoleAgent(0);
80    m_instrumentingAgents = 0;
81    m_state = 0;
82    m_injectedScriptManager = 0;
83}
84
85void InspectorConsoleAgent::enable(ErrorString*)
86{
87    if (m_enabled)
88        return;
89    m_enabled = true;
90    if (!s_enabledAgentCount)
91        ScriptController::setCaptureCallStackForUncaughtExceptions(true);
92    ++s_enabledAgentCount;
93
94    m_state->setBoolean(ConsoleAgentState::consoleMessagesEnabled, true);
95
96    if (m_expiredConsoleMessageCount) {
97        ConsoleMessage expiredMessage(!isWorkerAgent(), OtherMessageSource, LogMessageType, WarningMessageLevel, String::format("%d console messages are not shown.", m_expiredConsoleMessageCount));
98        expiredMessage.addToFrontend(m_frontend, m_injectedScriptManager, false);
99    }
100
101    size_t messageCount = m_consoleMessages.size();
102    for (size_t i = 0; i < messageCount; ++i)
103        m_consoleMessages[i]->addToFrontend(m_frontend, m_injectedScriptManager, false);
104}
105
106void InspectorConsoleAgent::disable(ErrorString*)
107{
108    if (!m_enabled)
109        return;
110    m_enabled = false;
111    if (!(--s_enabledAgentCount))
112        ScriptController::setCaptureCallStackForUncaughtExceptions(false);
113    m_state->setBoolean(ConsoleAgentState::consoleMessagesEnabled, false);
114}
115
116void InspectorConsoleAgent::clearMessages(ErrorString*)
117{
118    m_consoleMessages.clear();
119    m_expiredConsoleMessageCount = 0;
120    m_previousMessage = 0;
121    m_injectedScriptManager->releaseObjectGroup("console");
122    if (m_frontend && m_enabled)
123        m_frontend->messagesCleared();
124}
125
126void InspectorConsoleAgent::reset()
127{
128    ErrorString error;
129    clearMessages(&error);
130    m_times.clear();
131    m_counts.clear();
132}
133
134void InspectorConsoleAgent::restore()
135{
136    if (m_state->getBoolean(ConsoleAgentState::consoleMessagesEnabled)) {
137        m_frontend->messagesCleared();
138        ErrorString error;
139        enable(&error);
140    }
141}
142
143void InspectorConsoleAgent::setFrontend(InspectorFrontend* frontend)
144{
145    m_frontend = frontend->console();
146}
147
148void InspectorConsoleAgent::clearFrontend()
149{
150    m_frontend = 0;
151    String errorString;
152    disable(&errorString);
153}
154
155void InspectorConsoleAgent::addMessageToConsole(MessageSource source, MessageType type, MessageLevel level, const String& message, PassRefPtr<ScriptCallStack> callStack, unsigned long requestIdentifier)
156{
157    if (!developerExtrasEnabled())
158        return;
159
160    if (type == ClearMessageType) {
161        ErrorString error;
162        clearMessages(&error);
163    }
164
165    addConsoleMessage(adoptPtr(new ConsoleMessage(!isWorkerAgent(), source, type, level, message, callStack, requestIdentifier)));
166}
167
168void InspectorConsoleAgent::addMessageToConsole(MessageSource source, MessageType type, MessageLevel level, const String& message, ScriptState* state, PassRefPtr<ScriptArguments> arguments, unsigned long requestIdentifier)
169{
170    if (!developerExtrasEnabled())
171        return;
172
173    if (type == ClearMessageType) {
174        ErrorString error;
175        clearMessages(&error);
176    }
177
178    addConsoleMessage(adoptPtr(new ConsoleMessage(!isWorkerAgent(), source, type, level, message, arguments, state, requestIdentifier)));
179}
180
181void InspectorConsoleAgent::addMessageToConsole(MessageSource source, MessageType type, MessageLevel level, const String& message, const String& scriptId, unsigned lineNumber, unsigned columnNumber, ScriptState* state, unsigned long requestIdentifier)
182{
183    if (!developerExtrasEnabled())
184        return;
185
186    if (type == ClearMessageType) {
187        ErrorString error;
188        clearMessages(&error);
189    }
190
191    bool canGenerateCallStack = !isWorkerAgent() && m_frontend;
192    addConsoleMessage(adoptPtr(new ConsoleMessage(canGenerateCallStack, source, type, level, message, scriptId, lineNumber, columnNumber, state, requestIdentifier)));
193}
194
195Vector<unsigned> InspectorConsoleAgent::consoleMessageArgumentCounts()
196{
197    Vector<unsigned> result(m_consoleMessages.size());
198    for (size_t i = 0; i < m_consoleMessages.size(); i++)
199        result[i] = m_consoleMessages[i]->argumentCount();
200    return result;
201}
202
203void InspectorConsoleAgent::startTiming(const String& title)
204{
205    // Follow Firebug's behavior of requiring a title that is not null or
206    // undefined for timing functions
207    if (title.isNull())
208        return;
209
210    m_times.add(title, monotonicallyIncreasingTime());
211}
212
213void InspectorConsoleAgent::stopTiming(const String& title, PassRefPtr<ScriptCallStack> callStack)
214{
215    // Follow Firebug's behavior of requiring a title that is not null or
216    // undefined for timing functions
217    if (title.isNull())
218        return;
219
220    HashMap<String, double>::iterator it = m_times.find(title);
221    if (it == m_times.end())
222        return;
223
224    double startTime = it->value;
225    m_times.remove(it);
226
227    double elapsed = monotonicallyIncreasingTime() - startTime;
228    String message = title + String::format(": %.3fms", elapsed * 1000);
229    addMessageToConsole(ConsoleAPIMessageSource, TimingMessageType, DebugMessageLevel, message, callStack);
230}
231
232void InspectorConsoleAgent::count(ScriptState* state, PassRefPtr<ScriptArguments> arguments)
233{
234    RefPtr<ScriptCallStack> callStack(createScriptCallStackForConsole(state));
235    const ScriptCallFrame& lastCaller = callStack->at(0);
236    // Follow Firebug's behavior of counting with null and undefined title in
237    // the same bucket as no argument
238    String title;
239    arguments->getFirstArgumentAsString(title);
240    String identifier = title + '@' + lastCaller.sourceURL() + ':' + String::number(lastCaller.lineNumber());
241
242    HashMap<String, unsigned>::iterator it = m_counts.find(identifier);
243    int count;
244    if (it == m_counts.end())
245        count = 1;
246    else {
247        count = it->value + 1;
248        m_counts.remove(it);
249    }
250
251    m_counts.add(identifier, count);
252
253    String message = title + ": " + String::number(count);
254    addMessageToConsole(ConsoleAPIMessageSource, LogMessageType, DebugMessageLevel, message, callStack);
255}
256
257void InspectorConsoleAgent::frameWindowDiscarded(DOMWindow* window)
258{
259    size_t messageCount = m_consoleMessages.size();
260    for (size_t i = 0; i < messageCount; ++i)
261        m_consoleMessages[i]->windowCleared(window);
262    m_injectedScriptManager->discardInjectedScriptsFor(window);
263}
264
265void InspectorConsoleAgent::didFinishXHRLoading(unsigned long requestIdentifier, const String& url, const String& sendURL, unsigned sendLineNumber)
266{
267    if (!developerExtrasEnabled())
268        return;
269    if (m_frontend && m_state->getBoolean(ConsoleAgentState::monitoringXHR)) {
270        String message = "XHR finished loading: \"" + url + "\".";
271        // FIXME: <http://webkit.org/b/114316> InspectorConsoleAgent::didFinishXHRLoading ConsoleMessage should include a column number
272        addMessageToConsole(NetworkMessageSource, LogMessageType, DebugMessageLevel, message, sendURL, sendLineNumber, 0, 0, requestIdentifier);
273    }
274}
275
276void InspectorConsoleAgent::didReceiveResponse(unsigned long requestIdentifier, const ResourceResponse& response)
277{
278    if (!developerExtrasEnabled())
279        return;
280
281    if (response.httpStatusCode() >= 400) {
282        String message = "Failed to load resource: the server responded with a status of " + String::number(response.httpStatusCode()) + " (" + response.httpStatusText() + ')';
283        addMessageToConsole(NetworkMessageSource, LogMessageType, ErrorMessageLevel, message, response.url().string(), 0, 0, 0, requestIdentifier);
284    }
285}
286
287void InspectorConsoleAgent::didFailLoading(unsigned long requestIdentifier, const ResourceError& error)
288{
289    if (!developerExtrasEnabled())
290        return;
291    if (error.isCancellation()) // Report failures only.
292        return;
293    StringBuilder message;
294    message.appendLiteral("Failed to load resource");
295    if (!error.localizedDescription().isEmpty()) {
296        message.appendLiteral(": ");
297        message.append(error.localizedDescription());
298    }
299    addMessageToConsole(NetworkMessageSource, LogMessageType, ErrorMessageLevel, message.toString(), error.failingURL(), 0, 0, 0, requestIdentifier);
300}
301
302void InspectorConsoleAgent::setMonitoringXHREnabled(ErrorString*, bool enabled)
303{
304    m_state->setBoolean(ConsoleAgentState::monitoringXHR, enabled);
305}
306
307static bool isGroupMessage(MessageType type)
308{
309    return type == StartGroupMessageType
310        || type ==  StartGroupCollapsedMessageType
311        || type == EndGroupMessageType;
312}
313
314void InspectorConsoleAgent::addConsoleMessage(PassOwnPtr<ConsoleMessage> consoleMessage)
315{
316    ASSERT(developerExtrasEnabled());
317    ASSERT_ARG(consoleMessage, consoleMessage);
318
319    if (m_previousMessage && !isGroupMessage(m_previousMessage->type()) && m_previousMessage->isEqual(consoleMessage.get())) {
320        m_previousMessage->incrementCount();
321        if (m_frontend && m_enabled)
322            m_previousMessage->updateRepeatCountInConsole(m_frontend);
323    } else {
324        m_previousMessage = consoleMessage.get();
325        m_consoleMessages.append(consoleMessage);
326        if (m_frontend && m_enabled)
327            m_previousMessage->addToFrontend(m_frontend, m_injectedScriptManager, true);
328    }
329
330    if (!m_frontend && m_consoleMessages.size() >= maximumConsoleMessages) {
331        m_expiredConsoleMessageCount += expireConsoleMessagesStep;
332        m_consoleMessages.remove(0, expireConsoleMessagesStep);
333    }
334}
335
336class InspectableHeapObject : public InjectedScriptHost::InspectableObject {
337public:
338    explicit InspectableHeapObject(int heapObjectId) : m_heapObjectId(heapObjectId) { }
339    virtual ScriptValue get(ScriptState*)
340    {
341        return ScriptProfiler::objectByHeapObjectId(m_heapObjectId);
342    }
343private:
344    int m_heapObjectId;
345};
346
347void InspectorConsoleAgent::addInspectedHeapObject(ErrorString*, int inspectedHeapObjectId)
348{
349    m_injectedScriptManager->injectedScriptHost()->addInspectedObject(adoptPtr(new InspectableHeapObject(inspectedHeapObjectId)));
350}
351
352} // namespace WebCore
353
354#endif // ENABLE(INSPECTOR)
355