summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibWeb/WebDriver/Capabilities.cpp
blob: d8f671ea659256efa4988a2fc534e54a7b50f733 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
/*
 * Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Debug.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/Optional.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/WebDriver/Capabilities.h>
#include <LibWeb/WebDriver/TimeoutsConfiguration.h>

namespace Web::WebDriver {

// https://w3c.github.io/webdriver/#dfn-deserialize-as-a-page-load-strategy
static Response deserialize_as_a_page_load_strategy(JsonValue value)
{
    // 1. If value is not a string return an error with error code invalid argument.
    if (!value.is_string())
        return Error::from_code(ErrorCode::InvalidArgument, "Capability pageLoadStrategy must be a string"sv);

    // 2. If there is no entry in the table of page load strategies with keyword value return an error with error code invalid argument.
    if (!value.as_string().is_one_of("none"sv, "eager"sv, "normal"sv))
        return Error::from_code(ErrorCode::InvalidArgument, "Invalid pageLoadStrategy capability"sv);

    // 3. Return success with data value.
    return value;
}

// https://w3c.github.io/webdriver/#dfn-deserialize-as-an-unhandled-prompt-behavior
static Response deserialize_as_an_unhandled_prompt_behavior(JsonValue value)
{
    // 1. If value is not a string return an error with error code invalid argument.
    if (!value.is_string())
        return Error::from_code(ErrorCode::InvalidArgument, "Capability unhandledPromptBehavior must be a string"sv);

    // 2. If value is not present as a keyword in the known prompt handling approaches table return an error with error code invalid argument.
    if (!value.as_string().is_one_of("dismiss"sv, "accept"sv, "dismiss and notify"sv, "accept and notify"sv, "ignore"sv))
        return Error::from_code(ErrorCode::InvalidArgument, "Invalid pageLoadStrategy capability"sv);

    // 3. Return success with data value.
    return value;
}

static Response deserialize_as_ladybird_options(JsonValue value)
{
    if (!value.is_object())
        return Error::from_code(ErrorCode::InvalidArgument, "Extension capability serenity:ladybird must be an object"sv);

    auto const& object = value.as_object();

    if (auto const* headless = object.get_ptr("headless"sv); headless && !headless->is_bool())
        return Error::from_code(ErrorCode::InvalidArgument, "Extension capability serenity:ladybird/headless must be a boolean"sv);

    return value;
}

static JsonObject default_ladybird_options()
{
    JsonObject options;
    options.set("headless"sv, false);

    return options;
}

// https://w3c.github.io/webdriver/#dfn-validate-capabilities
static ErrorOr<JsonObject, Error> validate_capabilities(JsonValue const& capability)
{
    // 1. If capability is not a JSON Object return an error with error code invalid argument.
    if (!capability.is_object())
        return Error::from_code(ErrorCode::InvalidArgument, "Capability is not an Object"sv);

    // 2. Let result be an empty JSON Object.
    JsonObject result;

    // 3. For each enumerable own property in capability, run the following substeps:
    TRY(capability.as_object().try_for_each_member([&](auto const& name, auto const& value) -> ErrorOr<void, Error> {
        // a. Let name be the name of the property.
        // b. Let value be the result of getting a property named name from capability.

        // c. Run the substeps of the first matching condition:
        JsonValue deserialized;

        // -> value is null
        if (value.is_null()) {
            // Let deserialized be set to null.
        }

        // -> name equals "acceptInsecureCerts"
        else if (name == "acceptInsecureCerts"sv) {
            // If value is not a boolean return an error with error code invalid argument. Otherwise, let deserialized be set to value
            if (!value.is_bool())
                return Error::from_code(ErrorCode::InvalidArgument, "Capability acceptInsecureCerts must be a boolean"sv);
            deserialized = value;
        }

        // -> name equals "browserName"
        // -> name equals "browserVersion"
        // -> name equals "platformName"
        else if (name.is_one_of("browserName"sv, "browser_version"sv, "platformName"sv)) {
            // If value is not a string return an error with error code invalid argument. Otherwise, let deserialized be set to value.
            if (!value.is_string())
                return Error::from_code(ErrorCode::InvalidArgument, DeprecatedString::formatted("Capability {} must be a string", name));
            deserialized = value;
        }

        // -> name equals "pageLoadStrategy"
        else if (name == "pageLoadStrategy"sv) {
            // Let deserialized be the result of trying to deserialize as a page load strategy with argument value.
            deserialized = TRY(deserialize_as_a_page_load_strategy(value));
        }

        // FIXME: -> name equals "proxy"
        // FIXME:     Let deserialized be the result of trying to deserialize as a proxy with argument value.

        // -> name equals "strictFileInteractability"
        else if (name == "strictFileInteractability"sv) {
            // If value is not a boolean return an error with error code invalid argument. Otherwise, let deserialized be set to value
            if (!value.is_bool())
                return Error::from_code(ErrorCode::InvalidArgument, "Capability strictFileInteractability must be a boolean"sv);
            deserialized = value;
        }

        // -> name equals "timeouts"
        else if (name == "timeouts"sv) {
            // Let deserialized be the result of trying to JSON deserialize as a timeouts configuration the value.
            auto timeouts = TRY(json_deserialize_as_a_timeouts_configuration(value));
            deserialized = JsonValue { timeouts_object(timeouts) };
        }

        // -> name equals "unhandledPromptBehavior"
        else if (name == "unhandledPromptBehavior"sv) {
            // Let deserialized be the result of trying to deserialize as an unhandled prompt behavior with argument value.
            deserialized = TRY(deserialize_as_an_unhandled_prompt_behavior(value));
        }

        // FIXME: -> name is the name of an additional WebDriver capability
        // FIXME:     Let deserialized be the result of trying to run the additional capability deserialization algorithm for the extension capability corresponding to name, with argument value.

        // -> name is the key of an extension capability
        //     If name is known to the implementation, let deserialized be the result of trying to deserialize value in an implementation-specific way. Otherwise, let deserialized be set to value.
        else if (name == "serenity:ladybird"sv) {
            deserialized = TRY(deserialize_as_ladybird_options(value));
        }

        // -> The remote end is an endpoint node
        else {
            // Return an error with error code invalid argument.
            return Error::from_code(ErrorCode::InvalidArgument, DeprecatedString::formatted("Unrecognized capability: {}", name));
        }

        // d. If deserialized is not null, set a property on result with name name and value deserialized.
        if (!deserialized.is_null())
            result.set(name, move(deserialized));

        return {};
    }));

    // 4. Return success with data result.
    return result;
}

// https://w3c.github.io/webdriver/#dfn-merging-capabilities
static ErrorOr<JsonObject, Error> merge_capabilities(JsonObject const& primary, Optional<JsonObject const&> const& secondary)
{
    // 1. Let result be a new JSON Object.
    JsonObject result;

    // 2. For each enumerable own property in primary, run the following substeps:
    primary.for_each_member([&](auto const& name, auto const& value) {
        // a. Let name be the name of the property.
        // b. Let value be the result of getting a property named name from primary.

        // c. Set a property on result with name name and value value.
        result.set(name, value);
    });

    // 3. If secondary is undefined, return result.
    if (!secondary.has_value())
        return result;

    // 4. For each enumerable own property in secondary, run the following substeps:
    TRY(secondary->try_for_each_member([&](auto const& name, auto const& value) -> ErrorOr<void, Error> {
        // a. Let name be the name of the property.
        // b. Let value be the result of getting a property named name from secondary.

        // c. Let primary value be the result of getting the property name from primary.
        auto const* primary_value = primary.get_ptr(name);

        // d. If primary value is not undefined, return an error with error code invalid argument.
        if (primary_value != nullptr)
            return Error::from_code(ErrorCode::InvalidArgument, DeprecatedString::formatted("Unable to merge capability {}", name));

        // e. Set a property on result with name name and value value.
        result.set(name, value);
        return {};
    }));

    // 5. Return result.
    return result;
}

static bool matches_browser_version(StringView requested_version, StringView required_version)
{
    // FIXME: Handle relative (>, >=, <. <=) comparisons. For now, require an exact match.
    return requested_version == required_version;
}

static bool matches_platform_name(StringView requested_platform_name, StringView required_platform_name)
{
    if (requested_platform_name == required_platform_name)
        return true;

    // The following platform names are in common usage with well-understood semantics and, when matching capabilities, greatest interoperability can be achieved by honoring them as valid synonyms for well-known Operating Systems:
    //     "linux"   Any server or desktop system based upon the Linux kernel.
    //     "mac"     Any version of Apple’s macOS.
    //     "windows" Any version of Microsoft Windows, including desktop and mobile versions.
    // This list is not exhaustive.

    // NOTE: Of the synonyms listed in the spec, the only one that differs for us is macOS.
    //       Further, we are allowed to handle synonyms for SerenityOS.
    if (requested_platform_name == "mac"sv && required_platform_name == "macos"sv)
        return true;
    if (requested_platform_name == "serenity"sv && required_platform_name == "serenityos"sv)
        return true;
    return false;
}

// https://w3c.github.io/webdriver/#dfn-matching-capabilities
static JsonValue match_capabilities(JsonObject const& capabilities)
{
    static auto browser_name = StringView { BROWSER_NAME, strlen(BROWSER_NAME) }.to_lowercase_string();
    static auto platform_name = StringView { OS_STRING, strlen(OS_STRING) }.to_lowercase_string();

    // 1. Let matched capabilities be a JSON Object with the following entries:
    JsonObject matched_capabilities;
    // "browserName"
    //     ASCII Lowercase name of the user agent as a string.
    matched_capabilities.set("browserName"sv, browser_name);
    // "browserVersion"
    //     The user agent version, as a string.
    matched_capabilities.set("browserVersion"sv, BROWSER_VERSION);
    // "platformName"
    //     ASCII Lowercase name of the current platform as a string.
    matched_capabilities.set("platformName"sv, platform_name);
    // "acceptInsecureCerts"
    //     Boolean initially set to false, indicating the session will not implicitly trust untrusted or self-signed TLS certificates on navigation.
    matched_capabilities.set("acceptInsecureCerts"sv, false);
    // "strictFileInteractability"
    //     Boolean initially set to false, indicating that interactability checks will be applied to <input type=file>.
    matched_capabilities.set("strictFileInteractability"sv, false);
    // "setWindowRect"
    //     Boolean indicating whether the remote end supports all of the resizing and positioning commands.
    matched_capabilities.set("setWindowRect"sv, true);

    // 2. Optionally add extension capabilities as entries to matched capabilities. The values of these may be elided, and there is no requirement that all extension capabilities be added.
    matched_capabilities.set("serenity:ladybird"sv, default_ladybird_options());

    // 3. For each name and value corresponding to capability’s own properties:
    auto result = capabilities.try_for_each_member([&](auto const& name, auto const& value) -> ErrorOr<void> {
        // a. Let match value equal value.

        // b. Run the substeps of the first matching name:
        // -> "browserName"
        if (name == "browserName"sv) {
            // If value is not a string equal to the "browserName" entry in matched capabilities, return success with data null.
            if (value.as_string() != matched_capabilities.get(name).as_string())
                return AK::Error::from_string_view("browserName"sv);
        }
        // -> "browserVersion"
        else if (name == "browserVersion"sv) {
            // Compare value to the "browserVersion" entry in matched capabilities using an implementation-defined comparison algorithm. The comparison is to accept a value that places constraints on the version using the "<", "<=", ">", and ">=" operators.
            // If the two values do not match, return success with data null.
            if (!matches_browser_version(value.as_string(), matched_capabilities.get(name).as_string()))
                return AK::Error::from_string_view("browserVersion"sv);
        }
        // -> "platformName"
        else if (name == "platformName"sv) {
            // If value is not a string equal to the "platformName" entry in matched capabilities, return success with data null.
            if (!matches_platform_name(value.as_string(), matched_capabilities.get(name).as_string()))
                return AK::Error::from_string_view("platformName"sv);
        }
        // -> "acceptInsecureCerts"
        else if (name == "acceptInsecureCerts"sv) {
            // If value is true and the endpoint node does not support insecure TLS certificates, return success with data null.
            if (value.as_bool())
                return AK::Error::from_string_view("acceptInsecureCerts"sv);
        }
        // -> "proxy"
        else if (name == "proxy"sv) {
            // FIXME: If the endpoint node does not allow the proxy it uses to be configured, or if the proxy configuration defined in value is not one that passes the endpoint node’s implementation-specific validity checks, return success with data null.
        }
        // -> Otherwise
        else {
            // FIXME: If name is the name of an additional WebDriver capability which defines a matched capability serialization algorithm, let match value be the result of running the matched capability serialization algorithm for capability name with argument value.
            // FIXME: Otherwise, if name is the key of an extension capability, let match value be the result of trying implementation-specific steps to match on name with value. If the match is not successful, return success with data null.
        }

        // c. Set a property on matched capabilities with name name and value match value.
        matched_capabilities.set(name, value);
        return {};
    });

    if (result.is_error()) {
        dbgln_if(WEBDRIVER_DEBUG, "Failed to match capability: {}", result.error());
        return JsonValue {};
    }

    // 4. Return success with data matched capabilities.
    return matched_capabilities;
}

// https://w3c.github.io/webdriver/#dfn-capabilities-processing
Response process_capabilities(JsonValue const& parameters)
{
    if (!parameters.is_object())
        return Error::from_code(ErrorCode::InvalidArgument, "Session parameters is not an object"sv);

    // 1. Let capabilities request be the result of getting the property "capabilities" from parameters.
    //     a. If capabilities request is not a JSON Object, return error with error code invalid argument.
    auto const* capabilities_request_ptr = parameters.as_object().get_ptr("capabilities"sv);
    if (!capabilities_request_ptr || !capabilities_request_ptr->is_object())
        return Error::from_code(ErrorCode::InvalidArgument, "Capabilities is not an object"sv);

    auto const& capabilities_request = capabilities_request_ptr->as_object();

    // 2. Let required capabilities be the result of getting the property "alwaysMatch" from capabilities request.
    //     a. If required capabilities is undefined, set the value to an empty JSON Object.
    JsonObject required_capabilities;

    if (auto const* capability = capabilities_request.get_ptr("alwaysMatch"sv)) {
        // b. Let required capabilities be the result of trying to validate capabilities with argument required capabilities.
        required_capabilities = TRY(validate_capabilities(*capability));
    }

    // 3. Let all first match capabilities be the result of getting the property "firstMatch" from capabilities request.
    JsonArray all_first_match_capabilities;

    if (auto const* capabilities = capabilities_request.get_ptr("firstMatch"sv)) {
        // b. If all first match capabilities is not a JSON List with one or more entries, return error with error code invalid argument.
        if (!capabilities->is_array() || capabilities->as_array().is_empty())
            return Error::from_code(ErrorCode::InvalidArgument, "Capability firstMatch must be an array with at least one entry"sv);

        all_first_match_capabilities = capabilities->as_array();
    } else {
        // a. If all first match capabilities is undefined, set the value to a JSON List with a single entry of an empty JSON Object.
        all_first_match_capabilities.append(JsonObject {});
    }

    // 4. Let validated first match capabilities be an empty JSON List.
    JsonArray validated_first_match_capabilities;
    validated_first_match_capabilities.ensure_capacity(all_first_match_capabilities.size());

    // 5. For each first match capabilities corresponding to an indexed property in all first match capabilities:
    TRY(all_first_match_capabilities.try_for_each([&](auto const& first_match_capabilities) -> ErrorOr<void, Error> {
        // a. Let validated capabilities be the result of trying to validate capabilities with argument first match capabilities.
        auto validated_capabilities = TRY(validate_capabilities(first_match_capabilities));

        // b. Append validated capabilities to validated first match capabilities.
        validated_first_match_capabilities.append(move(validated_capabilities));
        return {};
    }));

    // 6. Let merged capabilities be an empty List.
    JsonArray merged_capabilities;
    merged_capabilities.ensure_capacity(validated_first_match_capabilities.size());

    // 7. For each first match capabilities corresponding to an indexed property in validated first match capabilities:
    TRY(validated_first_match_capabilities.try_for_each([&](auto const& first_match_capabilities) -> ErrorOr<void, Error> {
        // a. Let merged be the result of trying to merge capabilities with required capabilities and first match capabilities as arguments.
        auto merged = TRY(merge_capabilities(required_capabilities, first_match_capabilities.as_object()));

        // b. Append merged to merged capabilities.
        merged_capabilities.append(move(merged));
        return {};
    }));

    // 8. For each capabilities corresponding to an indexed property in merged capabilities:
    for (auto const& capabilities : merged_capabilities.values()) {
        // a. Let matched capabilities be the result of trying to match capabilities with capabilities as an argument.
        auto matched_capabilities = match_capabilities(capabilities.as_object());

        // b. If matched capabilities is not null, return success with data matched capabilities.
        if (!matched_capabilities.is_null())
            return matched_capabilities;
    }

    // 9. Return success with data null.
    return JsonValue {};
}

LadybirdOptions::LadybirdOptions(JsonObject const& capabilities)
{
    auto const* options = capabilities.get_ptr("serenity:ladybird"sv);
    if (!options || !options->is_object())
        return;

    auto const* headless = options->as_object().get_ptr("headless"sv);
    if (headless && headless->is_bool())
        this->headless = headless->as_bool();
}

}