Web Inspector: provide a way to edit the user agent of a remote target
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 26 Jan 2019 22:32:38 +0000 (22:32 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 26 Jan 2019 22:32:38 +0000 (22:32 +0000)
https://bugs.webkit.org/show_bug.cgi?id=193862
<rdar://problem/47359292>

Reviewed by Joseph Pecoraro.

Source/JavaScriptCore:

* inspector/protocol/Page.json:
Add `overrideUserAgent` command.

Source/WebCore:

Test: inspector/page/overrideUserAgent.html

* loader/FrameLoader.cpp:
(WebCore::FrameLoader::userAgent const):
(WebCore::FrameLoader::userAgentForJavaScript const):

* inspector/InspectorInstrumentation.h:
(WebCore::InspectorInstrumentation::applyUserAgentOverride): Added.
* inspector/InspectorInstrumentation.cpp:
(WebCore::InspectorInstrumentation::applyUserAgentOverrideImpl): Added.

* inspector/agents/InspectorPageAgent.h:
* inspector/agents/InspectorPageAgent.cpp:
(WebCore::InspectorPageAgent::disable):
(WebCore::InspectorPageAgent::overrideUserAgent): Added.
(WebCore::InspectorPageAgent::applyUserAgentOverride): Added.

Source/WebInspectorUI:

* UserInterface/Base/Main.js:
(WI.loaded):
(WI.contentLoaded):
(WI.initializeTarget):
(WI._handleDeviceSettingsToolbarButtonClicked):
(WI._handleDeviceSettingsToolbarButtonClicked.updateActivatedState):
(WI._handleDeviceSettingsToolbarButtonClicked.applyOverriddenUserAgent):
(WI._handleDeviceSettingsToolbarButtonClicked.applyOverriddenSetting):
(WI._handleDeviceSettingsToolbarButtonClicked.createContainer):
(WI._handleDeviceSettingsToolbarButtonClicked.createColumns):
(WI._handleDeviceSettingsToolbarButtonClicked.calculateTargetFrame):
(WI._handleDeviceSettingsToolbarButtonClicked.showUserAgentInput):

* UserInterface/Views/Main.css:
(.device-settings-content):
(.device-settings-content .user-agent-value): Added.
(.device-settings-content .user-agent-value > select): Added.
(.device-settings-content .user-agent-value > input): Added.
(body[dir=ltr] .device-settings-content .user-agent-value > input): Added.
(body[dir=rtl] .device-settings-content .user-agent-value > input): Added.
(.device-settings-content label > input): Added.
(body[dir=ltr] .device-settings-content label > input): Deleted.
(body[dir=rtl] .device-settings-content label > input): Deleted.

* Localizations/en.lproj/localizedStrings.js:

LayoutTests:

* inspector/page/overrideUserAgent.html: Added.
* inspector/page/overrideUserAgent-expected.txt: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@240549 268f45cc-cd09-0410-ab3c-d52691b4dbfc

15 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/page/overrideUserAgent-expected.txt [new file with mode: 0644]
LayoutTests/inspector/page/overrideUserAgent.html [new file with mode: 0644]
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/protocol/Page.json
Source/WebCore/ChangeLog
Source/WebCore/inspector/InspectorInstrumentation.cpp
Source/WebCore/inspector/InspectorInstrumentation.h
Source/WebCore/inspector/agents/InspectorPageAgent.cpp
Source/WebCore/inspector/agents/InspectorPageAgent.h
Source/WebCore/loader/FrameLoader.cpp
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/Main.js
Source/WebInspectorUI/UserInterface/Views/Main.css

index 235a687..39ae751 100644 (file)
@@ -1,3 +1,14 @@
+2019-01-26  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: provide a way to edit the user agent of a remote target
+        https://bugs.webkit.org/show_bug.cgi?id=193862
+        <rdar://problem/47359292>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/page/overrideUserAgent.html: Added.
+        * inspector/page/overrideUserAgent-expected.txt: Added.
+
 2019-01-26  Simon Fraser  <simon.fraser@apple.com>
 
         Move scrolling-tree/fixed-inside-frame.html into scrolling tree tests
diff --git a/LayoutTests/inspector/page/overrideUserAgent-expected.txt b/LayoutTests/inspector/page/overrideUserAgent-expected.txt
new file mode 100644 (file)
index 0000000..57f0bef
--- /dev/null
@@ -0,0 +1,13 @@
+Tests for the Page.overrideUserAgent command.
+
+
+== Running test suite: Page.overrideUserAgent
+-- Running test case: Page.overrideUserAgent.Custom
+PASS: There should be a default UserAgent.
+Overriding UserAgent...
+Reloading...
+PASS: The UserAgent should have changed.
+Removing UserAgent override...
+Reloading...
+PASS: The UserAgent should be back to its default value.
+
diff --git a/LayoutTests/inspector/page/overrideUserAgent.html b/LayoutTests/inspector/page/overrideUserAgent.html
new file mode 100644 (file)
index 0000000..8254244
--- /dev/null
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+
+function test()
+{
+    let suite = InspectorTest.createAsyncSuite("Page.overrideUserAgent");
+
+    suite.addTestCase({
+        name: "Page.overrideUserAgent.Custom",
+        description: "Test that author/user styles aren't applied when that setting is overridden.",
+        async test() {
+            const testUserAgent = "TEST USER AGENT";
+
+            let defaultUserAgent = await InspectorTest.evaluateInPage(`navigator.userAgent`);
+            InspectorTest.expectGreaterThan(defaultUserAgent.length, 0, "There should be a default UserAgent.");
+
+            InspectorTest.log("Overriding UserAgent...");
+            await PageAgent.overrideUserAgent(testUserAgent);
+
+            InspectorTest.log("Reloading...");
+            await InspectorTest.reloadPage();
+            await InspectorTest.awaitEvent(FrontendTestHarness.Event.TestPageDidLoad);
+
+            let overriddenUserAgent = await InspectorTest.evaluateInPage(`navigator.userAgent`);
+            InspectorTest.expectEqual(overriddenUserAgent, testUserAgent, "The UserAgent should have changed.");
+
+            InspectorTest.log("Removing UserAgent override...");
+            await PageAgent.overrideUserAgent();
+
+            InspectorTest.log("Reloading...");
+            await InspectorTest.reloadPage();
+            await InspectorTest.awaitEvent(FrontendTestHarness.Event.TestPageDidLoad);
+
+            let originalUserAgent = await InspectorTest.evaluateInPage(`navigator.userAgent`);
+            InspectorTest.expectEqual(originalUserAgent, defaultUserAgent, "The UserAgent should be back to its default value.");
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+
+</script>
+</head>
+<body onload="runTest()">
+    <p>Tests for the Page.overrideUserAgent command.</p>
+</body>
+</html>
index 3ac1016..64b553e 100644 (file)
@@ -1,3 +1,14 @@
+2019-01-26  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: provide a way to edit the user agent of a remote target
+        https://bugs.webkit.org/show_bug.cgi?id=193862
+        <rdar://problem/47359292>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/protocol/Page.json:
+        Add `overrideUserAgent` command.
+
 2019-01-25  Yusuke Suzuki  <ysuzuki@apple.com>
 
         [JSC] NativeErrorConstructor should not have own IsoSubspace
index 7dba156..7142a0e 100644 (file)
             ]
         },
         {
+            "name": "overrideUserAgent",
+            "description": "Override's the user agent of the inspected page",
+            "parameters": [
+                { "name": "value", "type": "string", "optional": true, "description": "Value to override the user agent with. If this value is not provided, the override is removed. Overrides are removed when Web Inspector closes/disconnects." }
+            ]
+        },
+        {
             "name": "overrideSetting",
             "description": "Allows the frontend to override the inspected page's settings.",
             "parameters": [
index f809958..6bd8de6 100644 (file)
@@ -1,3 +1,28 @@
+2019-01-26  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: provide a way to edit the user agent of a remote target
+        https://bugs.webkit.org/show_bug.cgi?id=193862
+        <rdar://problem/47359292>
+
+        Reviewed by Joseph Pecoraro.
+
+        Test: inspector/page/overrideUserAgent.html
+
+        * loader/FrameLoader.cpp:
+        (WebCore::FrameLoader::userAgent const):
+        (WebCore::FrameLoader::userAgentForJavaScript const):
+
+        * inspector/InspectorInstrumentation.h:
+        (WebCore::InspectorInstrumentation::applyUserAgentOverride): Added.
+        * inspector/InspectorInstrumentation.cpp:
+        (WebCore::InspectorInstrumentation::applyUserAgentOverrideImpl): Added.
+
+        * inspector/agents/InspectorPageAgent.h:
+        * inspector/agents/InspectorPageAgent.cpp:
+        (WebCore::InspectorPageAgent::disable):
+        (WebCore::InspectorPageAgent::overrideUserAgent): Added.
+        (WebCore::InspectorPageAgent::applyUserAgentOverride): Added.
+
 2019-01-26  Zalan Bujtas  <zalan@apple.com>
 
         [LFC] The initial values for top/bottom in contentHeightForFormattingContextRoot should not be 0.
index b50303c..2b43062 100644 (file)
@@ -568,6 +568,12 @@ void InspectorInstrumentation::didScheduleStyleRecalculationImpl(InstrumentingAg
         networkAgent->didScheduleStyleRecalculation(document);
 }
 
+void InspectorInstrumentation::applyUserAgentOverrideImpl(InstrumentingAgents& instrumentingAgents, String& userAgent)
+{
+    if (InspectorPageAgent* pageAgent = instrumentingAgents.inspectorPageAgent())
+        pageAgent->applyUserAgentOverride(userAgent);
+}
+
 void InspectorInstrumentation::applyEmulatedMediaImpl(InstrumentingAgents& instrumentingAgents, String& media)
 {
     if (InspectorPageAgent* pageAgent = instrumentingAgents.inspectorPageAgent())
index d559c86..efe265a 100644 (file)
@@ -173,6 +173,7 @@ public:
     static InspectorInstrumentationCookie willRecalculateStyle(Document&);
     static void didRecalculateStyle(const InspectorInstrumentationCookie&);
     static void didScheduleStyleRecalculation(Document&);
+    static void applyUserAgentOverride(Frame&, String&);
     static void applyEmulatedMedia(Frame&, String&);
 
     static void willSendRequest(Frame*, unsigned long identifier, DocumentLoader*, ResourceRequest&, const ResourceResponse& redirectResponse);
@@ -365,6 +366,7 @@ private:
     static InspectorInstrumentationCookie willRecalculateStyleImpl(InstrumentingAgents&, Document&);
     static void didRecalculateStyleImpl(const InspectorInstrumentationCookie&);
     static void didScheduleStyleRecalculationImpl(InstrumentingAgents&, Document&);
+    static void applyUserAgentOverrideImpl(InstrumentingAgents&, String&);
     static void applyEmulatedMediaImpl(InstrumentingAgents&, String&);
 
     static void willSendRequestImpl(InstrumentingAgents&, unsigned long identifier, DocumentLoader*, ResourceRequest&, const ResourceResponse& redirectResponse);
@@ -947,6 +949,13 @@ inline void InspectorInstrumentation::didScheduleStyleRecalculation(Document& do
         didScheduleStyleRecalculationImpl(*instrumentingAgents, document);
 }
 
+inline void InspectorInstrumentation::applyUserAgentOverride(Frame& frame, String& userAgent)
+{
+    FAST_RETURN_IF_NO_FRONTENDS(void());
+    if (InstrumentingAgents* instrumentingAgents = instrumentingAgentsForFrame(frame))
+        applyUserAgentOverrideImpl(*instrumentingAgents, userAgent);
+}
+
 inline void InspectorInstrumentation::applyEmulatedMedia(Frame& frame, String& media)
 {
     FAST_RETURN_IF_NO_FRONTENDS(void());
index 8dd8491..6cd1fd7 100644 (file)
@@ -320,6 +320,7 @@ void InspectorPageAgent::disable(ErrorString&)
 
     ErrorString unused;
     setShowPaintRects(unused, false);
+    overrideUserAgent(unused, nullptr);
     setEmulatedMedia(unused, emptyString());
     setForcedAppearance(unused, emptyString());
 
@@ -355,6 +356,11 @@ void InspectorPageAgent::navigate(ErrorString&, const String& url)
     frame.loader().changeLocation(WTFMove(frameLoadRequest));
 }
 
+void InspectorPageAgent::overrideUserAgent(ErrorString&, const String* value)
+{
+    m_userAgentOverride = value ? *value : String();
+}
+
 void InspectorPageAgent::overrideSetting(ErrorString& errorString, const String& settingString, const bool* value)
 {
     if (settingString.isEmpty()) {
@@ -880,6 +886,12 @@ void InspectorPageAgent::setForcedAppearance(ErrorString&, const String& appeara
         m_page.setUseDarkAppearanceOverride(WTF::nullopt);
 }
 
+void InspectorPageAgent::applyUserAgentOverride(String& userAgent)
+{
+    if (!m_userAgentOverride.isEmpty())
+        userAgent = m_userAgentOverride;
+}
+
 void InspectorPageAgent::applyEmulatedMedia(String& media)
 {
     if (!m_emulatedMedia.isEmpty())
index 41b7e07..9bfa2bb 100644 (file)
@@ -90,6 +90,7 @@ public:
     void disable(ErrorString&) final;
     void reload(ErrorString&, const bool* optionalReloadFromOrigin, const bool* optionalRevalidateAllResources) final;
     void navigate(ErrorString&, const String& url) final;
+    void overrideUserAgent(ErrorString&, const String* value) final;
     void overrideSetting(ErrorString&, const String& setting, const bool* value) final;
     void getCookies(ErrorString&, RefPtr<JSON::ArrayOf<Inspector::Protocol::Page::Cookie>>& cookies) final;
     void deleteCookie(ErrorString&, const String& cookieName, const String& url) final;
@@ -118,6 +119,7 @@ public:
     void frameScheduledNavigation(Frame&, Seconds delay);
     void frameClearedScheduledNavigation(Frame&);
     void defaultAppearanceDidChange(bool useDarkAppearance);
+    void applyUserAgentOverride(String&);
     void applyEmulatedMedia(String&);
     void didPaint(RenderObject&, const LayoutRect&);
     void didLayout();
@@ -161,6 +163,7 @@ private:
     bool m_enabled { false };
     bool m_isFirstLayoutAfterOnLoad { false };
     bool m_showPaintRects { false };
+    String m_userAgentOverride;
     String m_emulatedMedia;
     String m_forcedAppearance;
 };
index 43d7c64..fb8cbad 100644 (file)
@@ -2697,26 +2697,34 @@ int FrameLoader::numPendingOrLoadingRequests(bool recurse) const
 
 String FrameLoader::userAgent(const URL& url) const
 {
-    if (auto* documentLoader = m_frame.mainFrame().loader().activeDocumentLoader()) {
-        auto& customUserAgent = documentLoader->customUserAgent();
-        if (!customUserAgent.isEmpty())
-            return customUserAgent;
-    }
+    String userAgent;
+
+    if (auto* documentLoader = m_frame.mainFrame().loader().activeDocumentLoader())
+        userAgent = documentLoader->customUserAgent();
+
+    InspectorInstrumentation::applyUserAgentOverride(m_frame, userAgent);
+
+    if (!userAgent.isEmpty())
+        return userAgent;
 
     return m_client.userAgent(url);
 }
 
 String FrameLoader::userAgentForJavaScript(const URL& url) const
 {
+    String userAgent;
+
     if (auto* documentLoader = m_frame.mainFrame().loader().activeDocumentLoader()) {
-        auto& customJavaScriptUserAgent = documentLoader->customJavaScriptUserAgent();
-        if (!customJavaScriptUserAgent.isEmpty())
-            return customJavaScriptUserAgent;
-        auto& customUserAgent = documentLoader->customUserAgent();
-        if (!customUserAgent.isEmpty())
-            return customUserAgent;
+        userAgent = documentLoader->customJavaScriptUserAgent();
+        if (userAgent.isEmpty())
+            userAgent = documentLoader->customUserAgent();
     }
 
+    InspectorInstrumentation::applyUserAgentOverride(m_frame, userAgent);
+
+    if (!userAgent.isEmpty())
+        return userAgent;
+
     return m_client.userAgent(url);
 }
     
index 3810121..d44512a 100644 (file)
@@ -1,3 +1,37 @@
+2019-01-26  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: provide a way to edit the user agent of a remote target
+        https://bugs.webkit.org/show_bug.cgi?id=193862
+        <rdar://problem/47359292>
+
+        Reviewed by Joseph Pecoraro.
+
+        * UserInterface/Base/Main.js:
+        (WI.loaded):
+        (WI.contentLoaded):
+        (WI.initializeTarget):
+        (WI._handleDeviceSettingsToolbarButtonClicked):
+        (WI._handleDeviceSettingsToolbarButtonClicked.updateActivatedState):
+        (WI._handleDeviceSettingsToolbarButtonClicked.applyOverriddenUserAgent):
+        (WI._handleDeviceSettingsToolbarButtonClicked.applyOverriddenSetting):
+        (WI._handleDeviceSettingsToolbarButtonClicked.createContainer):
+        (WI._handleDeviceSettingsToolbarButtonClicked.createColumns):
+        (WI._handleDeviceSettingsToolbarButtonClicked.calculateTargetFrame):
+        (WI._handleDeviceSettingsToolbarButtonClicked.showUserAgentInput):
+
+        * UserInterface/Views/Main.css:
+        (.device-settings-content):
+        (.device-settings-content .user-agent-value): Added.
+        (.device-settings-content .user-agent-value > select): Added.
+        (.device-settings-content .user-agent-value > input): Added.
+        (body[dir=ltr] .device-settings-content .user-agent-value > input): Added.
+        (body[dir=rtl] .device-settings-content .user-agent-value > input): Added.
+        (.device-settings-content label > input): Added.
+        (body[dir=ltr] .device-settings-content label > input): Deleted.
+        (body[dir=rtl] .device-settings-content label > input): Deleted.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
 2019-01-25  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: provide a way to edit page settings on a remote target
index 7a42d89..bf92baf 100644 (file)
@@ -289,6 +289,7 @@ localizedStrings["Debugger disabled during Timeline recording"] = "Debugger disa
 localizedStrings["Debugger:"] = "Debugger:";
 localizedStrings["Debugs"] = "Debugs";
 localizedStrings["Decoded"] = "Decoded";
+localizedStrings["Default"] = "Default";
 localizedStrings["Delete"] = "Delete";
 localizedStrings["Delete Breakpoint"] = "Delete Breakpoint";
 localizedStrings["Delete Breakpoints"] = "Delete Breakpoints";
@@ -665,6 +666,7 @@ localizedStrings["Originally %s"] = "Originally %s";
 localizedStrings["Originator"] = "Originator";
 localizedStrings["Other"] = "Other";
 localizedStrings["Other Issue"] = "Other Issue";
+localizedStrings["Other\u2026"] = "Other\u2026";
 localizedStrings["Outgoing message"] = "Outgoing message";
 localizedStrings["Output: "] = "Output: ";
 localizedStrings["Over 1 ms"] = "Over 1 ms";
@@ -1019,6 +1021,7 @@ localizedStrings["Use Default Media Styles"] = "Use Default Media Styles";
 localizedStrings["Use the resource cache when loading resources"] = "Use the resource cache when loading resources";
 localizedStrings["User Agent"] = "User Agent";
 localizedStrings["User Agent Stylesheet"] = "User Agent Stylesheet";
+localizedStrings["User Agent:"] = "User Agent:";
 localizedStrings["User Interface:"] = "User Interface:";
 localizedStrings["User Stylesheet"] = "User Stylesheet";
 localizedStrings["Valid From"] = "Valid From";
index c61f937..3e0e136 100644 (file)
@@ -161,7 +161,8 @@ WI.loaded = function()
     this.visible = false;
     this._windowKeydownListeners = [];
     this._targetsAvailablePromise = new WI.WrappedPromise;
-    this._overridenDeviceSettings = new Set;
+    WI._overridenDeviceUserAgent = null;
+    WI._overridenDeviceSettings = new Set;
 
     // Targets.
     WI.backendTarget = null;
@@ -447,12 +448,12 @@ WI.contentLoaded = function()
     this._inspectModeToolbarButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleInspectMode, this);
 
     // COMPATIBILITY (iOS 12.2): Page.overrideSetting did not exist.
-    if (InspectorFrontendHost.isRemote && WI.sharedApp.debuggableType === WI.DebuggableType.Web && InspectorBackend.domains.Page && InspectorBackend.domains.Page.overrideSetting) {
+    if (InspectorFrontendHost.isRemote && WI.sharedApp.debuggableType === WI.DebuggableType.Web && InspectorBackend.domains.Page && InspectorBackend.domains.Page.overrideUserAgent && InspectorBackend.domains.Page.overrideSetting) {
         const deviceSettingsTooltip = WI.UIString("Device Settings");
-        this._deviceSettingsToolbarButton = new WI.ActivateButtonToolbarItem("device-settings", deviceSettingsTooltip, deviceSettingsTooltip, "Images/Device.svg");
-        this._deviceSettingsToolbarButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleDeviceSettingsToolbarButtonClicked, this);
+        WI._deviceSettingsToolbarButton = new WI.ActivateButtonToolbarItem("device-settings", deviceSettingsTooltip, deviceSettingsTooltip, "Images/Device.svg");
+        WI._deviceSettingsToolbarButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleDeviceSettingsToolbarButtonClicked, this);
 
-        this._deviceSettingsPopover = null;
+        WI._deviceSettingsPopover = null;
     }
 
     this._updateReloadToolbarButton();
@@ -480,8 +481,8 @@ WI.contentLoaded = function()
 
     this.toolbar.addToolbarItem(this._inspectModeToolbarButton, WI.Toolbar.Section.CenterRight);
 
-    if (this._deviceSettingsToolbarButton)
-        this.toolbar.addToolbarItem(this._deviceSettingsToolbarButton, WI.Toolbar.Section.CenterRight);
+    if (WI._deviceSettingsToolbarButton)
+        this.toolbar.addToolbarItem(WI._deviceSettingsToolbarButton, WI.Toolbar.Section.CenterRight);
 
     this._searchTabContentView = new WI.SearchTabContentView;
 
@@ -605,9 +606,13 @@ WI.performOneTimeFrontendInitializationsUsingTarget = function(target)
 WI.initializeTarget = function(target)
 {
     if (target.PageAgent) {
+        // COMPATIBILITY (iOS 12.2): Page.overrideUserAgent did not exist.
+        if (target.PageAgent.overrideUserAgent && WI._overridenDeviceUserAgent)
+            target.PageAgent.overrideUserAgent(WI._overridenDeviceUserAgent);
+
         // COMPATIBILITY (iOS 12.2): Page.overrideSetting did not exist.
         if (target.PageAgent.overrideSetting) {
-            for (let setting of this._overridenDeviceSettings)
+            for (let setting of WI._overridenDeviceSettings)
                 target.PageAgent.overrideSetting(setting, true);
         }
 
@@ -1952,11 +1957,69 @@ WI._handleDeviceSettingsToolbarButtonClicked = function(event)
         return;
     }
 
-    let updateActivatedState = () => {
-        this._deviceSettingsToolbarButton.activated = this._overridenDeviceSettings.size > 0;
-    };
+    function updateActivatedState() {
+        WI._deviceSettingsToolbarButton.activated = WI._overridenDeviceUserAgent || WI._overridenDeviceSettings.size > 0;
+    }
+
+    function applyOverriddenUserAgent(value, force) {
+        if (value === WI._overridenDeviceUserAgent)
+            return;
+
+        if (!force && (!value || value === "default")) {
+            PageAgent.overrideUserAgent((error) => {
+                if (error) {
+                    console.error(error);
+                    return;
+                }
+
+                WI._overridenDeviceUserAgent = null;
+                updateActivatedState();
+                PageAgent.reload();
+            });
+        } else {
+            PageAgent.overrideUserAgent(value, (error) => {
+                if (error) {
+                    console.error(error);
+                    return;
+                }
+
+                WI._overridenDeviceUserAgent = value;
+                updateActivatedState();
+                PageAgent.reload();
+            });
+        }
+    }
+
+    function applyOverriddenSetting(setting, callback) {
+        if (WI._overridenDeviceSettings.has(setting)) {
+            // We've just "disabled" the checkbox, so clear the override instead of applying it.
+            PageAgent.overrideSetting(setting, (error) => {
+                if (error) {
+                    console.error(error);
+                    return;
+                }
+
+                WI._overridenDeviceSettings.delete(setting);
+                callback(false);
+                updateActivatedState();
+            });
+        } else {
+            // Override to false since the labels are the inverse of the setting.
+            const value = false;
+            PageAgent.overrideSetting(setting, value, (error) => {
+                if (error) {
+                    console.error(error);
+                    return;
+                }
+
+                WI._overridenDeviceSettings.add(setting);
+                callback(true);
+                updateActivatedState();
+            });
+        }
+    }
 
-    let createContainer = (parent, title) => {
+    function createContainer(parent, title) {
         let container = parent.appendChild(document.createElement("div"));
         container.classList.add("container");
 
@@ -1966,9 +2029,9 @@ WI._handleDeviceSettingsToolbarButtonClicked = function(event)
         }
 
         return container;
-    };
+    }
 
-    let createColumns = (parent, count) => {
+    function createColumns(parent, count) {
         let columnContainer = parent.appendChild(document.createElement("div"));
         columnContainer.classList.add("columns");
 
@@ -1979,52 +2042,26 @@ WI._handleDeviceSettingsToolbarButtonClicked = function(event)
             columns.push(column);
         }
         return columns;
-    };
-
-    let createCheckbox = (container, label, setting) => {
-        let enabled = this._overridenDeviceSettings.has(setting);
+    }
 
+    function createCheckbox(container, label, setting) {
         let labelElement = container.appendChild(document.createElement("label"));
 
         let checkboxElement = labelElement.appendChild(document.createElement("input"));
         checkboxElement.type = "checkbox";
-        checkboxElement.checked = enabled;
+        checkboxElement.checked = WI._overridenDeviceSettings.has(setting);
         checkboxElement.addEventListener("change", (event) => {
-            if (enabled) {
-                // We've just "disabled" the checkbox, so clear the override instead of applying it.
-                PageAgent.overrideSetting(setting, (error) => {
-                    if (error) {
-                        console.error(error);
-                        return;
-                    }
-
-                    this._overridenDeviceSettings.delete(setting);
-                    enabled = checkboxElement.checked = false;
-                    updateActivatedState();
-                });
-                
-            } else {
-                // Override to false since the labels are the inverse of the setting.
-                const value = false;
-                PageAgent.overrideSetting(setting, value, (error) => {
-                    if (error) {
-                        console.error(error);
-                        return;
-                    }
-
-                    this._overridenDeviceSettings.add(setting);
-                    enabled = checkboxElement.checked = true;
-                    updateActivatedState();
-                });
-            }
+            applyOverriddenSetting(setting, (enabled) => {
+                checkboxElement.checked = enabled;
+            });
         });
 
         labelElement.append(label);
-    };
+    }
 
-    let calculateTargetFrame = () => {
-        return WI.Rect.rectFromClientRect(this._deviceSettingsToolbarButton.element.getBoundingClientRect());
-    };
+    function calculateTargetFrame() {
+        return WI.Rect.rectFromClientRect(WI._deviceSettingsToolbarButton.element.getBoundingClientRect());
+    }
 
     const preferredEdges = [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_X, WI.RectEdge.MAX_X];
 
@@ -2036,6 +2073,101 @@ WI._handleDeviceSettingsToolbarButtonClicked = function(event)
     let contentElement = document.createElement("div");
     contentElement.classList.add("device-settings-content");
 
+    let userAgentContainer = contentElement.appendChild(document.createElement("label"));
+    userAgentContainer.textContent = WI.UIString("User Agent:");
+
+    let userAgentValueContainer = userAgentContainer.appendChild(document.createElement("span"));
+    userAgentValueContainer.classList.add("user-agent-value");
+
+    let userAgentSelect = userAgentValueContainer.appendChild(document.createElement("select"));
+
+    const userAgents = [
+        [
+            { name: WI.UIString("Default"), value: "default" },
+        ],
+        [
+            { name: `Safari ${emDash} iOS 12.1.3 ${emDash} iPhone`, value: "Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1" },
+            { name: `Safari ${emDash} iOS 12.1.3 ${emDash} iPod Touch`, value: "Mozilla/5.0 (iPod; CPU iPhone OS 12_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1" },
+            { name: `Safari ${emDash} iOS 12.1.3 ${emDash} iPad`, value: "Mozilla/5.0 (iPad; CPU iPhone OS 12_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1" },
+        ],
+        [
+            { name: `Microsoft Edge`, value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299" },
+        ],
+        [
+            { name: `Internet Explorer 11`, value: "Mozilla/5.0 (Windows NT 6.3; Win64, x64; Trident/7.0; rv:11.0) like Gecko" },
+            { name: `Internet Explorer 10`, value: "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)" },
+            { name: `Internet Explorer 9`, value: "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)" },
+            { name: `Internet Explorer 8`, value: "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)" },
+            { name: `Internet Explorer 7`, value: "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)" },
+        ],
+        [
+            { name: `Google Chrome ${emDash} macOS`, value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" },
+            { name: `Google Chrome ${emDash} Windows`, value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" },
+        ],
+        [
+            { name: `Firefox ${emDash} macOS`, value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:63.0) Gecko/20100101 Firefox/63.0" },
+            { name: `Firefox ${emDash} Windows`, value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0" },
+        ],
+        [
+            { name: WI.UIString("Other\u2026"), value: "other" },
+        ],
+    ];
+
+    let selectedOptionElement = null;
+
+    for (let group of userAgents) {
+        for (let {name, value} of group) {
+            let optionElement = userAgentSelect.appendChild(document.createElement("option"));
+            optionElement.value = value;
+            optionElement.textContent = name;
+
+            if (value === WI._overridenDeviceUserAgent)
+                selectedOptionElement = optionElement;
+        }
+
+        if (group !== userAgents.lastValue)
+            userAgentSelect.appendChild(document.createElement("hr"));
+    }
+
+    let userAgentInput = null;
+
+    function showUserAgentInput() {
+        if (userAgentInput)
+            return;
+
+        userAgentInput = userAgentValueContainer.appendChild(document.createElement("input"));
+        userAgentInput.value = userAgentInput.placeholder = WI._overridenDeviceUserAgent || navigator.userAgent;
+        userAgentInput.addEventListener("click", (clickEvent) => {
+            clickEvent.preventDefault();
+        });
+        userAgentInput.addEventListener("change", (inputEvent) => {
+            applyOverriddenUserAgent(userAgentInput.value, true);
+        });
+    }
+
+    if (selectedOptionElement)
+        userAgentSelect.value = selectedOptionElement.value;
+    else if (WI._overridenDeviceUserAgent) {
+        userAgentSelect.value = "other";
+        showUserAgentInput();
+    }
+
+    userAgentSelect.addEventListener("change", () => {
+        let value = userAgentSelect.value;
+        if (value === "other") {
+            showUserAgentInput();
+            userAgentInput.selectionStart = userAgentInput.selectionEnd = 0;
+            userAgentInput.focus();
+        } else {
+            if (userAgentInput) {
+                userAgentInput.remove();
+                userAgentInput = null;
+            }
+
+            applyOverriddenUserAgent(value);
+        }
+    });
+
     let disableColumns = createColumns(createContainer(contentElement, WI.UIString("Disable:")), 2);
     createCheckbox(disableColumns[0], WI.UIString("Images"), PageAgent.Setting.ImagesEnabled);
     createCheckbox(disableColumns[0], WI.UIString("Styles"), PageAgent.Setting.AuthorAndUserStylesEnabled);
index f8b0e1a..15a71ef 100644 (file)
@@ -389,8 +389,6 @@ body[dir=rtl] .go-to-arrow {
 
 .device-settings-content {
     padding: 8px;
-
-    --label-input-margin-after: 4px;
 }
 
 .device-settings-content .columns {
@@ -406,12 +404,47 @@ body[dir=rtl] .go-to-arrow {
     -webkit-margin-start: 20px;
 }
 
-body[dir=ltr] .device-settings-content label > input {
-    margin-right: var(--label-input-margin-after);
+.device-settings-content .user-agent-value {
+    display: inline-block;
+    position: relative;
+}
+
+.device-settings-content .user-agent-value > select {
+    -webkit-margin-start: 4px;
+}
+
+.device-settings-content .user-agent-value > input {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 1;
+    margin: 0;
+    -webkit-margin-start: 5px;
+    -webkit-margin-before: 3px;
+    -webkit-margin-after: 3px;
+    -webkit-padding-start: 7px;
+    -webkit-padding-end: 9px;
+    background-color: white;
+    border: none;
+    -webkit-border-top-left-radius: 3px;
+    -webkit-border-bottom-left-radius: 3px;
+    outline: none;
+    pointer-events: all;
+
+    --offset-end: 17px;
+}
+
+body[dir=ltr] .device-settings-content .user-agent-value > input {
+    right: var(--offset-end);
+}
+
+body[dir=rtl] .device-settings-content .user-agent-value > input {
+    left: var(--offset-end);
 }
 
-body[dir=rtl] .device-settings-content label > input {
-    margin-left: var(--label-input-margin-after);
+.device-settings-content label > input {
+    -webkit-margin-end: 4px;
 }
 
 @media (prefers-color-scheme: dark) {