RemoteInspectorProtocolHandler.cpp   [plain text]


/*
 * Copyright (C) 2019 Sony Interactive Entertainment Inc.
 *
 * 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 "RemoteInspectorProtocolHandler.h"

#if ENABLE(REMOTE_INSPECTOR)

#include "APIContentWorld.h"
#include "APILoaderClient.h"
#include "APINavigation.h"
#include "WebPageGroup.h"
#include "WebPageProxy.h"
#include "WebScriptMessageHandler.h"
#include "WebUserContentControllerProxy.h"
#include <WebCore/JSDOMExceptionHandling.h>
#include <WebCore/SerializedScriptValue.h>
#include <wtf/URL.h>
#include <wtf/text/StringBuilder.h>

namespace WebKit {

using namespace WebCore;

class ScriptMessageClient final : public WebScriptMessageHandler::Client {
    WTF_MAKE_FAST_ALLOCATED;
public:
    ScriptMessageClient(RemoteInspectorProtocolHandler& inspectorProtocolHandler)
        : m_inspectorProtocolHandler(inspectorProtocolHandler) { }

    ~ScriptMessageClient() { }

    void didPostMessage(WebPageProxy& page, FrameInfoData&&, API::ContentWorld&, WebCore::SerializedScriptValue& serializedScriptValue) override
    {
        auto tokens = serializedScriptValue.toString().split(":");
        if (tokens.size() != 3)
            return;

        URL requestURL { { }, page.pageLoadState().url() };
        m_inspectorProtocolHandler.inspect(requestURL.hostAndPort(), tokens[0].toUIntStrict(), tokens[1].toUIntStrict(), tokens[2]);
    }
    
    bool supportsAsyncReply() override
    {
        return false;
    }
    
    void didPostMessageWithAsyncReply(WebPageProxy&, FrameInfoData&&, API::ContentWorld&, WebCore::SerializedScriptValue&, WTF::Function<void(API::SerializedScriptValue*, const String&)>&&) override
    {
    }

private:
    RemoteInspectorProtocolHandler& m_inspectorProtocolHandler;
};

class LoaderClient final : public API::LoaderClient {
    WTF_MAKE_FAST_ALLOCATED;
public:
    LoaderClient(Function<void()>&& loadedCallback)
        : m_loadedCallback { WTFMove(loadedCallback) } { }

    void didFinishLoadForFrame(WebKit::WebPageProxy&, WebKit::WebFrameProxy&, API::Navigation*, API::Object*) final
    {
        m_loadedCallback();
    }

private:
    Function<void()> m_loadedCallback;
};

static Optional<Inspector::DebuggableType> parseDebuggableTypeFromString(const String& debuggableTypeString)
{
    if (debuggableTypeString == "itml"_s)
        return Inspector::DebuggableType::ITML;
    if (debuggableTypeString == "javascript"_s)
        return Inspector::DebuggableType::JavaScript;
    if (debuggableTypeString == "page"_s)
        return Inspector::DebuggableType::Page;
    if (debuggableTypeString == "service-worker"_s)
        return Inspector::DebuggableType::ServiceWorker;
    if (debuggableTypeString == "web-page"_s)
        return Inspector::DebuggableType::WebPage;

    return WTF::nullopt;
}

void RemoteInspectorProtocolHandler::inspect(const String& hostAndPort, ConnectionID connectionID, TargetID targetID, const String& type)
{
    auto debuggableType = parseDebuggableTypeFromString(type);
    if (!debuggableType) {
        LOG_ERROR("Unknown debuggable type: \"%s\"", type.utf8().data());
        return;
    }

    if (m_inspectorClient)
        m_inspectorClient->inspect(connectionID, targetID, debuggableType.value());
}

void RemoteInspectorProtocolHandler::runScript(const String& script)
{
    m_page.runJavaScriptInMainFrame({ script, URL { }, false, WTF::nullopt, false }, 
        [] (auto&& result) {
        if (!result.has_value())
            LOG_ERROR("Exception running script \"%s\"", result.error().message.utf8().data());
    });
}

void RemoteInspectorProtocolHandler::targetListChanged(RemoteInspectorClient& client)
{
    StringBuilder html;
    if (client.targets().isEmpty())
        html.append("<p>No targets found</p>"_s);
    else {
        html.append("<table>");
        for (auto& connectionID : client.targets().keys()) {
            for (auto& target : client.targets().get(connectionID)) {
                html.append(makeString(
                    "<tbody><tr>"
                    "<td class=\"data\"><div class=\"targetname\">", target.name, "</div><div class=\"targeturl\">", target.url, "</div></td>"
                    "<td class=\"input\"><input type=\"button\" value=\"Inspect\" onclick=\"window.webkit.messageHandlers.inspector.postMessage(\\'", connectionID, ":", target.id, ":", target.type, "\\');\"></td>"
                    "</tr></tbody>"
                ));
            }
        }
        html.append("</table>");
    }
    m_targetListsHtml = html.toString();
    if (m_pageLoaded)
        updateTargetList();
}

void RemoteInspectorProtocolHandler::updateTargetList()
{
    if (!m_targetListsHtml.isEmpty()) {
        runScript(makeString("updateTargets(`", m_targetListsHtml, "`);"));
        m_targetListsHtml = { };
    }
}

void RemoteInspectorProtocolHandler::platformStartTask(WebPageProxy& pageProxy, WebURLSchemeTask& task)
{
    auto& requestURL = task.request().url();

    // Destroy the client before creating a new connection so it can connect to the same port
    m_inspectorClient = nullptr;
    m_inspectorClient = makeUnique<RemoteInspectorClient>(requestURL, *this);

    // Setup target postMessage listener
    auto handler = WebScriptMessageHandler::create(makeUnique<ScriptMessageClient>(*this), "inspector", API::ContentWorld::pageContentWorld());
    pageProxy.pageGroup().userContentController().addUserScriptMessageHandler(handler.get());

    // Setup loader client to get notified of page load
    m_page.setLoaderClient(makeUnique<LoaderClient>([this] {
        m_pageLoaded = true;
        updateTargetList();
    }));
    m_pageLoaded = false;

    StringBuilder htmlBuilder;
    htmlBuilder.append(
        "<html><head><title>Remote Inspector</title>"
        "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />"
        "<style>"
        "  h1 { color: #babdb6; text-shadow: 0 1px 0 white; margin-bottom: 0; }"
        "  html { font-family: -webkit-system-font; font-size: 11pt; color: #2e3436; padding: 20px 20px 0 20px; background-color: #f6f6f4; "
        "         background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #eeeeec), color-stop(1, #f6f6f4));"
        "         background-size: 100% 5em; background-repeat: no-repeat; }"
        "  table { width: 100%; border-collapse: collapse; table-layout: fixed; }"
        "  table, td { border: 1px solid #d3d7cf; border-left: none; border-right: none; }"
        "  p { margin-bottom: 30px; }"
        "  td { padding: 15px; }"
        "  td.data { width: 200px; }"
        "  .targetname { font-weight: bold; overflow: hidden; white-space:nowrap; text-overflow: ellipsis; }"
        "  .targeturl { color: #babdb6; background: #eee; word-wrap: break-word; overflow-wrap: break-word; }"
        "  td.input { width: 64px; }"
        "  input { width: 100%; padding: 8px; }"
        "</style>"
        "</head><body><h1>Inspectable targets</h1>"
        "<div id=\"targetlist\"><p>No targets found</p></div></body>"
        "<script>"
        "function updateTargets(str) {"
            "let targetDiv = document.getElementById('targetlist');"
            "targetDiv.innerHTML = str;"
        "}"
        "</script>");
    htmlBuilder.append("</html>");

    auto html = htmlBuilder.toString().utf8();
    auto data = SharedBuffer::create(html.data(), html.length());
    ResourceResponse response(requestURL, "text/html"_s, html.length(), "UTF-8"_s);
    task.didReceiveResponse(response);
    task.didReceiveData(WTFMove(data));
    task.didComplete(ResourceError());
}

} // namespace WebKit

#endif // ENABLE(REMOTE_INSPECTOR)