/* * Copyright (c) 2023, Cameron Youell * Copyright (c) 2025, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include namespace WebView { static constexpr auto builtin_autocomplete_engines = to_array({ { "DuckDuckGo"sv, "https://duckduckgo.com/ac/?q={}"sv }, { "Google"sv, "https://www.google.com/complete/search?client=chrome&q={}"sv }, { "Yahoo"sv, "https://search.yahoo.com/sugg/gossip/gossip-us-ura/?output=sd1&command={}"sv }, }); ReadonlySpan autocomplete_engines() { return builtin_autocomplete_engines; } Optional find_autocomplete_engine_by_name(StringView name) { return find_value(builtin_autocomplete_engines, [&](auto const& engine) { return engine.name == name; }); } Autocomplete::Autocomplete() = default; Autocomplete::~Autocomplete() = default; void Autocomplete::query_autocomplete_engine(String query) { if (m_request) { m_request->stop(); m_request.clear(); } if (query.bytes_as_string_view().trim_whitespace().is_empty()) { invoke_autocomplete_query_complete({}); return; } auto engine = Application::settings().autocomplete_engine(); if (!engine.has_value()) { invoke_autocomplete_query_complete({}); return; } auto url_string = MUST(String::formatted(engine->query_url, URL::percent_encode(query))); auto url = URL::Parser::basic_parse(url_string); if (!url.has_value()) { invoke_autocomplete_query_complete({}); return; } m_request = Application::request_server_client().start_request("GET"sv, *url); m_query = move(query); m_request->set_buffered_request_finished_callback( [this, engine = engine.release_value()](u64, Requests::RequestTimingInfo const&, Optional const& network_error, HTTP::HeaderMap const&, Optional response_code, Optional const& reason_phrase, ReadonlyBytes payload) { Core::deferred_invoke([this]() { m_request.clear(); }); if (network_error.has_value()) { warnln("Unable to fetch autocomplete suggestions: {}", Requests::network_error_to_string(*network_error)); invoke_autocomplete_query_complete({}); return; } if (response_code.has_value() && *response_code >= 400) { warnln("Received error response code {} from autocomplete engine: {}", *response_code, reason_phrase); invoke_autocomplete_query_complete({}); return; } if (auto result = received_autocomplete_respsonse(engine, payload); result.is_error()) { warnln("Unable to handle autocomplete response: {}", result.error()); invoke_autocomplete_query_complete({}); } else { invoke_autocomplete_query_complete(result.release_value()); } }); } static ErrorOr> parse_duckduckgo_autocomplete(JsonValue const& json) { if (!json.is_array()) return Error::from_string_literal("Expected DuckDuckGo autocomplete response to be a JSON array"); Vector results; results.ensure_capacity(json.as_array().size()); TRY(json.as_array().try_for_each([&](JsonValue const& suggestion) -> ErrorOr { if (!suggestion.is_object()) return Error::from_string_literal("Invalid DuckDuckGo autocomplete response, expected value to be an object"); if (auto value = suggestion.as_object().get_string("phrase"sv); value.has_value()) results.unchecked_append(*value); return {}; })); return results; } static ErrorOr> parse_google_autocomplete(JsonValue const& json) { if (!json.is_array()) return Error::from_string_literal("Expected Google autocomplete response to be a JSON array"); auto const& values = json.as_array(); if (values.size() != 5) return Error::from_string_literal("Invalid Google autocomplete response, expected 5 elements in array"); if (!values[1].is_array()) return Error::from_string_literal("Invalid Google autocomplete response, expected second element to be an array"); auto const& suggestions = values[1].as_array(); Vector results; results.ensure_capacity(suggestions.size()); TRY(suggestions.try_for_each([&](JsonValue const& suggestion) -> ErrorOr { if (!suggestion.is_string()) return Error::from_string_literal("Invalid Google autocomplete response, expected value to be a string"); results.unchecked_append(suggestion.as_string()); return {}; })); return results; } static ErrorOr> parse_yahoo_autocomplete(JsonValue const& json) { if (!json.is_object()) return Error::from_string_literal("Expected Yahoo autocomplete response to be a JSON array"); auto suggestions = json.as_object().get_array("r"sv); if (!suggestions.has_value()) return Error::from_string_literal("Invalid Yahoo autocomplete response, expected \"r\" to be an object"); Vector results; results.ensure_capacity(suggestions->size()); TRY(suggestions->try_for_each([&](JsonValue const& suggestion) -> ErrorOr { if (!suggestion.is_object()) return Error::from_string_literal("Invalid Yahoo autocomplete response, expected value to be an object"); auto result = suggestion.as_object().get_string("k"sv); if (!result.has_value()) return Error::from_string_literal("Invalid Yahoo autocomplete response, expected \"k\" to be a string"); results.unchecked_append(*result); return {}; })); return results; } ErrorOr> Autocomplete::received_autocomplete_respsonse(AutocompleteEngine const& engine, StringView response) { auto json = TRY(JsonValue::from_string(response)); if (engine.name == "DuckDuckGo") return parse_duckduckgo_autocomplete(json); if (engine.name == "Google") return parse_google_autocomplete(json); if (engine.name == "Yahoo") return parse_yahoo_autocomplete(json); return Error::from_string_literal("Invalid engine name"); } void Autocomplete::invoke_autocomplete_query_complete(Vector suggestions) const { if (on_autocomplete_query_complete) on_autocomplete_query_complete(move(suggestions)); } }