1/*
2 * Copyright (C) 2010 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 are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31#include "config.h"
32
33#if ENABLE(INSPECTOR)
34
35#include "InspectorFrontendClientLocal.h"
36
37#include "Chrome.h"
38#include "DOMWrapperWorld.h"
39#include "Document.h"
40#include "FloatRect.h"
41#include "FrameLoadRequest.h"
42#include "FrameLoader.h"
43#include "FrameView.h"
44#include "InspectorController.h"
45#include "InspectorFrontendHost.h"
46#include "InspectorPageAgent.h"
47#include "InspectorWebBackendDispatchers.h"
48#include "MainFrame.h"
49#include "Page.h"
50#include "ScriptController.h"
51#include "ScriptGlobalObject.h"
52#include "ScriptState.h"
53#include "Settings.h"
54#include "Timer.h"
55#include "UserGestureIndicator.h"
56#include "WindowFeatures.h"
57#include <bindings/ScriptValue.h>
58#include <wtf/Deque.h>
59#include <wtf/text/CString.h>
60#include <wtf/text/WTFString.h>
61
62using namespace Inspector;
63
64namespace WebCore {
65
66static const char* inspectorAttachedHeightSetting = "inspectorAttachedHeight";
67static const unsigned defaultAttachedHeight = 300;
68static const float minimumAttachedHeight = 250.0f;
69static const float maximumAttachedHeightRatio = 0.75f;
70static const float minimumAttachedWidth = 750.0f;
71static const float minimumAttachedInspectedWidth = 320.0f;
72
73class InspectorBackendDispatchTask {
74    WTF_MAKE_FAST_ALLOCATED;
75public:
76    InspectorBackendDispatchTask(InspectorController* inspectorController)
77        : m_inspectorController(inspectorController)
78        , m_timer(this, &InspectorBackendDispatchTask::timerFired)
79    {
80    }
81
82    void dispatch(const String& message)
83    {
84        m_messages.append(message);
85        if (!m_timer.isActive())
86            m_timer.startOneShot(0);
87    }
88
89    void reset()
90    {
91        m_messages.clear();
92        m_timer.stop();
93    }
94
95    void timerFired(Timer<InspectorBackendDispatchTask>&)
96    {
97        if (!m_messages.isEmpty()) {
98            // Dispatch can lead to the timer destruction -> schedule the next shot first.
99            m_timer.startOneShot(0);
100            m_inspectorController->dispatchMessageFromFrontend(m_messages.takeFirst());
101        }
102    }
103
104private:
105    InspectorController* m_inspectorController;
106    Timer<InspectorBackendDispatchTask> m_timer;
107    Deque<String> m_messages;
108};
109
110String InspectorFrontendClientLocal::Settings::getProperty(const String&)
111{
112    return String();
113}
114
115void InspectorFrontendClientLocal::Settings::setProperty(const String&, const String&)
116{
117}
118
119InspectorFrontendClientLocal::InspectorFrontendClientLocal(InspectorController* inspectorController, Page* frontendPage, std::unique_ptr<Settings> settings)
120    : m_inspectorController(inspectorController)
121    , m_frontendPage(frontendPage)
122    , m_settings(WTF::move(settings))
123    , m_frontendLoaded(false)
124    , m_dockSide(UNDOCKED)
125{
126    m_frontendPage->settings().setAllowFileAccessFromFileURLs(true);
127    m_dispatchTask = std::make_unique<InspectorBackendDispatchTask>(inspectorController);
128}
129
130InspectorFrontendClientLocal::~InspectorFrontendClientLocal()
131{
132    if (m_frontendHost)
133        m_frontendHost->disconnectClient();
134    m_frontendPage = 0;
135    m_inspectorController = 0;
136}
137
138void InspectorFrontendClientLocal::windowObjectCleared()
139{
140    if (m_frontendHost)
141        m_frontendHost->disconnectClient();
142
143    JSC::ExecState* frontendExecState = execStateFromPage(debuggerWorld(), m_frontendPage);
144    m_frontendHost = InspectorFrontendHost::create(this, m_frontendPage);
145    ScriptGlobalObject::set(frontendExecState, "InspectorFrontendHost", m_frontendHost.get());
146}
147
148void InspectorFrontendClientLocal::frontendLoaded()
149{
150    // Call setDockingUnavailable before bringToFront. If we display the inspector window via bringToFront first it causes
151    // the call to canAttachWindow to return the wrong result on Windows.
152    // Calling bringToFront first causes the visibleHeight of the inspected page to always return 0 immediately after.
153    // Thus if we call canAttachWindow first we can avoid this problem. This change does not cause any regressions on Mac.
154    setDockingUnavailable(!canAttachWindow());
155    bringToFront();
156    m_frontendLoaded = true;
157    for (Vector<String>::iterator it = m_evaluateOnLoad.begin(); it != m_evaluateOnLoad.end(); ++it)
158        evaluateOnLoad(*it);
159    m_evaluateOnLoad.clear();
160}
161
162void InspectorFrontendClientLocal::requestSetDockSide(DockSide dockSide)
163{
164    if (dockSide == UNDOCKED) {
165        detachWindow();
166        setAttachedWindow(dockSide);
167    } else if (canAttachWindow()) {
168        attachWindow(dockSide);
169        setAttachedWindow(dockSide);
170    }
171}
172
173bool InspectorFrontendClientLocal::canAttachWindow()
174{
175    // Don't allow attaching to another inspector -- two inspectors in one window is too much!
176    bool isInspectorPage = m_inspectorController->hasInspectorFrontendClient();
177    if (isInspectorPage)
178        return false;
179
180    // If we are already attached, allow attaching again to allow switching sides.
181    if (m_dockSide != UNDOCKED)
182        return true;
183
184    // Don't allow the attach if the window would be too small to accommodate the minimum inspector size.
185    unsigned inspectedPageHeight = m_inspectorController->inspectedPage().mainFrame().view()->visibleHeight();
186    unsigned inspectedPageWidth = m_inspectorController->inspectedPage().mainFrame().view()->visibleWidth();
187    unsigned maximumAttachedHeight = inspectedPageHeight * maximumAttachedHeightRatio;
188    return minimumAttachedHeight <= maximumAttachedHeight && minimumAttachedWidth <= inspectedPageWidth;
189}
190
191void InspectorFrontendClientLocal::setDockingUnavailable(bool unavailable)
192{
193    evaluateOnLoad(String::format("[\"setDockingUnavailable\", %s]", unavailable ? "true" : "false"));
194}
195
196void InspectorFrontendClientLocal::changeAttachedWindowHeight(unsigned height)
197{
198    unsigned totalHeight = m_frontendPage->mainFrame().view()->visibleHeight() + m_inspectorController->inspectedPage().mainFrame().view()->visibleHeight();
199    unsigned attachedHeight = constrainedAttachedWindowHeight(height, totalHeight);
200    m_settings->setProperty(inspectorAttachedHeightSetting, String::number(attachedHeight));
201    setAttachedWindowHeight(attachedHeight);
202}
203
204void InspectorFrontendClientLocal::changeAttachedWindowWidth(unsigned width)
205{
206    unsigned totalWidth = m_frontendPage->mainFrame().view()->visibleWidth() + m_inspectorController->inspectedPage().mainFrame().view()->visibleWidth();
207    unsigned attachedWidth = constrainedAttachedWindowWidth(width, totalWidth);
208    setAttachedWindowWidth(attachedWidth);
209}
210
211void InspectorFrontendClientLocal::openInNewTab(const String& url)
212{
213    UserGestureIndicator indicator(DefinitelyProcessingUserGesture);
214    Frame& mainFrame = m_inspectorController->inspectedPage().mainFrame();
215    FrameLoadRequest request(mainFrame.document()->securityOrigin(), ResourceRequest(), "_blank");
216
217    bool created;
218    WindowFeatures windowFeatures;
219    RefPtr<Frame> frame = WebCore::createWindow(&mainFrame, &mainFrame, request, windowFeatures, created);
220    if (!frame)
221        return;
222
223    frame->loader().setOpener(&mainFrame);
224    frame->page()->setOpenedByDOM();
225
226    // FIXME: Why does one use mainFrame and the other frame?
227    frame->loader().changeLocation(mainFrame.document()->securityOrigin(), frame->document()->completeURL(url), "", LockHistory::No, LockBackForwardList::No);
228}
229
230void InspectorFrontendClientLocal::moveWindowBy(float x, float y)
231{
232    FloatRect frameRect = m_frontendPage->chrome().windowRect();
233    frameRect.move(x, y);
234    m_frontendPage->chrome().setWindowRect(frameRect);
235}
236
237void InspectorFrontendClientLocal::setAttachedWindow(DockSide dockSide)
238{
239    const char* side = "undocked";
240    switch (dockSide) {
241    case UNDOCKED:
242        side = "undocked";
243        break;
244    case DOCKED_TO_RIGHT:
245        side = "right";
246        break;
247    case DOCKED_TO_BOTTOM:
248        side = "bottom";
249        break;
250    }
251
252    m_dockSide = dockSide;
253
254    evaluateOnLoad(String::format("[\"setDockSide\", \"%s\"]", side));
255}
256
257void InspectorFrontendClientLocal::restoreAttachedWindowHeight()
258{
259    unsigned inspectedPageHeight = m_inspectorController->inspectedPage().mainFrame().view()->visibleHeight();
260    String value = m_settings->getProperty(inspectorAttachedHeightSetting);
261    unsigned preferredHeight = value.isEmpty() ? defaultAttachedHeight : value.toUInt();
262
263    // This call might not go through (if the window starts out detached), but if the window is initially created attached,
264    // InspectorController::attachWindow is never called, so we need to make sure to set the attachedWindowHeight.
265    // FIXME: Clean up code so we only have to call setAttachedWindowHeight in InspectorController::attachWindow
266    setAttachedWindowHeight(constrainedAttachedWindowHeight(preferredHeight, inspectedPageHeight));
267}
268
269bool InspectorFrontendClientLocal::isDebuggingEnabled()
270{
271    if (m_frontendLoaded)
272        return evaluateAsBoolean("[\"isDebuggingEnabled\"]");
273    return false;
274}
275
276void InspectorFrontendClientLocal::setDebuggingEnabled(bool enabled)
277{
278    evaluateOnLoad(String::format("[\"setDebuggingEnabled\", %s]", enabled ? "true" : "false"));
279}
280
281bool InspectorFrontendClientLocal::isTimelineProfilingEnabled()
282{
283    if (m_frontendLoaded)
284        return evaluateAsBoolean("[\"isTimelineProfilingEnabled\"]");
285    return false;
286}
287
288void InspectorFrontendClientLocal::setTimelineProfilingEnabled(bool enabled)
289{
290    evaluateOnLoad(String::format("[\"setTimelineProfilingEnabled\", %s]", enabled ? "true" : "false"));
291}
292
293bool InspectorFrontendClientLocal::isProfilingJavaScript()
294{
295    if (m_frontendLoaded)
296        return evaluateAsBoolean("[\"isProfilingJavaScript\"]");
297    return false;
298}
299
300void InspectorFrontendClientLocal::startProfilingJavaScript()
301{
302    evaluateOnLoad("[\"startProfilingJavaScript\"]");
303}
304
305void InspectorFrontendClientLocal::stopProfilingJavaScript()
306{
307    evaluateOnLoad("[\"stopProfilingJavaScript\"]");
308}
309
310void InspectorFrontendClientLocal::showConsole()
311{
312    evaluateOnLoad("[\"showConsole\"]");
313}
314
315void InspectorFrontendClientLocal::showResources()
316{
317    evaluateOnLoad("[\"showResources\"]");
318}
319
320void InspectorFrontendClientLocal::showMainResourceForFrame(Frame* frame)
321{
322    String frameId = m_inspectorController->pageAgent()->frameId(frame);
323    evaluateOnLoad(String::format("[\"showMainResourceForFrame\", \"%s\"]", frameId.ascii().data()));
324}
325
326unsigned InspectorFrontendClientLocal::constrainedAttachedWindowHeight(unsigned preferredHeight, unsigned totalWindowHeight)
327{
328    return roundf(std::max(minimumAttachedHeight, std::min<float>(preferredHeight, totalWindowHeight * maximumAttachedHeightRatio)));
329}
330
331unsigned InspectorFrontendClientLocal::constrainedAttachedWindowWidth(unsigned preferredWidth, unsigned totalWindowWidth)
332{
333    return roundf(std::max(minimumAttachedWidth, std::min<float>(preferredWidth, totalWindowWidth - minimumAttachedInspectedWidth)));
334}
335
336void InspectorFrontendClientLocal::sendMessageToBackend(const String& message)
337{
338    m_dispatchTask->dispatch(message);
339}
340
341bool InspectorFrontendClientLocal::isUnderTest()
342{
343    return m_inspectorController->isUnderTest();
344}
345
346bool InspectorFrontendClientLocal::evaluateAsBoolean(const String& expression)
347{
348    Deprecated::ScriptValue value = m_frontendPage->mainFrame().script().executeScript(expression);
349    return value.toString(mainWorldExecState(&m_frontendPage->mainFrame())) == "true";
350}
351
352void InspectorFrontendClientLocal::evaluateOnLoad(const String& expression)
353{
354    if (m_frontendLoaded)
355        m_frontendPage->mainFrame().script().executeScript("InspectorFrontendAPI.dispatch(" + expression + ")");
356    else
357        m_evaluateOnLoad.append(expression);
358}
359
360} // namespace WebCore
361
362#endif
363