2025-03-30 17:21:24 -03:00
/*
* Copyright ( c ) 2023 , Cameron Youell < cameronyouell @ gmail . com >
* Copyright ( c ) 2025 , Tim Flynn < trflynn89 @ ladybird . org >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
2026-04-11 19:32:29 -03:00
# include <AK/Debug.h>
2025-03-30 17:21:24 -03:00
# include <AK/Find.h>
# include <LibCore/EventLoop.h>
# include <LibRequests/Request.h>
# include <LibRequests/RequestClient.h>
2025-04-16 18:35:24 -03:00
# include <LibTextCodec/Decoder.h>
2025-03-30 17:21:24 -03:00
# include <LibURL/Parser.h>
# include <LibURL/URL.h>
2025-04-16 18:35:24 -03:00
# include <LibWeb/MimeSniff/MimeType.h>
2025-03-30 17:21:24 -03:00
# include <LibWebView/Application.h>
# include <LibWebView/Autocomplete.h>
2026-04-11 19:32:29 -03:00
# include <LibWebView/HistoryDebug.h>
# include <LibWebView/HistoryStore.h>
2025-03-30 17:21:24 -03:00
namespace WebView {
2025-07-17 18:17:54 -03:00
static constexpr auto file_url_prefix = " file:// " sv ;
2025-03-30 17:21:24 -03:00
static constexpr auto builtin_autocomplete_engines = to_array < AutocompleteEngine > ( {
{ " 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 < AutocompleteEngine > autocomplete_engines ( )
{
return builtin_autocomplete_engines ;
}
Optional < AutocompleteEngine const & > find_autocomplete_engine_by_name ( StringView name )
{
2025-04-05 10:06:51 -03:00
return find_value ( builtin_autocomplete_engines , [ & ] ( auto const & engine ) {
return engine . name = = name ;
} ) ;
2025-03-30 17:21:24 -03:00
}
Autocomplete : : Autocomplete ( ) = default ;
Autocomplete : : ~ Autocomplete ( ) = default ;
2026-04-11 19:32:29 -03:00
static Vector < String > merge_suggestions ( Vector < String > history_suggestions , Vector < String > remote_suggestions )
{
history_suggestions . ensure_capacity ( history_suggestions . size ( ) + remote_suggestions . size ( ) ) ;
for ( auto & suggestion : remote_suggestions ) {
if ( ! history_suggestions . contains_slow ( suggestion ) )
history_suggestions . unchecked_append ( move ( suggestion ) ) ;
}
return history_suggestions ;
}
2025-03-30 17:21:24 -03:00
void Autocomplete : : query_autocomplete_engine ( String query )
{
if ( m_request ) {
m_request - > stop ( ) ;
m_request . clear ( ) ;
}
2026-04-11 19:32:29 -03:00
auto trimmed_query = MUST ( String : : from_utf8 ( query . bytes_as_string_view ( ) . trim_whitespace ( ) ) ) ;
m_query = move ( query ) ;
dbgln_if ( WEBVIEW_HISTORY_DEBUG , " [History] Autocomplete query='{}' trimmed='{}' " , m_query , trimmed_query ) ;
m_history_suggestions = Application : : history_store ( ) . autocomplete_suggestions ( trimmed_query ) ;
dbgln_if ( WEBVIEW_HISTORY_DEBUG , " [History] History autocomplete suggestions for '{}': {} " , trimmed_query , history_log_suggestions ( m_history_suggestions ) ) ;
invoke_autocomplete_query_complete ( m_history_suggestions ) ;
if ( trimmed_query . is_empty ( ) ) {
dbgln_if ( WEBVIEW_HISTORY_DEBUG , " [History] Skipping remote autocomplete for empty query " ) ;
return ;
}
if ( trimmed_query . starts_with_bytes ( file_url_prefix ) ) {
dbgln_if ( WEBVIEW_HISTORY_DEBUG , " [History] Skipping remote autocomplete for file URL query '{}' " , trimmed_query ) ;
2025-03-30 17:21:24 -03:00
return ;
}
auto engine = Application : : settings ( ) . autocomplete_engine ( ) ;
if ( ! engine . has_value ( ) ) {
2026-04-11 19:32:29 -03:00
dbgln_if ( WEBVIEW_HISTORY_DEBUG , " [History] Skipping remote autocomplete because no engine is configured " ) ;
2025-03-30 17:21:24 -03:00
return ;
}
2026-04-11 19:32:29 -03:00
dbgln_if ( WEBVIEW_HISTORY_DEBUG , " [History] Fetching remote autocomplete suggestions from {} for '{}' " , engine - > name , m_query ) ;
auto url_string = MUST ( String : : formatted ( engine - > query_url , URL : : percent_encode ( m_query ) ) ) ;
2025-03-30 17:21:24 -03:00
auto url = URL : : Parser : : basic_parse ( url_string ) ;
2026-04-11 19:32:29 -03:00
if ( ! url . has_value ( ) )
2025-03-30 17:21:24 -03:00
return ;
m_request = Application : : request_server_client ( ) . start_request ( " GET " sv , * url ) ;
m_request - > set_buffered_request_finished_callback (
2026-04-11 19:32:29 -03:00
[ this , engine = engine . release_value ( ) , query = m_query ] ( u64 , Requests : : RequestTimingInfo const & , Optional < Requests : : NetworkError > const & network_error , HTTP : : HeaderList const & response_headers , Optional < u32 > response_code , Optional < String > const & reason_phrase , ReadonlyBytes payload ) {
2025-03-30 17:21:24 -03:00
Core : : deferred_invoke ( [ this ] ( ) { m_request . clear ( ) ; } ) ;
2026-04-11 19:32:29 -03:00
if ( m_query ! = query ) {
dbgln_if ( WEBVIEW_HISTORY_DEBUG , " [History] Discarding stale remote autocomplete response for '{}' while current query is '{}' " , query , m_query ) ;
return ;
}
2025-03-30 17:21:24 -03:00
if ( network_error . has_value ( ) ) {
warnln ( " Unable to fetch autocomplete suggestions: {} " , Requests : : network_error_to_string ( * network_error ) ) ;
2026-04-11 19:32:29 -03:00
invoke_autocomplete_query_complete ( m_history_suggestions ) ;
2025-03-30 17:21:24 -03:00
return ;
}
if ( response_code . has_value ( ) & & * response_code > = 400 ) {
warnln ( " Received error response code {} from autocomplete engine: {} " , * response_code , reason_phrase ) ;
2026-04-11 19:32:29 -03:00
invoke_autocomplete_query_complete ( m_history_suggestions ) ;
2025-03-30 17:21:24 -03:00
return ;
}
2025-04-16 18:35:24 -03:00
auto content_type = response_headers . get ( " Content-Type " sv ) ;
if ( auto result = received_autocomplete_respsonse ( engine , content_type , payload ) ; result . is_error ( ) ) {
2025-03-30 17:21:24 -03:00
warnln ( " Unable to handle autocomplete response: {} " , result . error ( ) ) ;
2026-04-11 19:32:29 -03:00
invoke_autocomplete_query_complete ( m_history_suggestions ) ;
2025-03-30 17:21:24 -03:00
} else {
2026-04-11 19:32:29 -03:00
auto remote_suggestions = result . release_value ( ) ;
dbgln_if ( WEBVIEW_HISTORY_DEBUG , " [History] Remote autocomplete suggestions for '{}': {} " , query , history_log_suggestions ( remote_suggestions ) ) ;
auto merged_suggestions = merge_suggestions ( m_history_suggestions , move ( remote_suggestions ) ) ;
dbgln_if ( WEBVIEW_HISTORY_DEBUG , " [History] Merged autocomplete suggestions for '{}': {} " , query , history_log_suggestions ( merged_suggestions ) ) ;
invoke_autocomplete_query_complete ( move ( merged_suggestions ) ) ;
2025-03-30 17:21:24 -03:00
}
} ) ;
}
static ErrorOr < Vector < String > > 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 < String > results ;
results . ensure_capacity ( json . as_array ( ) . size ( ) ) ;
TRY ( json . as_array ( ) . try_for_each ( [ & ] ( JsonValue const & suggestion ) - > ErrorOr < void > {
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 < Vector < String > > 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 < String > results ;
results . ensure_capacity ( suggestions . size ( ) ) ;
TRY ( suggestions . try_for_each ( [ & ] ( JsonValue const & suggestion ) - > ErrorOr < void > {
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 < Vector < String > > 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 < String > results ;
results . ensure_capacity ( suggestions - > size ( ) ) ;
TRY ( suggestions - > try_for_each ( [ & ] ( JsonValue const & suggestion ) - > ErrorOr < void > {
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 ;
}
2025-04-16 18:35:24 -03:00
ErrorOr < Vector < String > > Autocomplete : : received_autocomplete_respsonse ( AutocompleteEngine const & engine , Optional < ByteString const & > content_type , StringView response )
2025-03-30 17:21:24 -03:00
{
2025-04-16 18:35:24 -03:00
auto decoder = [ & ] ( ) - > Optional < TextCodec : : Decoder & > {
if ( ! content_type . has_value ( ) )
return { } ;
auto mime_type = Web : : MimeSniff : : MimeType : : parse ( * content_type ) ;
if ( ! mime_type . has_value ( ) )
return { } ;
auto charset = mime_type - > parameters ( ) . get ( " charset " sv ) ;
if ( ! charset . has_value ( ) )
return { } ;
return TextCodec : : decoder_for_exact_name ( * charset ) ;
} ( ) ;
if ( ! decoder . has_value ( ) )
decoder = TextCodec : : decoder_for_exact_name ( " UTF-8 " sv ) ;
auto decoded_response = TRY ( decoder - > to_utf8 ( response ) ) ;
auto json = TRY ( JsonValue : : from_string ( decoded_response ) ) ;
2025-03-30 17:21:24 -03:00
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 < String > suggestions ) const
{
2026-04-11 19:32:29 -03:00
dbgln_if ( WEBVIEW_HISTORY_DEBUG , " [History] Delivering {} autocomplete suggestion(s) " , suggestions . size ( ) ) ;
2025-03-30 17:21:24 -03:00
if ( on_autocomplete_query_complete )
on_autocomplete_query_complete ( move ( suggestions ) ) ;
}
}