Compare commits

...

27 commits

Author SHA1 Message Date
Sam Atkins
3716db1c61 LibWeb/CSS: Implement converting CSSTransformValues to StyleValues 2025-10-14 13:41:47 +01:00
Sam Atkins
5178d1ebe3 LibWeb/CSS: Add flag to disable create-internal-rep type checking
CSSTransformComponents hold other CSSStyleValues as their parameters. We
want to be able to create internal representations from those parameters
without them caring if they would be valid when directly assigned to the
property.

This is a temporary solution to make transform functions work. To be
completely correct, we need to know what is allowed in that context,
along with value ranges - a combination of the contexts we create when
parsing, and when computing calculations. For transform functions, this
doesn't matter, as there's no limit to the range of allowed values.
2025-10-14 13:41:47 +01:00
Sam Atkins
35fd3bda79 LibWeb/CSS: Promote <transform-list/function> to parsable types
The `transform` property is now parsed based on its JSON data, and
shouldn't behave any differently than before.

This makes `<transform-list>` and `<transform-function>` work in the
`syntax` descriptor for `@property`, and also means we know that
`transform` can accept the `none` keyword. We get a few WPT passes out
of that.
2025-10-14 13:41:47 +01:00
Sam Atkins
65ba5acf9d Tests: Import @property { syntax } parsing test
This gets some extra passes with the next commit.
2025-10-14 13:41:47 +01:00
Sam Atkins
8fffce07df LibWeb/CSS: Remove unused GenericShorthands.h include 2025-10-14 13:41:47 +01:00
Jelle Raaijmakers
21932661c2 CI: Update .github/actions/ folder to latest when building older commits
Older commits also have older setup code, such as requiring older XCode
versions - which fails on newer macOS systems. Let's always use the
latest custom actions so we can retroactively support these older
builds.
2025-10-14 14:19:22 +02:00
Timothy Flynn
163e8e5b44 LibWebView+RequestServer: Support clearing the HTTP disk cache
This is a bit of a blunt hammer, but this hooks an action to clear the
HTTP disk cache into the existing Clear Cache action. Upon invocation,
it stops all existing cache entries from making further progress, and
then deletes the entire cache index and all cache files.

In the future, we will of course want more fine-grained control over
cache deletion, e.g. via an about:history page.
2025-10-14 13:40:33 +02:00
Timothy Flynn
42eaea1043 LibWebView: Add a command line flag to enable the HTTP disk cache
This adds a RequestServerOptions structure to hold this option and the
only other RS option we currently have (certificates).
2025-10-14 13:40:33 +02:00
Timothy Flynn
3516a2344f LibRequests+RequestServer: Begin implementing an HTTP disk cache
This adds a disk cache for HTTP responses received from the network. For
now, we take a rather conservative approach to caching. We don't cache a
response until we're 100% sure it is cacheable (there are heuristics we
can implement in the future based on the absence of specific headers).

The cache is broken into 2 categories of files:

1. An index file. This is a SQL database containing metadata about each
   cache entry (URL, timestamps, etc.).
2. Cache files. Each cached response is in its own file. The file is an
   amalgamation of all info needed to reconstruct an HTTP response. This
   includes the status code, headers, body, etc.

A cache entry is created once we receive the headers for a response. The
index, however, is not updated at this point. We stream the body into
the cache entry as it is received. Once we've successfully cached the
entire body, we create an index entry in the database. If any of these
steps failed along the way, the cache entry is removed and the index is
left untouched.

Subsequent requests are checked for cache hits from the index. If a hit
is found, we read just enough of the cache entry to inform WebContent of
the status code and headers. The body of the response is piped to WC via
syscalls, such that the transfer happens entirely in the kernel; no need
to allocate the memory for the body in userspace (WC still allocates a
buffer to hold the data, of course). If an error occurs while piping the
body, we currently error out the request. There is a FIXME to switch to
a network request.

Cache hits are also validated for freshness before they are used. If a
response has expired, we remove it and its index entry, and proceed with
a network request.
2025-10-14 13:40:33 +02:00
Timothy Flynn
411aed96ab LibDatabase: Support all C++ integral types in SQL storage 2025-10-14 13:40:33 +02:00
Timothy Flynn
187d02c45d LibDatabase+LibWebView: Extract our SQLite wrapper to its own library
It currently lives in LibWebView as it was only used for cookies and
local storage, both of which are managed in the UI process. Let's move
it to its own library now to allow other processes to use it, without
having to depend on LibWebView (and therefore LibWeb).
2025-10-14 13:40:33 +02:00
Timothy Flynn
e433dee543 LibCore: Add a system wrapper to pipe a file
This uses splice on Linux and mmap+write elsewhere to transfer a file to
a pipe. Note we cannot use sendfile because (at least on macOS) the
receiving fd must be a socket.
2025-10-14 13:40:33 +02:00
Timothy Flynn
62e52640d0 LibCore: Add a standard path for cache data 2025-10-14 13:40:33 +02:00
Jelle Raaijmakers
d349e91339 CI: Inject COMMIT file into archives when missing
This allows us to run js-benchmarks against older commits and have the
workflow correctly identify the commit used to build the binaries. In
order to actually build commits where the wasm binary was not yet built,
we have to account for missing archives as well.
2025-10-14 12:47:25 +02:00
ayeteadoe
0d5136ae5c LibWeb: Add support for bitmap scaling in createImageBitmap() 2025-10-14 12:19:33 +02:00
ayeteadoe
05f3bd0fa8 Tests/LibWeb: Import several scaling createImageBitmap() tests 2025-10-14 12:19:33 +02:00
Tim Ledbetter
0bdb831c68 LibWeb: Avoid null dereference in ListItemBox specified content check 2025-10-14 10:27:11 +01:00
Tim Ledbetter
2f5481284d Meta: Don't attempt to download WPT test resources from external URLs 2025-10-14 10:27:11 +01:00
stelar7
61185d98aa LibWeb/IDB: Adjust how negative numbers increment the key generator
Directly mapping a negative double to a u64 causes it to wrap around
to the max value. We work around this here by comparing as doubles,
and only incrementing the generator if the new value is greater

Fixes #6455
2025-10-14 10:26:28 +01:00
Aliaksandr Kalenik
207f313b4b LibWeb: Delete unused Document::set_visibility_state() 2025-10-14 11:23:29 +02:00
Aliaksandr Kalenik
4853e2ffb1 LibWeb: Don't reach into layout node to check if paintable is SVG 2025-10-14 11:23:29 +02:00
Aliaksandr Kalenik
881ef21d40 LibWeb: Get rid of SVGPathPaintable::layout_box()
It was used exclusively to get corresponding DOM node pointer, which is
unnecessary indirection as Paintable owns the DOM node pointer directly.
2025-10-14 11:23:29 +02:00
Aliaksandr Kalenik
f706c883eb LibWeb: Don't reach into layout node to check if Paintable is positioned
We copy this information into Paintable, so it could be taken directly
from there.
2025-10-14 11:23:29 +02:00
Aliaksandr Kalenik
9e838cffb4 LibWeb: Copy "is inert" attribute into Paintable
...instead of reaching into DOM tree during hit-testing in order to
figure out if an element is inert. This is a part of the effert to make
possible running hit-testing solely based on data contained by the
paintable tree.
2025-10-14 11:23:29 +02:00
Aliaksandr Kalenik
81aeee3fb4 LibWeb: Get rid of PaintableBox::is_viewport()
This function used layout node pointer to check if it's corresponding to
viewport. There is no need for that, since `is_viewport_paintable()`
does exactly the same check without going through layout node.
2025-10-14 11:23:29 +02:00
InvalidUsernameException
1ffb0ca311 LibWeb: Remove redundant function parameter
This function can really only be called with a box and its containing
block, otherwise the results are not meaningful. Instead of passing
these two dependent values separatly, reduce it down to a single
parameter to not make the function appear more general than it is.
2025-10-14 10:23:27 +02:00
InvalidUsernameException
70c46e081d LibWeb: Correctly calculate nested positioned elements' static position
If there are multiple nested `position: fixed` or `position: absolute`
elements that are positioned based on their static position due to not
specifying any insets, we sum up all their ancestor offsets to calculate
said static position.

However, these offsets represent the offset to the containing block. So
summing up all the ancestor blocks will count elements multiple times
for cases where the containing block is not based on the closest element
capable of forming a containing block (i.e. absolute and fixed position
elements) when multiple such elements are nested.

With this change we only iterate over ancestors forming containing
blocks instead of over all ancestors boxes. To sum up everything between
the box currently being positioned and its containing block, we start
the iteration on the parent box of the current box.

This fixes 3 WPT tests that I could find. But these tests are not
intended to test the error cases fixed here, they just incidentally rely
on the correct behavior. As such, I have added dedicated tests myself.
Note that two of the tests already pass on master, but they seemed like
a good cases to have anyway.
2025-10-14 10:23:27 +02:00
110 changed files with 2795 additions and 281 deletions

View file

@ -49,6 +49,19 @@ jobs:
with: with:
ref: ${{ inputs.reference_to_build }} ref: ${{ inputs.reference_to_build }}
- name: 'Determine build commit hash'
id: build-commit
shell: bash
run: |
echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
- name: 'Use latest custom actions'
if: ${{ inputs.reference_to_build != '' && github.sha != steps.build-commit.outputs.sha }}
shell: bash
run: |
git fetch origin master
git checkout origin/master -- .github/actions
- name: "Set up environment" - name: "Set up environment"
uses: ./.github/actions/setup uses: ./.github/actions/setup
with: with:
@ -102,6 +115,17 @@ jobs:
run: | run: |
cpack cpack
# Inject the COMMIT file for older builds (before commit 5c5de0e30e04).
for package in ladybird-*.tar.gz; do
if ! tar -tzf "${package}" | grep -qx COMMIT; then
echo "${{ steps.build-commit.outputs.sha }}" > COMMIT
gunzip "${package}"
tar --append --file="${package%.gz}" COMMIT
gzip "${package%.gz}"
rm COMMIT
fi
done
- name: Save Caches - name: Save Caches
uses: ./.github/actions/cache-save uses: ./.github/actions/cache-save
with: with:
@ -115,23 +139,27 @@ jobs:
- name: Sanity-check the js repl - name: Sanity-check the js repl
shell: bash shell: bash
run: | run: |
set -e path="Build/distribution/ladybird-js-${{ matrix.package_type }}.tar.gz"
tar -xvzf Build/distribution/ladybird-js-${{ matrix.package_type }}.tar.gz if [ -f "${path}" ]; then
./bin/js -c "console.log('Hello, World\!');" > js-repl-out.txt tar -xvzf "${path}"
if ! grep -q "\"Hello, World\!\"" js-repl-out.txt; then bin/js -c "console.log('Hello, World\!');" > js-repl-out.txt
if ! grep -q "\"Hello, World\!\"" js-repl-out.txt; then
echo "Sanity check failed: \"Hello, World\!\" not found in output." echo "Sanity check failed: \"Hello, World\!\" not found in output."
exit 1 exit 1
fi
fi fi
- name: Sanity-check the wasm repl - name: Sanity-check the wasm repl
shell: bash shell: bash
run: | run: |
set -e path="Build/distribution/ladybird-wasm-${{ matrix.package_type }}.tar.gz"
tar -xvzf Build/distribution/ladybird-wasm-${{ matrix.package_type }}.tar.gz if [ -f "${path}" ]; then
./bin/wasm -e run_sanity_check -w ${{ github.workspace }}/Libraries/LibWasm/Tests/CI/ci-sanity-check.wasm > wasm-repl-out.txt tar -xvzf "${path}"
if ! grep -q "Hello, World\!" wasm-repl-out.txt; then bin/wasm -e run_sanity_check -w ${{ github.workspace }}/Libraries/LibWasm/Tests/CI/ci-sanity-check.wasm > wasm-repl-out.txt
if ! grep -q "Hello, World\!" wasm-repl-out.txt; then
echo "Sanity check failed: Hello, World\! not found in output." echo "Sanity check failed: Hello, World\! not found in output."
exit 1 exit 1
fi
fi fi
- name: Upload js package - name: Upload js package

View file

@ -1,5 +1,6 @@
add_subdirectory(LibCompress) add_subdirectory(LibCompress)
add_subdirectory(LibCrypto) add_subdirectory(LibCrypto)
add_subdirectory(LibDatabase)
add_subdirectory(LibDiff) add_subdirectory(LibDiff)
add_subdirectory(LibDNS) add_subdirectory(LibDNS)
add_subdirectory(LibGC) add_subdirectory(LibGC)

View file

@ -129,6 +129,27 @@ ByteString StandardPaths::videos_directory()
return LexicalPath::canonicalized_path(builder.to_byte_string()); return LexicalPath::canonicalized_path(builder.to_byte_string());
} }
ByteString StandardPaths::cache_directory()
{
#if defined(AK_OS_WINDOWS) || defined(AK_OS_HAIKU)
return user_data_directory();
#else
if (auto cache_directory = get_environment_if_not_empty("XDG_CACHE_HOME"sv); cache_directory.has_value())
return LexicalPath::canonicalized_path(*cache_directory);
StringBuilder builder;
builder.append(home_directory());
# if defined(AK_OS_MACOS)
builder.append("/Library/Caches"sv);
# else
builder.append("/.cache"sv);
# endif
return LexicalPath::canonicalized_path(builder.to_byte_string());
#endif
}
ByteString StandardPaths::config_directory() ByteString StandardPaths::config_directory()
{ {
StringBuilder builder; StringBuilder builder;

View file

@ -22,6 +22,7 @@ public:
static ByteString pictures_directory(); static ByteString pictures_directory();
static ByteString videos_directory(); static ByteString videos_directory();
static ByteString tempfile_directory(); static ByteString tempfile_directory();
static ByteString cache_directory();
static ByteString config_directory(); static ByteString config_directory();
static ByteString user_data_directory(); static ByteString user_data_directory();
static Vector<ByteString> system_data_directories(); static Vector<ByteString> system_data_directories();

View file

@ -870,4 +870,27 @@ ErrorOr<void> set_close_on_exec(int fd, bool enabled)
return {}; return {};
} }
ErrorOr<size_t> transfer_file_through_pipe(int source_fd, int target_fd, size_t source_offset, size_t source_length)
{
#if defined(AK_OS_LINUX)
auto sent = ::splice(source_fd, reinterpret_cast<off_t*>(&source_offset), target_fd, nullptr, source_length, SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
if (sent < 0)
return Error::from_syscall("send_file_to_pipe"sv, errno);
return sent;
#else
static auto page_size = PAGE_SIZE;
// mmap requires the offset to be page-aligned, so we must handle that here.
auto aligned_source_offset = (source_offset / page_size) * page_size;
auto offset_adjustment = source_offset - aligned_source_offset;
auto mapped_source_length = source_length + offset_adjustment;
// FIXME: We could use MappedFile here if we update it to support offsets and not auto-close the source fd.
auto* mapped = TRY(mmap(nullptr, mapped_source_length, PROT_READ, MAP_SHARED, source_fd, aligned_source_offset));
ScopeGuard guard { [&]() { (void)munmap(mapped, mapped_source_length); } };
return TRY(write(target_fd, { static_cast<u8*>(mapped) + offset_adjustment, source_length }));
#endif
}
} }

View file

@ -190,4 +190,6 @@ bool is_socket(int fd);
ErrorOr<void> sleep_ms(u32 milliseconds); ErrorOr<void> sleep_ms(u32 milliseconds);
ErrorOr<void> set_close_on_exec(int fd, bool enabled); ErrorOr<void> set_close_on_exec(int fd, bool enabled);
ErrorOr<size_t> transfer_file_through_pipe(int source_fd, int target_fd, size_t source_offset, size_t source_length);
} }

View file

@ -403,4 +403,14 @@ ErrorOr<void> kill(pid_t pid, int signal)
return {}; return {};
} }
ErrorOr<size_t> transfer_file_through_pipe(int source_fd, int target_fd, size_t source_offset, size_t source_length)
{
(void)source_fd;
(void)target_fd;
(void)source_offset;
(void)source_length;
return Error::from_string_literal("FIXME: Implement System::transfer_file_through_pipe on Windows (for HTTP disk cache)");
}
} }

View file

@ -0,0 +1,8 @@
set(SOURCES
Database.cpp
)
find_package(SQLite3 REQUIRED)
ladybird_lib(LibDatabase database EXPLICIT_SYMBOL_EXPORT)
target_link_libraries(LibDatabase PRIVATE LibCore SQLite::SQLite3)

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org> * Copyright (c) 2022-2025, Tim Flynn <trflynn89@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
@ -8,12 +8,11 @@
#include <AK/String.h> #include <AK/String.h>
#include <AK/Time.h> #include <AK/Time.h>
#include <LibCore/Directory.h> #include <LibCore/Directory.h>
#include <LibCore/StandardPaths.h> #include <LibDatabase/Database.h>
#include <LibWebView/Database.h>
#include <sqlite3.h> #include <sqlite3.h>
namespace WebView { namespace Database {
static constexpr StringView sql_error(int error_code) static constexpr StringView sql_error(int error_code)
{ {
@ -39,13 +38,25 @@ static constexpr StringView sql_error(int error_code)
} \ } \
}) })
ErrorOr<NonnullRefPtr<Database>> Database::create() #define ENUMERATE_SQL_TYPES \
{ __ENUMERATE_TYPE(String) \
// FIXME: Move this to a generic "Ladybird data directory" helper. __ENUMERATE_TYPE(UnixDateTime) \
auto database_path = ByteString::formatted("{}/Ladybird", Core::StandardPaths::user_data_directory()); __ENUMERATE_TYPE(i8) \
TRY(Core::Directory::create(database_path, Core::Directory::CreateDirectories::Yes)); __ENUMERATE_TYPE(i16) \
__ENUMERATE_TYPE(i32) \
__ENUMERATE_TYPE(long) \
__ENUMERATE_TYPE(long long) \
__ENUMERATE_TYPE(u8) \
__ENUMERATE_TYPE(u16) \
__ENUMERATE_TYPE(u32) \
__ENUMERATE_TYPE(unsigned long) \
__ENUMERATE_TYPE(unsigned long long) \
__ENUMERATE_TYPE(bool)
auto database_file = ByteString::formatted("{}/Ladybird.db", database_path); ErrorOr<NonnullRefPtr<Database>> Database::create(ByteString const& directory, StringView name)
{
TRY(Core::Directory::create(directory, Core::Directory::CreateDirectories::Yes));
auto database_file = ByteString::formatted("{}/{}.db", directory, name);
sqlite3* m_database { nullptr }; sqlite3* m_database { nullptr };
SQL_TRY(sqlite3_open(database_file.characters(), &m_database)); SQL_TRY(sqlite3_open(database_file.characters(), &m_database));
@ -67,7 +78,7 @@ Database::~Database()
sqlite3_close(m_database); sqlite3_close(m_database);
} }
ErrorOr<Database::StatementID> Database::prepare_statement(StringView statement) ErrorOr<StatementID> Database::prepare_statement(StringView statement)
{ {
sqlite3_stmt* prepared_statement { nullptr }; sqlite3_stmt* prepared_statement { nullptr };
SQL_TRY(sqlite3_prepare_v2(m_database, statement.characters_without_null_termination(), static_cast<int>(statement.length()), &prepared_statement, nullptr)); SQL_TRY(sqlite3_prepare_v2(m_database, statement.characters_without_null_termination(), static_cast<int>(statement.length()), &prepared_statement, nullptr));
@ -111,18 +122,21 @@ void Database::apply_placeholder(StatementID statement_id, int index, ValueType
StringView string { value }; StringView string { value };
SQL_MUST(sqlite3_bind_text(statement, index, string.characters_without_null_termination(), static_cast<int>(string.length()), SQLITE_TRANSIENT)); SQL_MUST(sqlite3_bind_text(statement, index, string.characters_without_null_termination(), static_cast<int>(string.length()), SQLITE_TRANSIENT));
} else if constexpr (IsSame<ValueType, UnixDateTime>) { } else if constexpr (IsSame<ValueType, UnixDateTime>) {
SQL_MUST(sqlite3_bind_int64(statement, index, value.offset_to_epoch().to_milliseconds())); apply_placeholder(statement_id, index, value.offset_to_epoch().to_milliseconds());
} else if constexpr (IsSame<ValueType, int>) { } else if constexpr (IsIntegral<ValueType>) {
SQL_MUST(sqlite3_bind_int(statement, index, value)); if constexpr (sizeof(ValueType) <= sizeof(int))
} else if constexpr (IsSame<ValueType, bool>) { SQL_MUST(sqlite3_bind_int(statement, index, static_cast<int>(value)));
SQL_MUST(sqlite3_bind_int(statement, index, static_cast<int>(value))); else
SQL_MUST(sqlite3_bind_int64(statement, index, static_cast<sqlite3_int64>(value)));
} else {
static_assert(DependentFalse<ValueType>);
} }
} }
template void Database::apply_placeholder(StatementID, int, String const&); #define __ENUMERATE_TYPE(type) \
template void Database::apply_placeholder(StatementID, int, UnixDateTime const&); template DATABASE_API void Database::apply_placeholder(StatementID, int, type const&);
template void Database::apply_placeholder(StatementID, int, int const&); ENUMERATE_SQL_TYPES
template void Database::apply_placeholder(StatementID, int, bool const&); #undef __ENUMERATE_TYPE
template<typename ValueType> template<typename ValueType>
ValueType Database::result_column(StatementID statement_id, int column) ValueType Database::result_column(StatementID statement_id, int column)
@ -133,20 +147,21 @@ ValueType Database::result_column(StatementID statement_id, int column)
auto const* text = reinterpret_cast<char const*>(sqlite3_column_text(statement, column)); auto const* text = reinterpret_cast<char const*>(sqlite3_column_text(statement, column));
return MUST(String::from_utf8(StringView { text, strlen(text) })); return MUST(String::from_utf8(StringView { text, strlen(text) }));
} else if constexpr (IsSame<ValueType, UnixDateTime>) { } else if constexpr (IsSame<ValueType, UnixDateTime>) {
auto milliseconds = sqlite3_column_int64(statement, column); auto milliseconds = result_column<sqlite3_int64>(statement_id, column);
return UnixDateTime::from_milliseconds_since_epoch(milliseconds); return UnixDateTime::from_milliseconds_since_epoch(milliseconds);
} else if constexpr (IsSame<ValueType, int>) { } else if constexpr (IsIntegral<ValueType>) {
return sqlite3_column_int(statement, column); if constexpr (sizeof(ValueType) <= sizeof(int))
} else if constexpr (IsSame<ValueType, bool>) { return static_cast<ValueType>(sqlite3_column_int(statement, column));
return static_cast<bool>(sqlite3_column_int(statement, column)); else
return static_cast<ValueType>(sqlite3_column_int64(statement, column));
} else {
static_assert(DependentFalse<ValueType>);
} }
VERIFY_NOT_REACHED();
} }
template String Database::result_column(StatementID, int); #define __ENUMERATE_TYPE(type) \
template UnixDateTime Database::result_column(StatementID, int); template DATABASE_API type Database::result_column(StatementID, int);
template int Database::result_column(StatementID, int); ENUMERATE_SQL_TYPES
template bool Database::result_column(StatementID, int); #undef __ENUMERATE_TYPE
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org> * Copyright (c) 2022-2025, Tim Flynn <trflynn89@ladybird.org>
* Copyright (c) 2023, Jelle Raaijmakers <jelle@ladybird.org> * Copyright (c) 2023, Jelle Raaijmakers <jelle@ladybird.org>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
@ -13,19 +13,18 @@
#include <AK/RefCounted.h> #include <AK/RefCounted.h>
#include <AK/StringView.h> #include <AK/StringView.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibWebView/Forward.h> #include <LibDatabase/Forward.h>
struct sqlite3; struct sqlite3;
struct sqlite3_stmt; struct sqlite3_stmt;
namespace WebView { namespace Database {
class WEBVIEW_API Database : public RefCounted<Database> { class DATABASE_API Database : public RefCounted<Database> {
public: public:
static ErrorOr<NonnullRefPtr<Database>> create(); static ErrorOr<NonnullRefPtr<Database>> create(ByteString const& directory, StringView name);
~Database(); ~Database();
using StatementID = size_t;
using OnResult = Function<void(StatementID)>; using OnResult = Function<void(StatementID)>;
ErrorOr<StatementID> prepare_statement(StringView statement); ErrorOr<StatementID> prepare_statement(StringView statement);

View file

@ -0,0 +1,18 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
#include <LibDatabase/Export.h>
namespace Database {
class Database;
using StatementID = size_t;
}

View file

@ -10,6 +10,11 @@
#include <AK/Checked.h> #include <AK/Checked.h>
#include <LibGfx/Bitmap.h> #include <LibGfx/Bitmap.h>
#include <LibGfx/ShareableBitmap.h> #include <LibGfx/ShareableBitmap.h>
#include <LibGfx/SkiaUtils.h>
#include <core/SkBitmap.h>
#include <core/SkColorSpace.h>
#include <core/SkImage.h>
#include <errno.h> #include <errno.h>
#ifdef AK_OS_MACOS #ifdef AK_OS_MACOS
@ -184,6 +189,24 @@ ErrorOr<NonnullRefPtr<Gfx::Bitmap>> Bitmap::cropped(Gfx::IntRect crop, Gfx::Colo
return new_bitmap; return new_bitmap;
} }
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::scaled(int const width, int const height, ScalingMode const scaling_mode) const
{
auto const source_info = SkImageInfo::Make(this->width(), this->height(), to_skia_color_type(format()), to_skia_alpha_type(format(), alpha_type()), nullptr);
SkPixmap const source_sk_pixmap(source_info, begin(), pitch());
SkBitmap source_sk_bitmap;
source_sk_bitmap.installPixels(source_sk_pixmap);
source_sk_bitmap.setImmutable();
auto scaled_bitmap = TRY(Gfx::Bitmap::create(format(), alpha_type(), { width, height }));
auto const scaled_info = SkImageInfo::Make(scaled_bitmap->width(), scaled_bitmap->height(), to_skia_color_type(scaled_bitmap->format()), to_skia_alpha_type(scaled_bitmap->format(), scaled_bitmap->alpha_type()), nullptr);
SkPixmap const scaled_sk_pixmap(scaled_info, scaled_bitmap->begin(), scaled_bitmap->pitch());
sk_sp<SkImage> source_sk_image = source_sk_bitmap.asImage();
if (!source_sk_image->scalePixels(scaled_sk_pixmap, to_skia_sampling_options(scaling_mode)))
return Error::from_string_literal("Unable to scale pixels for bitmap");
return scaled_bitmap;
}
ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::to_bitmap_backed_by_anonymous_buffer() const ErrorOr<NonnullRefPtr<Bitmap>> Bitmap::to_bitmap_backed_by_anonymous_buffer() const
{ {
if (m_buffer.is_valid()) { if (m_buffer.is_valid()) {

View file

@ -13,6 +13,7 @@
#include <LibGfx/Color.h> #include <LibGfx/Color.h>
#include <LibGfx/Forward.h> #include <LibGfx/Forward.h>
#include <LibGfx/Rect.h> #include <LibGfx/Rect.h>
#include <LibGfx/ScalingMode.h>
namespace Gfx { namespace Gfx {
@ -74,6 +75,8 @@ public:
ErrorOr<NonnullRefPtr<Gfx::Bitmap>> clone() const; ErrorOr<NonnullRefPtr<Gfx::Bitmap>> clone() const;
ErrorOr<NonnullRefPtr<Gfx::Bitmap>> cropped(Gfx::IntRect, Gfx::Color outside_color = Gfx::Color::Black) const; ErrorOr<NonnullRefPtr<Gfx::Bitmap>> cropped(Gfx::IntRect, Gfx::Color outside_color = Gfx::Color::Black) const;
ErrorOr<NonnullRefPtr<Bitmap>> scaled(int width, int height, ScalingMode scaling_mode) const;
ErrorOr<NonnullRefPtr<Gfx::Bitmap>> to_bitmap_backed_by_anonymous_buffer() const; ErrorOr<NonnullRefPtr<Gfx::Bitmap>> to_bitmap_backed_by_anonymous_buffer() const;
[[nodiscard]] ShareableBitmap to_shareable_bitmap() const; [[nodiscard]] ShareableBitmap to_shareable_bitmap() const;

View file

@ -21,6 +21,7 @@ enum class NetworkError {
MalformedUrl, MalformedUrl,
InvalidContentEncoding, InvalidContentEncoding,
RequestServerDied, RequestServerDied,
CacheReadFailed,
Unknown, Unknown,
}; };
@ -47,6 +48,8 @@ constexpr StringView network_error_to_string(NetworkError network_error)
return "Response could not be decoded with its Content-Encoding"sv; return "Response could not be decoded with its Content-Encoding"sv;
case NetworkError::RequestServerDied: case NetworkError::RequestServerDied:
return "RequestServer is currently unavailable"sv; return "RequestServer is currently unavailable"sv;
case NetworkError::CacheReadFailed:
return "RequestServer encountered an error reading a cached HTTP response"sv;
case NetworkError::Unknown: case NetworkError::Unknown:
return "An unexpected network error occurred"sv; return "An unexpected network error occurred"sv;
} }

View file

@ -40,7 +40,7 @@ WebIDL::ExceptionOr<String> CSSImageValue::to_string() const
} }
// https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation
WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSImageValue::create_an_internal_representation(PropertyNameAndID const& property) const WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSImageValue::create_an_internal_representation(PropertyNameAndID const& property, PerformTypeCheck perform_type_check) const
{ {
// If value is a CSSStyleValue subclass, // If value is a CSSStyleValue subclass,
// If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError.
@ -53,7 +53,7 @@ WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSImageValue::create_an_in
} }
return property_accepts_type(property.id(), ValueType::Image); return property_accepts_type(property.id(), ValueType::Image);
}(); }();
if (!matches_grammar) { if (perform_type_check == PerformTypeCheck::Yes && !matches_grammar) {
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Property '{}' does not accept <image>", property.name())) }; return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Property '{}' does not accept <image>", property.name())) };
} }

View file

@ -21,7 +21,7 @@ public:
virtual ~CSSImageValue() override = default; virtual ~CSSImageValue() override = default;
virtual WebIDL::ExceptionOr<String> to_string() const override; virtual WebIDL::ExceptionOr<String> to_string() const override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&) const override; virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const override;
private: private:
explicit CSSImageValue(JS::Realm&, NonnullRefPtr<StyleValue const> source_value); explicit CSSImageValue(JS::Realm&, NonnullRefPtr<StyleValue const> source_value);

View file

@ -68,7 +68,7 @@ WebIDL::ExceptionOr<String> CSSKeywordValue::to_string() const
} }
// https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation
WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSKeywordValue::create_an_internal_representation(PropertyNameAndID const& property) const WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSKeywordValue::create_an_internal_representation(PropertyNameAndID const& property, PerformTypeCheck perform_type_check) const
{ {
// If value is a CSSStyleValue subclass, // If value is a CSSStyleValue subclass,
// If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError.
@ -87,7 +87,7 @@ WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSKeywordValue::create_an_
auto keyword = keyword_from_string(m_value); auto keyword = keyword_from_string(m_value);
return keyword.has_value() && property_accepts_keyword(property.id(), keyword.value()); return keyword.has_value() && property_accepts_keyword(property.id(), keyword.value());
}(); }();
if (!matches_grammar) { if (perform_type_check == PerformTypeCheck::Yes && !matches_grammar) {
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Property '{}' does not accept the keyword '{}'", property.name(), m_value)) }; return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Property '{}' does not accept the keyword '{}'", property.name(), m_value)) };
} }

View file

@ -29,7 +29,7 @@ public:
WebIDL::ExceptionOr<void> set_value(FlyString value); WebIDL::ExceptionOr<void> set_value(FlyString value);
virtual WebIDL::ExceptionOr<String> to_string() const override; virtual WebIDL::ExceptionOr<String> to_string() const override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&) const override; virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const override;
private: private:
explicit CSSKeywordValue(JS::Realm&, FlyString value); explicit CSSKeywordValue(JS::Realm&, FlyString value);

View file

@ -26,7 +26,7 @@ void CSSMathValue::initialize(JS::Realm& realm)
} }
// https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation
WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSMathValue::create_an_internal_representation(PropertyNameAndID const& property) const WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSMathValue::create_an_internal_representation(PropertyNameAndID const& property, PerformTypeCheck perform_type_check) const
{ {
// If value is a CSSStyleValue subclass, // If value is a CSSStyleValue subclass,
// If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError.
@ -59,7 +59,7 @@ WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSMathValue::create_an_int
return false; return false;
}(); }();
if (!matches) if (perform_type_check == PerformTypeCheck::Yes && !matches)
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Property does not accept values of this type."sv }; return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Property does not accept values of this type."sv };
return CalculatedStyleValue::create(TRY(create_calculation_node(context)), type(), move(context)); return CalculatedStyleValue::create(TRY(create_calculation_node(context)), type(), move(context));

View file

@ -32,7 +32,7 @@ public:
}; };
virtual String serialize_math_value(Nested, Parens) const = 0; virtual String serialize_math_value(Nested, Parens) const = 0;
virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&) const final override; virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const final override;
protected: protected:
explicit CSSMathValue(JS::Realm&, Bindings::CSSMathOperator, NumericType); explicit CSSMathValue(JS::Realm&, Bindings::CSSMathOperator, NumericType);

View file

@ -7,6 +7,9 @@
#include "CSSMatrixComponent.h" #include "CSSMatrixComponent.h"
#include <LibWeb/Bindings/CSSMatrixComponentPrototype.h> #include <LibWeb/Bindings/CSSMatrixComponentPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/Geometry/DOMMatrix.h> #include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -82,4 +85,39 @@ WebIDL::ExceptionOr<void> CSSMatrixComponent::set_matrix(GC::Ref<Geometry::DOMMa
return {}; return {};
} }
WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> CSSMatrixComponent::create_style_value(PropertyNameAndID const& property) const
{
if (is_2d()) {
return TransformationStyleValue::create(property.id(), TransformFunction::Matrix,
{
NumberStyleValue::create(m_matrix->a()),
NumberStyleValue::create(m_matrix->b()),
NumberStyleValue::create(m_matrix->c()),
NumberStyleValue::create(m_matrix->d()),
NumberStyleValue::create(m_matrix->e()),
NumberStyleValue::create(m_matrix->f()),
});
}
return TransformationStyleValue::create(property.id(), TransformFunction::Matrix3d,
{
NumberStyleValue::create(m_matrix->m11()),
NumberStyleValue::create(m_matrix->m12()),
NumberStyleValue::create(m_matrix->m13()),
NumberStyleValue::create(m_matrix->m14()),
NumberStyleValue::create(m_matrix->m21()),
NumberStyleValue::create(m_matrix->m22()),
NumberStyleValue::create(m_matrix->m23()),
NumberStyleValue::create(m_matrix->m24()),
NumberStyleValue::create(m_matrix->m31()),
NumberStyleValue::create(m_matrix->m32()),
NumberStyleValue::create(m_matrix->m33()),
NumberStyleValue::create(m_matrix->m34()),
NumberStyleValue::create(m_matrix->m41()),
NumberStyleValue::create(m_matrix->m42()),
NumberStyleValue::create(m_matrix->m43()),
NumberStyleValue::create(m_matrix->m44()),
});
}
} }

View file

@ -33,6 +33,8 @@ public:
GC::Ref<Geometry::DOMMatrix> matrix() const { return m_matrix; } GC::Ref<Geometry::DOMMatrix> matrix() const { return m_matrix; }
WebIDL::ExceptionOr<void> set_matrix(GC::Ref<Geometry::DOMMatrix> matrix); WebIDL::ExceptionOr<void> set_matrix(GC::Ref<Geometry::DOMMatrix> matrix);
virtual WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> create_style_value(PropertyNameAndID const&) const override;
private: private:
explicit CSSMatrixComponent(JS::Realm&, Is2D, GC::Ref<Geometry::DOMMatrix>); explicit CSSMatrixComponent(JS::Realm&, Is2D, GC::Ref<Geometry::DOMMatrix>);

View file

@ -9,6 +9,8 @@
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSNumericValue.h> #include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/CSS/CSSUnitValue.h> #include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/Geometry/DOMMatrix.h> #include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -157,4 +159,12 @@ void CSSPerspective::set_is_2d(bool)
// The is2D attribute of a CSSPerspective object must, on setting, do nothing. // The is2D attribute of a CSSPerspective object must, on setting, do nothing.
} }
WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> CSSPerspective::create_style_value(PropertyNameAndID const& property) const
{
auto length = TRY(m_length.visit([&](auto const& value) {
return value->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No);
}));
return TransformationStyleValue::create(property.id(), TransformFunction::Perspective, { move(length) });
}
} }

View file

@ -37,6 +37,8 @@ public:
virtual void set_is_2d(bool value) override; virtual void set_is_2d(bool value) override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> create_style_value(PropertyNameAndID const&) const override;
private: private:
explicit CSSPerspective(JS::Realm&, CSSPerspectiveValueInternal); explicit CSSPerspective(JS::Realm&, CSSPerspectiveValueInternal);

View file

@ -8,6 +8,8 @@
#include <LibWeb/Bindings/CSSRotatePrototype.h> #include <LibWeb/Bindings/CSSRotatePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSUnitValue.h> #include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/Geometry/DOMMatrix.h> #include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -218,4 +220,22 @@ WebIDL::ExceptionOr<void> CSSRotate::set_angle(GC::Ref<CSSNumericValue> value)
return {}; return {};
} }
WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> CSSRotate::create_style_value(PropertyNameAndID const& property) const
{
if (is_2d()) {
return TransformationStyleValue::create(property.id(), TransformFunction::Rotate,
{
TRY(m_angle->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
});
}
return TransformationStyleValue::create(property.id(), TransformFunction::Rotate3d,
{
TRY(m_x->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
TRY(m_y->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
TRY(m_z->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
TRY(m_angle->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
});
}
} }

View file

@ -36,6 +36,8 @@ public:
WebIDL::ExceptionOr<void> set_z(CSSNumberish value); WebIDL::ExceptionOr<void> set_z(CSSNumberish value);
WebIDL::ExceptionOr<void> set_angle(GC::Ref<CSSNumericValue> value); WebIDL::ExceptionOr<void> set_angle(GC::Ref<CSSNumericValue> value);
virtual WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> create_style_value(PropertyNameAndID const&) const override;
private: private:
explicit CSSRotate(JS::Realm&, Is2D, GC::Ref<CSSNumericValue> x, GC::Ref<CSSNumericValue> y, GC::Ref<CSSNumericValue> z, GC::Ref<CSSNumericValue> angle); explicit CSSRotate(JS::Realm&, Is2D, GC::Ref<CSSNumericValue> x, GC::Ref<CSSNumericValue> y, GC::Ref<CSSNumericValue> z, GC::Ref<CSSNumericValue> angle);

View file

@ -9,6 +9,8 @@
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSNumericValue.h> #include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/CSS/CSSUnitValue.h> #include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/Geometry/DOMMatrix.h> #include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -199,4 +201,22 @@ WebIDL::ExceptionOr<void> CSSScale::set_z(CSSNumberish value)
return {}; return {};
} }
WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> CSSScale::create_style_value(PropertyNameAndID const& property) const
{
if (is_2d()) {
return TransformationStyleValue::create(property.id(), TransformFunction::Scale,
{
TRY(m_x->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
TRY(m_y->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
});
}
return TransformationStyleValue::create(property.id(), TransformFunction::Scale3d,
{
TRY(m_x->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
TRY(m_y->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
TRY(m_z->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
});
}
} }

View file

@ -33,6 +33,8 @@ public:
WebIDL::ExceptionOr<void> set_y(CSSNumberish value); WebIDL::ExceptionOr<void> set_y(CSSNumberish value);
WebIDL::ExceptionOr<void> set_z(CSSNumberish value); WebIDL::ExceptionOr<void> set_z(CSSNumberish value);
virtual WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> create_style_value(PropertyNameAndID const&) const override;
private: private:
explicit CSSScale(JS::Realm&, Is2D, GC::Ref<CSSNumericValue> x, GC::Ref<CSSNumericValue> y, GC::Ref<CSSNumericValue> z); explicit CSSScale(JS::Realm&, Is2D, GC::Ref<CSSNumericValue> x, GC::Ref<CSSNumericValue> y, GC::Ref<CSSNumericValue> z);

View file

@ -9,6 +9,8 @@
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSNumericValue.h> #include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/CSS/CSSUnitValue.h> #include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/Geometry/DOMMatrix.h> #include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -136,4 +138,13 @@ void CSSSkew::set_is_2d(bool)
// The is2D attribute of a CSSSkew, CSSSkewX, or CSSSkewY object must, on setting, do nothing. // The is2D attribute of a CSSSkew, CSSSkewX, or CSSSkewY object must, on setting, do nothing.
} }
WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> CSSSkew::create_style_value(PropertyNameAndID const& property) const
{
return TransformationStyleValue::create(property.id(), TransformFunction::Skew,
{
TRY(m_ax->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
TRY(m_ay->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
});
}
} }

View file

@ -32,6 +32,8 @@ public:
virtual void set_is_2d(bool value) override; virtual void set_is_2d(bool value) override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> create_style_value(PropertyNameAndID const&) const override;
private: private:
explicit CSSSkew(JS::Realm&, GC::Ref<CSSNumericValue> ax, GC::Ref<CSSNumericValue> ay); explicit CSSSkew(JS::Realm&, GC::Ref<CSSNumericValue> ax, GC::Ref<CSSNumericValue> ay);

View file

@ -9,6 +9,8 @@
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSNumericValue.h> #include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/CSS/CSSUnitValue.h> #include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/Geometry/DOMMatrix.h> #include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -106,4 +108,12 @@ void CSSSkewX::set_is_2d(bool)
// The is2D attribute of a CSSSkewX, CSSSkewXX, or CSSSkewXY object must, on setting, do nothing. // The is2D attribute of a CSSSkewX, CSSSkewXX, or CSSSkewXY object must, on setting, do nothing.
} }
WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> CSSSkewX::create_style_value(PropertyNameAndID const& property) const
{
return TransformationStyleValue::create(property.id(), TransformFunction::SkewX,
{
TRY(m_ax->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
});
}
} }

View file

@ -30,6 +30,8 @@ public:
virtual void set_is_2d(bool value) override; virtual void set_is_2d(bool value) override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> create_style_value(PropertyNameAndID const&) const override;
private: private:
explicit CSSSkewX(JS::Realm&, GC::Ref<CSSNumericValue> ax); explicit CSSSkewX(JS::Realm&, GC::Ref<CSSNumericValue> ax);

View file

@ -9,6 +9,8 @@
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSNumericValue.h> #include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/CSS/CSSUnitValue.h> #include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/Geometry/DOMMatrix.h> #include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -106,4 +108,12 @@ void CSSSkewY::set_is_2d(bool)
// The is2D attribute of a CSSSkewY, CSSSkewYX, or CSSSkewYY object must, on setting, do nothing. // The is2D attribute of a CSSSkewY, CSSSkewYX, or CSSSkewYY object must, on setting, do nothing.
} }
WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> CSSSkewY::create_style_value(PropertyNameAndID const& property) const
{
return TransformationStyleValue::create(property.id(), TransformFunction::SkewY,
{
TRY(m_ay->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
});
}
} }

View file

@ -30,6 +30,8 @@ public:
virtual void set_is_2d(bool value) override; virtual void set_is_2d(bool value) override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> create_style_value(PropertyNameAndID const&) const override;
private: private:
explicit CSSSkewY(JS::Realm&, GC::Ref<CSSNumericValue> ay); explicit CSSSkewY(JS::Realm&, GC::Ref<CSSNumericValue> ay);

View file

@ -123,7 +123,7 @@ WebIDL::ExceptionOr<String> CSSStyleValue::to_string() const
} }
// https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation
WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSStyleValue::create_an_internal_representation(PropertyNameAndID const&) const WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSStyleValue::create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const
{ {
// If value is a direct CSSStyleValue, // If value is a direct CSSStyleValue,
// Return value’s associated value. // Return value’s associated value.

View file

@ -37,7 +37,12 @@ public:
virtual WebIDL::ExceptionOr<String> to_string() const; virtual WebIDL::ExceptionOr<String> to_string() const;
virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&) const; // FIXME: Temporary hack. Really we want to pass something like a CalculationContext with the valid types and ranges.
enum class PerformTypeCheck : u8 {
No,
Yes,
};
virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const;
protected: protected:
explicit CSSStyleValue(JS::Realm&); explicit CSSStyleValue(JS::Realm&);

View file

@ -31,6 +31,8 @@ public:
virtual WebIDL::ExceptionOr<GC::Ref<Geometry::DOMMatrix>> to_matrix() const = 0; virtual WebIDL::ExceptionOr<GC::Ref<Geometry::DOMMatrix>> to_matrix() const = 0;
virtual WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> create_style_value(PropertyNameAndID const&) const = 0;
protected: protected:
explicit CSSTransformComponent(JS::Realm&, Is2D is_2d); explicit CSSTransformComponent(JS::Realm&, Is2D is_2d);

View file

@ -8,6 +8,9 @@
#include <LibWeb/Bindings/CSSTransformValuePrototype.h> #include <LibWeb/Bindings/CSSTransformValuePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSTransformComponent.h> #include <LibWeb/CSS/CSSTransformComponent.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/Geometry/DOMMatrix.h> #include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -155,4 +158,35 @@ WebIDL::ExceptionOr<String> CSSTransformValue::to_string() const
return builder.to_string_without_validation(); return builder.to_string_without_validation();
} }
// https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation
WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSTransformValue::create_an_internal_representation(PropertyNameAndID const& property, PerformTypeCheck perform_type_check) const
{
// NB: This can become <transform-function> or <transform-list>, and we don't know which is wanted without performing the type checking.
// We can worry about that if and when we ever do have a CSSTransformValue that isn't top-level.
VERIFY(perform_type_check == PerformTypeCheck::Yes);
// If value is a CSSStyleValue subclass,
// If value does not match the grammar of a list-valued property iteration of property, throw a TypeError.
//
// If any component of property’s CSS grammar has a limited numeric range, and the corresponding part of value
// is a CSSUnitValue that is outside of that range, replace that value with the result of wrapping it in a
// fresh CSSMathSum whose values internal slot contains only that part of value.
//
// Return the value.
// NB: We match <transform-function> if we have 1 transform. We match <transform-list> always.
if (m_transforms.size() == 1 && property_accepts_type(property.id(), ValueType::TransformFunction))
return TRY(m_transforms.first()->create_style_value(property));
if (property_accepts_type(property.id(), ValueType::TransformList)) {
StyleValueVector transforms;
transforms.ensure_capacity(m_transforms.size());
for (auto const transform : m_transforms)
transforms.unchecked_append(TRY(transform->create_style_value(property)));
return StyleValueList::create(move(transforms), StyleValueList::Separator::Space);
}
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Property does not accept values of this type."sv };
}
} }

View file

@ -32,6 +32,8 @@ public:
virtual WebIDL::ExceptionOr<String> to_string() const override; virtual WebIDL::ExceptionOr<String> to_string() const override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const override;
private: private:
explicit CSSTransformValue(JS::Realm&, Vector<GC::Ref<CSSTransformComponent>>); explicit CSSTransformValue(JS::Realm&, Vector<GC::Ref<CSSTransformComponent>>);

View file

@ -9,6 +9,8 @@
#include <LibWeb/Bindings/Intrinsics.h> #include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSNumericValue.h> #include <LibWeb/CSS/CSSNumericValue.h>
#include <LibWeb/CSS/CSSUnitValue.h> #include <LibWeb/CSS/CSSUnitValue.h>
#include <LibWeb/CSS/PropertyNameAndID.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/Geometry/DOMMatrix.h> #include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/WebIDL/ExceptionOr.h>
@ -178,4 +180,22 @@ WebIDL::ExceptionOr<void> CSSTranslate::set_z(GC::Ref<CSSNumericValue> z)
return {}; return {};
} }
WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> CSSTranslate::create_style_value(PropertyNameAndID const& property) const
{
if (is_2d()) {
return TransformationStyleValue::create(property.id(), TransformFunction::Translate,
{
TRY(m_x->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
TRY(m_y->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
});
}
return TransformationStyleValue::create(property.id(), TransformFunction::Translate3d,
{
TRY(m_x->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
TRY(m_y->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
TRY(m_z->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::No)),
});
}
} }

View file

@ -32,6 +32,8 @@ public:
WebIDL::ExceptionOr<void> set_y(GC::Ref<CSSNumericValue> value); WebIDL::ExceptionOr<void> set_y(GC::Ref<CSSNumericValue> value);
WebIDL::ExceptionOr<void> set_z(GC::Ref<CSSNumericValue> value); WebIDL::ExceptionOr<void> set_z(GC::Ref<CSSNumericValue> value);
virtual WebIDL::ExceptionOr<NonnullRefPtr<TransformationStyleValue const>> create_style_value(PropertyNameAndID const&) const override;
private: private:
explicit CSSTranslate(JS::Realm&, Is2D, GC::Ref<CSSNumericValue> x, GC::Ref<CSSNumericValue> y, GC::Ref<CSSNumericValue> z); explicit CSSTranslate(JS::Realm&, Is2D, GC::Ref<CSSNumericValue> x, GC::Ref<CSSNumericValue> y, GC::Ref<CSSNumericValue> z);

View file

@ -326,7 +326,7 @@ static Optional<CalculationNode::NumericValue> create_numeric_value(double value
} }
// https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation
WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSUnitValue::create_an_internal_representation(PropertyNameAndID const& property) const WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSUnitValue::create_an_internal_representation(PropertyNameAndID const& property, PerformTypeCheck perform_type_check) const
{ {
// If value is a CSSStyleValue subclass, // If value is a CSSStyleValue subclass,
// If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError.
@ -338,7 +338,7 @@ WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSUnitValue::create_an_int
// Return the value. // Return the value.
// NB: We store all custom properties as UnresolvedStyleValue, so we always need to create one here. // NB: We store all custom properties as UnresolvedStyleValue, so we always need to create one here.
if (property.is_custom_property()) { if (perform_type_check == PerformTypeCheck::Yes && property.is_custom_property()) {
auto token = [this]() { auto token = [this]() {
if (m_unit == "number"_fly_string) if (m_unit == "number"_fly_string)
return Parser::Token::create_number(Number { Number::Type::Number, m_value }); return Parser::Token::create_number(Number { Number::Type::Number, m_value });
@ -361,6 +361,35 @@ WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSUnitValue::create_an_int
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Unrecognized unit '{}'.", m_unit)) }; return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Unrecognized unit '{}'.", m_unit)) };
} }
if (perform_type_check == PerformTypeCheck::No) {
return value->visit(
[&](Number const& number) -> RefPtr<StyleValue const> {
return NumberStyleValue::create(number.value());
},
[&](Percentage const& percentage) -> RefPtr<StyleValue const> {
return PercentageStyleValue::create(percentage);
},
[&](Angle const& angle) -> RefPtr<StyleValue const> {
return AngleStyleValue::create(angle);
},
[&](Flex const& flex) -> RefPtr<StyleValue const> {
return FlexStyleValue::create(flex);
},
[&](Frequency const& frequency) -> RefPtr<StyleValue const> {
return FrequencyStyleValue::create(frequency);
},
[&](Length const& length) -> RefPtr<StyleValue const> {
return LengthStyleValue::create(length);
},
[&](Resolution const& resolution) -> RefPtr<StyleValue const> {
return ResolutionStyleValue::create(resolution);
},
[&](Time const& time) -> RefPtr<StyleValue const> {
return TimeStyleValue::create(time);
})
.release_nonnull();
}
// FIXME: Check types allowed by registered custom properties. // FIXME: Check types allowed by registered custom properties.
auto style_value = value->visit( auto style_value = value->visit(
[&](Number const& number) -> RefPtr<StyleValue const> { [&](Number const& number) -> RefPtr<StyleValue const> {

View file

@ -35,7 +35,7 @@ public:
virtual bool is_equal_numeric_value(GC::Ref<CSSNumericValue> other) const override; virtual bool is_equal_numeric_value(GC::Ref<CSSNumericValue> other) const override;
virtual Optional<SumValue> create_a_sum_value() const override; virtual Optional<SumValue> create_a_sum_value() const override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&) const override; virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<CalculationNode const>> create_calculation_node(CalculationContext const&) const override; virtual WebIDL::ExceptionOr<NonnullRefPtr<CalculationNode const>> create_calculation_node(CalculationContext const&) const override;
private: private:

View file

@ -168,7 +168,7 @@ WebIDL::ExceptionOr<String> CSSUnparsedValue::to_string() const
} }
// https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation // https://drafts.css-houdini.org/css-typed-om-1/#create-an-internal-representation
WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSUnparsedValue::create_an_internal_representation(PropertyNameAndID const&) const WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> CSSUnparsedValue::create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const
{ {
// If value is a CSSStyleValue subclass, // If value is a CSSStyleValue subclass,
// If value does not match the grammar of a list-valued property iteration of property, throw a TypeError. // If value does not match the grammar of a list-valued property iteration of property, throw a TypeError.

View file

@ -31,7 +31,7 @@ public:
virtual WebIDL::ExceptionOr<void> set_value_of_new_indexed_property(u32, JS::Value) override; virtual WebIDL::ExceptionOr<void> set_value_of_new_indexed_property(u32, JS::Value) override;
virtual WebIDL::ExceptionOr<String> to_string() const override; virtual WebIDL::ExceptionOr<String> to_string() const override;
virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&) const override; virtual WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_representation(PropertyNameAndID const&, PerformTypeCheck) const override;
private: private:
explicit CSSUnparsedValue(JS::Realm&, Vector<CSSUnparsedSegment>); explicit CSSUnparsedValue(JS::Realm&, Vector<CSSUnparsedSegment>);

View file

@ -484,7 +484,8 @@ private:
RefPtr<StyleValue const> parse_rotate_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_rotate_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_stroke_dasharray_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_stroke_dasharray_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_easing_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_easing_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_transform_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_transform_function_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_transform_list_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_transform_origin_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_transform_origin_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_transition_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_transition_value(TokenStream<ComponentValue>&);
RefPtr<StyleValue const> parse_transition_property_value(TokenStream<ComponentValue>&); RefPtr<StyleValue const> parse_transition_property_value(TokenStream<ComponentValue>&);

View file

@ -178,6 +178,10 @@ Optional<Parser::PropertyAndValue> Parser::parse_css_value_for_properties(Readon
return parsed.release_value(); return parsed.release_value();
if (auto parsed = parse_for_type(ValueType::String); parsed.has_value()) if (auto parsed = parse_for_type(ValueType::String); parsed.has_value())
return parsed.release_value(); return parsed.release_value();
if (auto parsed = parse_for_type(ValueType::TransformFunction); parsed.has_value())
return parsed.release_value();
if (auto parsed = parse_for_type(ValueType::TransformList); parsed.has_value())
return parsed.release_value();
if (auto parsed = parse_for_type(ValueType::Url); parsed.has_value()) if (auto parsed = parse_for_type(ValueType::Url); parsed.has_value())
return parsed.release_value(); return parsed.release_value();
@ -802,10 +806,6 @@ Parser::ParseErrorOr<NonnullRefPtr<StyleValue const>> Parser::parse_css_value(Pr
if (auto parsed_value = parse_touch_action_value(tokens); parsed_value && !tokens.has_next_token()) if (auto parsed_value = parse_touch_action_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull(); return parsed_value.release_nonnull();
return ParseError::SyntaxError; return ParseError::SyntaxError;
case PropertyID::Transform:
if (auto parsed_value = parse_transform_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull();
return ParseError::SyntaxError;
case PropertyID::TransformOrigin: case PropertyID::TransformOrigin:
if (auto parsed_value = parse_transform_origin_value(tokens); parsed_value && !tokens.has_next_token()) if (auto parsed_value = parse_transform_origin_value(tokens); parsed_value && !tokens.has_next_token())
return parsed_value.release_nonnull(); return parsed_value.release_nonnull();
@ -4913,119 +4913,6 @@ RefPtr<StyleValue const> Parser::parse_touch_action_value(TokenStream<ComponentV
return StyleValueList::create(move(parsed_values), StyleValueList::Separator::Space); return StyleValueList::create(move(parsed_values), StyleValueList::Separator::Space);
} }
// https://www.w3.org/TR/css-transforms-1/#transform-property
RefPtr<StyleValue const> Parser::parse_transform_value(TokenStream<ComponentValue>& tokens)
{
// <transform> = none | <transform-list>
// <transform-list> = <transform-function>+
if (auto none = parse_all_as_single_keyword_value(tokens, Keyword::None))
return none;
StyleValueVector transformations;
auto transaction = tokens.begin_transaction();
while (tokens.has_next_token()) {
auto const& part = tokens.consume_a_token();
if (!part.is_function())
return nullptr;
auto maybe_function = transform_function_from_string(part.function().name);
if (!maybe_function.has_value())
return nullptr;
auto context_guard = push_temporary_value_parsing_context(FunctionContext { part.function().name });
auto function = maybe_function.release_value();
auto function_metadata = transform_function_metadata(function);
auto function_tokens = TokenStream { part.function().value };
auto arguments = parse_a_comma_separated_list_of_component_values(function_tokens);
if (arguments.size() > function_metadata.parameters.size()) {
ErrorReporter::the().report(InvalidValueError {
.value_type = "<transform-function>"_fly_string,
.value_string = part.function().original_source_text(),
.description = MUST(String::formatted("Too many arguments to {}. max: {}", part.function().name, function_metadata.parameters.size())),
});
return nullptr;
}
if (arguments.size() < function_metadata.parameters.size() && function_metadata.parameters[arguments.size()].required) {
ErrorReporter::the().report(InvalidValueError {
.value_type = "<transform-function>"_fly_string,
.value_string = part.function().original_source_text(),
.description = MUST(String::formatted("Required parameter at position {} is missing", arguments.size())),
});
return nullptr;
}
StyleValueVector values;
for (auto argument_index = 0u; argument_index < arguments.size(); ++argument_index) {
TokenStream argument_tokens { arguments[argument_index] };
argument_tokens.discard_whitespace();
switch (function_metadata.parameters[argument_index].type) {
case TransformFunctionParameterType::Angle: {
// These are `<angle> | <zero>` in the spec, so we have to check for both kinds.
if (auto angle_value = parse_angle_value(argument_tokens)) {
values.append(angle_value.release_nonnull());
break;
}
if (argument_tokens.next_token().is(Token::Type::Number) && argument_tokens.next_token().token().number_value() == 0) {
argument_tokens.discard_a_token(); // 0
values.append(AngleStyleValue::create(Angle::make_degrees(0)));
break;
}
return nullptr;
}
case TransformFunctionParameterType::Length:
case TransformFunctionParameterType::LengthNone: {
if (auto length_value = parse_length_value(argument_tokens)) {
values.append(length_value.release_nonnull());
break;
}
if (function_metadata.parameters[argument_index].type == TransformFunctionParameterType::LengthNone
&& argument_tokens.next_token().is_ident("none"sv)) {
argument_tokens.discard_a_token(); // none
values.append(KeywordStyleValue::create(Keyword::None));
break;
}
return nullptr;
}
case TransformFunctionParameterType::LengthPercentage: {
if (auto length_percentage_value = parse_length_percentage_value(argument_tokens)) {
values.append(length_percentage_value.release_nonnull());
break;
}
return nullptr;
}
case TransformFunctionParameterType::Number: {
if (auto number_value = parse_number_value(argument_tokens)) {
values.append(number_value.release_nonnull());
break;
}
return nullptr;
}
case TransformFunctionParameterType::NumberPercentage: {
if (auto number_percentage_value = parse_number_percentage_value(argument_tokens)) {
values.append(number_percentage_value.release_nonnull());
break;
}
return nullptr;
}
}
argument_tokens.discard_whitespace();
if (argument_tokens.has_next_token())
return nullptr;
}
transformations.append(TransformationStyleValue::create(PropertyID::Transform, function, move(values)));
}
transaction.commit();
return StyleValueList::create(move(transformations), StyleValueList::Separator::Space);
}
// https://www.w3.org/TR/css-transforms-1/#propdef-transform-origin // https://www.w3.org/TR/css-transforms-1/#propdef-transform-origin
RefPtr<StyleValue const> Parser::parse_transform_origin_value(TokenStream<ComponentValue>& tokens) RefPtr<StyleValue const> Parser::parse_transform_origin_value(TokenStream<ComponentValue>& tokens)
{ {

View file

@ -67,6 +67,7 @@
#include <LibWeb/CSS/StyleValues/StyleValueList.h> #include <LibWeb/CSS/StyleValues/StyleValueList.h>
#include <LibWeb/CSS/StyleValues/SuperellipseStyleValue.h> #include <LibWeb/CSS/StyleValues/SuperellipseStyleValue.h>
#include <LibWeb/CSS/StyleValues/TimeStyleValue.h> #include <LibWeb/CSS/StyleValues/TimeStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/CSS/StyleValues/URLStyleValue.h> #include <LibWeb/CSS/StyleValues/URLStyleValue.h>
#include <LibWeb/CSS/StyleValues/UnicodeRangeStyleValue.h> #include <LibWeb/CSS/StyleValues/UnicodeRangeStyleValue.h>
#include <LibWeb/CSS/StyleValues/UnresolvedStyleValue.h> #include <LibWeb/CSS/StyleValues/UnresolvedStyleValue.h>
@ -4700,6 +4701,131 @@ NonnullRefPtr<StyleValue const> Parser::resolve_unresolved_style_value(DOM::Abst
return parsed_value.release_value(); return parsed_value.release_value();
} }
// https://drafts.csswg.org/css-transforms-1/#typedef-transform-function
RefPtr<StyleValue const> Parser::parse_transform_function_value(TokenStream<ComponentValue>& tokens)
{
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto const& part = tokens.consume_a_token();
if (!part.is_function())
return nullptr;
auto maybe_function = transform_function_from_string(part.function().name);
if (!maybe_function.has_value())
return nullptr;
auto context_guard = push_temporary_value_parsing_context(FunctionContext { part.function().name });
auto function = maybe_function.release_value();
auto function_metadata = transform_function_metadata(function);
auto function_tokens = TokenStream { part.function().value };
auto arguments = parse_a_comma_separated_list_of_component_values(function_tokens);
if (arguments.size() > function_metadata.parameters.size()) {
ErrorReporter::the().report(InvalidValueError {
.value_type = "<transform-function>"_fly_string,
.value_string = part.function().original_source_text(),
.description = MUST(String::formatted("Too many arguments to {}. max: {}", part.function().name, function_metadata.parameters.size())),
});
return nullptr;
}
if (arguments.size() < function_metadata.parameters.size() && function_metadata.parameters[arguments.size()].required) {
ErrorReporter::the().report(InvalidValueError {
.value_type = "<transform-function>"_fly_string,
.value_string = part.function().original_source_text(),
.description = MUST(String::formatted("Required parameter at position {} is missing", arguments.size())),
});
return nullptr;
}
StyleValueVector values;
for (auto argument_index = 0u; argument_index < arguments.size(); ++argument_index) {
TokenStream argument_tokens { arguments[argument_index] };
argument_tokens.discard_whitespace();
switch (function_metadata.parameters[argument_index].type) {
case TransformFunctionParameterType::Angle: {
// These are `<angle> | <zero>` in the spec, so we have to check for both kinds.
if (auto angle_value = parse_angle_value(argument_tokens)) {
values.append(angle_value.release_nonnull());
break;
}
if (argument_tokens.next_token().is(Token::Type::Number) && argument_tokens.next_token().token().number_value() == 0) {
argument_tokens.discard_a_token(); // 0
values.append(AngleStyleValue::create(Angle::make_degrees(0)));
break;
}
return nullptr;
}
case TransformFunctionParameterType::Length:
case TransformFunctionParameterType::LengthNone: {
if (auto length_value = parse_length_value(argument_tokens)) {
values.append(length_value.release_nonnull());
break;
}
if (function_metadata.parameters[argument_index].type == TransformFunctionParameterType::LengthNone
&& argument_tokens.next_token().is_ident("none"sv)) {
argument_tokens.discard_a_token(); // none
values.append(KeywordStyleValue::create(Keyword::None));
break;
}
return nullptr;
}
case TransformFunctionParameterType::LengthPercentage: {
if (auto length_percentage_value = parse_length_percentage_value(argument_tokens)) {
values.append(length_percentage_value.release_nonnull());
break;
}
return nullptr;
}
case TransformFunctionParameterType::Number: {
if (auto number_value = parse_number_value(argument_tokens)) {
values.append(number_value.release_nonnull());
break;
}
return nullptr;
}
case TransformFunctionParameterType::NumberPercentage: {
if (auto number_percentage_value = parse_number_percentage_value(argument_tokens)) {
values.append(number_percentage_value.release_nonnull());
break;
}
return nullptr;
}
}
argument_tokens.discard_whitespace();
if (argument_tokens.has_next_token())
return nullptr;
}
transaction.commit();
return TransformationStyleValue::create(PropertyID::Transform, function, move(values));
}
// https://drafts.csswg.org/css-transforms-1/#typedef-transform-list
RefPtr<StyleValue const> Parser::parse_transform_list_value(TokenStream<ComponentValue>& tokens)
{
// <transform-list> = <transform-function>+
// https://www.w3.org/TR/css-transforms-1/#transform-property
StyleValueVector transformations;
auto transaction = tokens.begin_transaction();
while (tokens.has_next_token()) {
if (auto maybe_function = parse_transform_function_value(tokens)) {
transformations.append(maybe_function.release_nonnull());
tokens.discard_whitespace();
continue;
}
break;
}
if (transformations.is_empty())
return {};
transaction.commit();
return StyleValueList::create(move(transformations), StyleValueList::Separator::Space);
}
RefPtr<StyleValue const> Parser::parse_value(ValueType value_type, TokenStream<ComponentValue>& tokens) RefPtr<StyleValue const> Parser::parse_value(ValueType value_type, TokenStream<ComponentValue>& tokens)
{ {
switch (value_type) { switch (value_type) {
@ -4758,6 +4884,10 @@ RefPtr<StyleValue const> Parser::parse_value(ValueType value_type, TokenStream<C
return parse_string_value(tokens); return parse_string_value(tokens);
case ValueType::Time: case ValueType::Time:
return parse_time_value(tokens); return parse_time_value(tokens);
case ValueType::TransformFunction:
return parse_transform_function_value(tokens);
case ValueType::TransformList:
return parse_transform_list_value(tokens);
case ValueType::Url: case ValueType::Url:
return parse_url_value(tokens); return parse_url_value(tokens);
} }

View file

@ -3783,7 +3783,13 @@
"affects-layout": false, "affects-layout": false,
"affects-stacking-context": true, "affects-stacking-context": true,
"percentages-resolve-to": "length", "percentages-resolve-to": "length",
"multiplicity": "list" "multiplicity": "list",
"valid-types": [
"transform-list"
],
"valid-identifiers": [
"none"
]
}, },
"transform-box": { "transform-box": {
"animation-type": "discrete", "animation-type": "discrete",

View file

@ -7,7 +7,6 @@
#pragma once #pragma once
#include <AK/FlyString.h> #include <AK/FlyString.h>
#include <AK/GenericShorthands.h>
#include <AK/Optional.h> #include <AK/Optional.h>
#include <LibWeb/CSS/PropertyID.h> #include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/PropertyName.h> #include <LibWeb/CSS/PropertyName.h>

View file

@ -62,7 +62,7 @@ static WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_r
// To create an internal representation, given a string property and a string or CSSStyleValue value: // To create an internal representation, given a string property and a string or CSSStyleValue value:
return value.visit( return value.visit(
[&property](GC::Root<CSSStyleValue> const& css_style_value) { [&property](GC::Root<CSSStyleValue> const& css_style_value) {
return css_style_value->create_an_internal_representation(property); return css_style_value->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::Yes);
}, },
[&](String const& css_text) -> WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> { [&](String const& css_text) -> WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> {
// If value is a USVString, // If value is a USVString,
@ -71,7 +71,7 @@ static WebIDL::ExceptionOr<NonnullRefPtr<StyleValue const>> create_an_internal_r
// FIXME: Avoid passing name as a string, as it gets immediately converted back to PropertyNameAndID. // FIXME: Avoid passing name as a string, as it gets immediately converted back to PropertyNameAndID.
auto result = TRY(CSSStyleValue::parse_a_css_style_value(vm, property.name(), css_text, CSSStyleValue::ParseMultiple::No)); auto result = TRY(CSSStyleValue::parse_a_css_style_value(vm, property.name(), css_text, CSSStyleValue::ParseMultiple::No));
// AD-HOC: Result is a CSSStyleValue but we want an internal representation, so... convert it again I guess? // AD-HOC: Result is a CSSStyleValue but we want an internal representation, so... convert it again I guess?
return result.get<GC::Ref<CSSStyleValue>>()->create_an_internal_representation(property); return result.get<GC::Ref<CSSStyleValue>>()->create_an_internal_representation(property, CSSStyleValue::PerformTypeCheck::Yes);
}); });
} }

View file

@ -61,6 +61,10 @@ Optional<ValueType> value_type_from_string(StringView string)
return ValueType::String; return ValueType::String;
if (string.equals_ignoring_ascii_case("time"sv)) if (string.equals_ignoring_ascii_case("time"sv))
return ValueType::Time; return ValueType::Time;
if (string.equals_ignoring_ascii_case("transform-function"sv))
return ValueType::TransformFunction;
if (string.equals_ignoring_ascii_case("transform-list"sv))
return ValueType::TransformList;
if (string.equals_ignoring_ascii_case("url"sv)) if (string.equals_ignoring_ascii_case("url"sv))
return ValueType::Url; return ValueType::Url;
return {}; return {};
@ -123,6 +127,10 @@ StringView value_type_to_string(ValueType value_type)
return "String"sv; return "String"sv;
case Web::CSS::ValueType::Time: case Web::CSS::ValueType::Time:
return "Time"sv; return "Time"sv;
case Web::CSS::ValueType::TransformFunction:
return "TransformFunction"sv;
case Web::CSS::ValueType::TransformList:
return "TransformList"sv;
case Web::CSS::ValueType::Url: case Web::CSS::ValueType::Url:
return "Url"sv; return "Url"sv;
} }

View file

@ -40,6 +40,8 @@ enum class ValueType : u8 {
Resolution, Resolution,
String, String,
Time, Time,
TransformFunction,
TransformList,
Url, Url,
}; };

View file

@ -3329,11 +3329,6 @@ StringView Document::visibility_state() const
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }
void Document::set_visibility_state(Badge<HTML::BrowsingContext>, HTML::VisibilityState visibility_state)
{
m_visibility_state = visibility_state;
}
// https://html.spec.whatwg.org/multipage/interaction.html#update-the-visibility-state // https://html.spec.whatwg.org/multipage/interaction.html#update-the-visibility-state
void Document::update_the_visibility_state(HTML::VisibilityState visibility_state) void Document::update_the_visibility_state(HTML::VisibilityState visibility_state)
{ {

View file

@ -542,9 +542,6 @@ public:
// https://html.spec.whatwg.org/multipage/interaction.html#update-the-visibility-state // https://html.spec.whatwg.org/multipage/interaction.html#update-the-visibility-state
void update_the_visibility_state(HTML::VisibilityState); void update_the_visibility_state(HTML::VisibilityState);
// NOTE: This does not fire any events, unlike update_the_visibility_state().
void set_visibility_state(Badge<HTML::BrowsingContext>, HTML::VisibilityState);
void run_the_resize_steps(); void run_the_resize_steps();
void run_the_scroll_steps(); void run_the_scroll_steps();

View file

@ -1245,7 +1245,7 @@ Vector<CSSPixelRect> Element::get_client_rects() const
Vector<CSSPixelRect> rects; Vector<CSSPixelRect> rects;
if (auto const* paintable_box = this->paintable_box()) { if (auto const* paintable_box = this->paintable_box()) {
transform = Gfx::extract_2d_affine_transform(paintable_box->transform()); transform = Gfx::extract_2d_affine_transform(paintable_box->transform());
for (auto const* containing_block = paintable->containing_block(); !containing_block->is_viewport(); containing_block = containing_block->containing_block()) { for (auto const* containing_block = paintable->containing_block(); !containing_block->is_viewport_paintable(); containing_block = containing_block->containing_block()) {
transform = Gfx::extract_2d_affine_transform(containing_block->transform()).multiply(transform); transform = Gfx::extract_2d_affine_transform(containing_block->transform()).multiply(transform);
} }

View file

@ -788,6 +788,9 @@ void HTMLElement::set_subtree_inertness(bool is_inert)
html_element.set_inert(is_inert); html_element.set_inert(is_inert);
return TraversalDecision::Continue; return TraversalDecision::Continue;
}); });
if (auto paintable_box = this->paintable_box())
paintable_box->set_needs_paint_only_properties_update(true);
} }
WebIDL::ExceptionOr<void> HTMLElement::cloned(Web::DOM::Node& copy, bool clone_children) const WebIDL::ExceptionOr<void> HTMLElement::cloned(Web::DOM::Node& copy, bool clone_children) const

View file

@ -8,6 +8,7 @@
#include <AK/OwnPtr.h> #include <AK/OwnPtr.h>
#include <LibGfx/Bitmap.h> #include <LibGfx/Bitmap.h>
#include <LibWeb/Bindings/ImageBitmapPrototype.h>
#include <LibWeb/Bindings/PlatformObject.h> #include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/Bindings/Serializable.h> #include <LibWeb/Bindings/Serializable.h>
#include <LibWeb/Bindings/Transferable.h> #include <LibWeb/Bindings/Transferable.h>
@ -23,6 +24,7 @@ struct ImageBitmapOptions {
// FIXME: Implement the rest of the fields // FIXME: Implement the rest of the fields
Optional<WebIDL::UnsignedLong> resize_width; Optional<WebIDL::UnsignedLong> resize_width;
Optional<WebIDL::UnsignedLong> resize_height; Optional<WebIDL::UnsignedLong> resize_height;
Bindings::ResizeQuality resize_quality = Bindings::ResizeQuality::Low;
}; };
class ImageBitmap final : public Bindings::PlatformObject class ImageBitmap final : public Bindings::PlatformObject

View file

@ -28,5 +28,5 @@ dictionary ImageBitmapOptions {
// FIXME: ColorSpaceConversion colorSpaceConversion = "default"; // FIXME: ColorSpaceConversion colorSpaceConversion = "default";
[EnforceRange] unsigned long resizeWidth; [EnforceRange] unsigned long resizeWidth;
[EnforceRange] unsigned long resizeHeight; [EnforceRange] unsigned long resizeHeight;
// FIXME: ResizeQuality resizeQuality = "low"; ResizeQuality resizeQuality = "low";
}; };

View file

@ -12,8 +12,10 @@
#include <AK/Utf8View.h> #include <AK/Utf8View.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibGC/Function.h> #include <LibGC/Function.h>
#include <LibGfx/ScalingMode.h>
#include <LibJS/Runtime/Array.h> #include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/TypedArray.h> #include <LibJS/Runtime/TypedArray.h>
#include <LibWeb/Bindings/ImageBitmapPrototype.h>
#include <LibWeb/Bindings/MainThreadVM.h> #include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h> #include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h>
#include <LibWeb/Crypto/Crypto.h> #include <LibWeb/Crypto/Crypto.h>
@ -198,10 +200,49 @@ static ErrorOr<NonnullRefPtr<Gfx::Bitmap>> crop_to_the_source_rectangle_with_for
// 6. Let output be the rectangle on the plane denoted by sourceRectangle. // 6. Let output be the rectangle on the plane denoted by sourceRectangle.
auto output = TRY(input->cropped(source_rectangle, Gfx::Color::Transparent)); auto output = TRY(input->cropped(source_rectangle, Gfx::Color::Transparent));
// FIXME: 7. Scale output to the size specified by outputWidth and outputHeight. The user agent should use the // 7. Scale output to the size specified by outputWidth and outputHeight. The user agent should use the
// value of the resizeQuality option to guide the choice of scaling algorithm. // value of the resizeQuality option to guide the choice of scaling algorithm.
(void)output_width; struct ScalingPass {
(void)output_height; Gfx::ScalingMode mode { Gfx::ScalingMode::None };
int width { 0 };
int height { 0 };
};
Vector<ScalingPass> scaling_passes;
switch (options.has_value() ? options->resize_quality : Bindings::ResizeQuality::Low) {
// NOTE: The spec mentions Bicubic or Lanczos scaling as higher quality options; however, Skia does not implement the latter, so for now we will use SkCubicResampler::Mitchell() for both medium and high
case Bindings::ResizeQuality::High:
// The "high" value indicates a preference for a high level of image interpolation quality. High-quality image interpolation may be more computationally expensive than lower settings.
case Bindings::ResizeQuality::Medium:
// The "medium" value indicates a preference for a medium level of image interpolation quality.
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::BoxSampling, .width = output_width, .height = output_height });
break;
case Bindings::ResizeQuality::Low:
// The "low" value indicates a preference for a low level of image interpolation quality. Low-quality image interpolation may be more computationally efficient than higher settings.
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::BilinearBlend, .width = output_width, .height = output_height });
break;
case Bindings::ResizeQuality::Pixelated: {
// The "pixelated" value indicates a preference for scaling the image to preserve the pixelation of the original as much as possible, with minor smoothing as necessary to avoid distorting the image when the target size is not a clean multiple of the original.
// To implement "pixelated", for each axis independently, first determine the integer multiple of its natural size that puts it closest to the target size and is greater than zero. Scale it to this integer-multiple-size using nearest neighbor,
auto determine_closest_multiple = [](int const source_length, int const output_length) {
// NOTE: The previous cropping action would've failed if the source bitmap we are scaling had invalid lengths. Given the precondition that are lengths are always > 0, this integer division is safe from divide-by-zero and the quotient truncation is also safe as we will always round down
ASSERT(source_length > 0);
return output_length / source_length;
};
auto const source_width = source_rectangle.width();
auto const source_height = source_rectangle.height();
auto const width_multiple = determine_closest_multiple(source_width, output_width);
auto const height_multiple = determine_closest_multiple(source_height, output_height);
if (width_multiple > 0 && height_multiple > 0)
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::NearestNeighbor, .width = source_width * width_multiple, .height = source_height * height_multiple });
// then scale it the rest of the way to the target size using bilinear interpolation.
scaling_passes.append(ScalingPass { .mode = Gfx::ScalingMode::BilinearBlend, .width = output_width, .height = output_height });
} break;
}
for (ScalingPass& scaling_pass : scaling_passes) {
output = TRY(output->scaled(scaling_pass.width, scaling_pass.height, scaling_pass.mode));
}
// FIXME: 8. If the value of the imageOrientation member of options is "flipY", output must be flipped vertically, // FIXME: 8. If the value of the imageOrientation member of options is "flipY", output must be flipped vertically,
// disregarding any image orientation metadata of the source (such as EXIF metadata), if any. [EXIF] // disregarding any image orientation metadata of the source (such as EXIF metadata), if any. [EXIF]

View file

@ -1293,20 +1293,21 @@ void possibly_update_the_key_generator(GC::Ref<ObjectStore> store, GC::Ref<Key>
return; return;
// 2. Let value be the value of key. // 2. Let value be the value of key.
auto temp_value = key->value_as_double(); auto value = key->value_as_double();
// 3. Set value to the minimum of value and 2^53 (9007199254740992). // 3. Set value to the minimum of value and 2^53 (9007199254740992).
temp_value = min(temp_value, MAX_KEY_GENERATOR_VALUE); value = min(value, MAX_KEY_GENERATOR_VALUE);
// 4. Set value to the largest integer not greater than value. // 4. Set value to the largest integer not greater than value.
u64 value = floor(temp_value); value = floor(value);
// 5. Let generator be store’s key generator. // 5. Let generator be store’s key generator.
auto& generator = store->key_generator(); auto& generator = store->key_generator();
// 6. If value is greater than or equal to generator’s current number, then set generator’s current number to value + 1. // 6. If value is greater than or equal to generator’s current number, then set generator’s current number to value + 1.
if (value >= generator.current_number()) if (value >= static_cast<double>(generator.current_number())) {
generator.set(value + 1); generator.set(static_cast<u64>(value + 1));
}
} }
// https://w3c.github.io/IndexedDB/#inject-a-key-into-a-value-using-a-key-path // https://w3c.github.io/IndexedDB/#inject-a-key-into-a-value-using-a-key-path

View file

@ -809,14 +809,18 @@ void BlockFormattingContext::layout_block_level_box(Box const& box, BlockContain
// This monster basically means: "a ListItemBox that does not have specified content in the ::marker pseudo-element". // This monster basically means: "a ListItemBox that does not have specified content in the ::marker pseudo-element".
// This happens for ::marker with content 'normal'. // This happens for ::marker with content 'normal'.
// FIXME: We currently so not support ListItemBox-es generated by pseudo-elements. We will need to, eventually. // FIXME: We currently so not support ListItemBox-es generated by pseudo-elements. We will need to, eventually.
ListItemBox const* li_box = as_if<ListItemBox>(box); auto const* li_box = as_if<ListItemBox>(box);
bool is_list_item_box_without_css_content = li_box && (!(box.dom_node() && box.dom_node()->is_element() && as_if<DOM::Element const>(box.dom_node())->computed_properties(CSS::PseudoElement::Marker)->property(CSS::PropertyID::Content).is_content())); auto is_list_item_box_without_css_content = li_box != nullptr;
if (auto const* dom_node = as_if<DOM::Element>(box.dom_node()); li_box && dom_node) {
if (auto const computed_properties = dom_node->computed_properties(CSS::PseudoElement::Marker))
is_list_item_box_without_css_content = !computed_properties->property(CSS::PropertyID::Content).is_content();
}
// Before we insert the children of a list item we need to know the location of the marker. // Before we insert the children of a list item we need to know the location of the marker.
// If we do not do this then left-floating elements inside the list item will push the marker to the right, // If we do not do this then left-floating elements inside the list item will push the marker to the right,
// in some cases even causing it to overlap with the non-floating content of the list. // in some cases even causing it to overlap with the non-floating content of the list.
CSSPixels left_space_before_children_formatted; CSSPixels left_space_before_children_formatted;
if (is_list_item_box_without_css_content) { if (is_list_item_box_without_css_content && li_box->marker()) {
// We need to ensure that our height and width are final before we calculate our left offset. // We need to ensure that our height and width are final before we calculate our left offset.
// Otherwise, the y at which we calculate the intrusion by floats might be incorrect. // Otherwise, the y at which we calculate the intrusion by floats might be incorrect.
ensure_sizes_correct_for_left_offset_calculation(*li_box); ensure_sizes_correct_for_left_offset_calculation(*li_box);

View file

@ -1163,17 +1163,19 @@ void FormattingContext::compute_height_for_absolutely_positioned_non_replaced_el
box_state.margin_bottom = margin_bottom.to_px_or_zero(box, width_of_containing_block); box_state.margin_bottom = margin_bottom.to_px_or_zero(box, width_of_containing_block);
} }
CSSPixelRect FormattingContext::content_box_rect_in_static_position_ancestor_coordinate_space(Box const& box, Box const& ancestor_box) const CSSPixelRect FormattingContext::content_box_rect_in_static_position_ancestor_coordinate_space(Box const& box) const
{ {
auto box_used_values = m_state.get(box); auto box_used_values = m_state.get(box);
CSSPixelRect rect = { { 0, 0 }, box_used_values.content_size() }; CSSPixelRect rect = { { 0, 0 }, box_used_values.content_size() };
for (auto const* current = &box; current; current = current->static_position_containing_block()) { VERIFY(box_used_values.offset.is_zero()); // Set as result of this calculation
if (current == &ancestor_box) for (auto const* current = box.static_position_containing_block(); current; current = current->containing_block()) {
if (current == box.containing_block())
return rect; return rect;
auto const& current_state = m_state.get(*current); auto const& current_state = m_state.get(*current);
rect.translate_by(current_state.offset); rect.translate_by(current_state.offset);
} }
// If we get here, ancestor_box was not an ancestor of `box`! // If we get here, `ancestor_box` was not in the containing block chain of the static position containing block of `box`!
// Something about the containing block chain is set up incorrectly then.
VERIFY_NOT_REACHED(); VERIFY_NOT_REACHED();
} }
@ -1245,7 +1247,7 @@ void FormattingContext::layout_absolutely_positioned_element(Box const& box, Ava
CSSPixelPoint used_offset; CSSPixelPoint used_offset;
auto static_position = m_state.get(box).static_position(); auto static_position = m_state.get(box).static_position();
auto offset_to_static_parent = content_box_rect_in_static_position_ancestor_coordinate_space(box, *box.containing_block()); auto offset_to_static_parent = content_box_rect_in_static_position_ancestor_coordinate_space(box);
static_position += offset_to_static_parent.location(); static_position += offset_to_static_parent.location();
if (box.computed_values().inset().top().is_auto() && box.computed_values().inset().bottom().is_auto()) { if (box.computed_values().inset().top().is_auto() && box.computed_values().inset().bottom().is_auto()) {

View file

@ -87,7 +87,7 @@ public:
[[nodiscard]] CSSPixelRect content_box_rect(LayoutState::UsedValues const&) const; [[nodiscard]] CSSPixelRect content_box_rect(LayoutState::UsedValues const&) const;
[[nodiscard]] CSSPixelRect content_box_rect_in_ancestor_coordinate_space(LayoutState::UsedValues const&, Box const& ancestor_box) const; [[nodiscard]] CSSPixelRect content_box_rect_in_ancestor_coordinate_space(LayoutState::UsedValues const&, Box const& ancestor_box) const;
[[nodiscard]] CSSPixels box_baseline(Box const&) const; [[nodiscard]] CSSPixels box_baseline(Box const&) const;
[[nodiscard]] CSSPixelRect content_box_rect_in_static_position_ancestor_coordinate_space(Box const&, Box const& ancestor_box) const; [[nodiscard]] CSSPixelRect content_box_rect_in_static_position_ancestor_coordinate_space(Box const&) const;
[[nodiscard]] CSSPixels containing_block_width_for(NodeWithStyleAndBoxModelMetrics const&) const; [[nodiscard]] CSSPixels containing_block_width_for(NodeWithStyleAndBoxModelMetrics const&) const;

View file

@ -167,7 +167,7 @@ void paint_background(DisplayListRecordingContext& context, PaintableBox const&
background_positioning_area.set_location(paintable_box.layout_node().root().navigable()->viewport_scroll_offset()); background_positioning_area.set_location(paintable_box.layout_node().root().navigable()->viewport_scroll_offset());
break; break;
case CSS::BackgroundAttachment::Local: case CSS::BackgroundAttachment::Local:
if (!paintable_box.is_viewport()) { if (!paintable_box.is_viewport_paintable()) {
auto scroll_offset = paintable_box.scroll_offset(); auto scroll_offset = paintable_box.scroll_offset();
background_positioning_area.translate_by(-scroll_offset.x(), -scroll_offset.y()); background_positioning_area.translate_by(-scroll_offset.x(), -scroll_offset.y());
} }

View file

@ -52,6 +52,17 @@ String Paintable::debug_description() const
return MUST(String::formatted("{}({})", class_name(), layout_node().debug_description())); return MUST(String::formatted("{}({})", class_name(), layout_node().debug_description()));
} }
void Paintable::resolve_paint_properties()
{
m_visible_for_hit_testing = true;
if (auto dom_node = this->dom_node(); dom_node && dom_node->is_inert()) {
// https://html.spec.whatwg.org/multipage/interaction.html#inert-subtrees
// When a node is inert:
// - Hit-testing must act as if the 'pointer-events' CSS property were set to 'none'.
m_visible_for_hit_testing = false;
}
}
bool Paintable::is_visible() const bool Paintable::is_visible() const
{ {
auto const& computed_values = this->computed_values(); auto const& computed_values = this->computed_values();
@ -90,13 +101,7 @@ CSS::ImmutableComputedValues const& Paintable::computed_values() const
bool Paintable::visible_for_hit_testing() const bool Paintable::visible_for_hit_testing() const
{ {
// https://html.spec.whatwg.org/multipage/interaction.html#inert-subtrees return m_visible_for_hit_testing && computed_values().pointer_events() != CSS::PointerEvents::None;
// When a node is inert:
// - Hit-testing must act as if the 'pointer-events' CSS property were set to 'none'.
if (auto dom_node = this->dom_node(); dom_node && dom_node->is_inert())
return false;
return computed_values().pointer_events() != CSS::PointerEvents::None;
} }
void Paintable::set_dom_node(GC::Ptr<DOM::Node> dom_node) void Paintable::set_dom_node(GC::Ptr<DOM::Node> dom_node)

View file

@ -145,7 +145,7 @@ public:
SelectionState selection_state() const { return m_selection_state; } SelectionState selection_state() const { return m_selection_state; }
void set_selection_state(SelectionState state) { m_selection_state = state; } void set_selection_state(SelectionState state) { m_selection_state = state; }
virtual void resolve_paint_properties() { } virtual void resolve_paint_properties();
[[nodiscard]] String debug_description() const; [[nodiscard]] String debug_description() const;
@ -177,6 +177,7 @@ private:
bool m_absolutely_positioned : 1 { false }; bool m_absolutely_positioned : 1 { false };
bool m_floating : 1 { false }; bool m_floating : 1 { false };
bool m_inline : 1 { false }; bool m_inline : 1 { false };
bool m_visible_for_hit_testing : 1 { true };
bool m_needs_paint_only_properties_update : 1 { true }; bool m_needs_paint_only_properties_update : 1 { true };
}; };

View file

@ -95,7 +95,7 @@ PaintableWithLines::~PaintableWithLines()
CSSPixelPoint PaintableBox::scroll_offset() const CSSPixelPoint PaintableBox::scroll_offset() const
{ {
if (is_viewport()) { if (is_viewport_paintable()) {
auto navigable = document().navigable(); auto navigable = document().navigable();
VERIFY(navigable); VERIFY(navigable);
return navigable->viewport_scroll_offset(); return navigable->viewport_scroll_offset();
@ -361,7 +361,7 @@ bool PaintableBox::could_be_scrolled_by_wheel_event(ScrollDirection direction) c
auto scrollable_overflow_size = direction == ScrollDirection::Horizontal ? scrollable_overflow_rect->width() : scrollable_overflow_rect->height(); auto scrollable_overflow_size = direction == ScrollDirection::Horizontal ? scrollable_overflow_rect->width() : scrollable_overflow_rect->height();
auto scrollport_size = direction == ScrollDirection::Horizontal ? absolute_padding_box_rect().width() : absolute_padding_box_rect().height(); auto scrollport_size = direction == ScrollDirection::Horizontal ? absolute_padding_box_rect().width() : absolute_padding_box_rect().height();
auto overflow_value_allows_scrolling = overflow == CSS::Overflow::Auto || overflow == CSS::Overflow::Scroll; auto overflow_value_allows_scrolling = overflow == CSS::Overflow::Auto || overflow == CSS::Overflow::Scroll;
if ((is_viewport() && overflow != CSS::Overflow::Hidden) || overflow_value_allows_scrolling) if ((is_viewport_paintable() && overflow != CSS::Overflow::Hidden) || overflow_value_allows_scrolling)
return scrollable_overflow_size > scrollport_size; return scrollable_overflow_size > scrollport_size;
return false; return false;
} }
@ -488,7 +488,7 @@ void PaintableBox::paint(DisplayListRecordingContext& context, PaintPhase phase)
} }
} }
if (phase == PaintPhase::Overlay && (g_paint_viewport_scrollbars || !is_viewport()) && computed_values().scrollbar_width() != CSS::ScrollbarWidth::None) { if (phase == PaintPhase::Overlay && (g_paint_viewport_scrollbars || !is_viewport_paintable()) && computed_values().scrollbar_width() != CSS::ScrollbarWidth::None) {
auto scrollbar_colors = computed_values().scrollbar_color(); auto scrollbar_colors = computed_values().scrollbar_color();
if (auto scrollbar_data = compute_scrollbar_data(ScrollDirection::Vertical); scrollbar_data.has_value()) { if (auto scrollbar_data = compute_scrollbar_data(ScrollDirection::Vertical); scrollbar_data.has_value()) {
auto gutter_rect = context.rounded_device_rect(scrollbar_data->gutter_rect).to_type<int>(); auto gutter_rect = context.rounded_device_rect(scrollbar_data->gutter_rect).to_type<int>();
@ -1153,10 +1153,10 @@ void PaintableBox::scroll_to_mouse_position(CSSPixelPoint position)
auto scroll_position_in_pixels = CSSPixels::nearest_value_for(scroll_position * (scrollable_overflow_size - padding_size)); auto scroll_position_in_pixels = CSSPixels::nearest_value_for(scroll_position * (scrollable_overflow_size - padding_size));
// Set the new scroll offset. // Set the new scroll offset.
auto new_scroll_offset = is_viewport() ? document().navigable()->viewport_scroll_offset() : scroll_offset(); auto new_scroll_offset = is_viewport_paintable() ? document().navigable()->viewport_scroll_offset() : scroll_offset();
new_scroll_offset.set_primary_offset_for_orientation(orientation, scroll_position_in_pixels); new_scroll_offset.set_primary_offset_for_orientation(orientation, scroll_position_in_pixels);
if (is_viewport()) if (is_viewport_paintable())
document().navigable()->perform_scroll_of_viewport(new_scroll_offset); document().navigable()->perform_scroll_of_viewport(new_scroll_offset);
else else
(void)set_scroll_offset(new_scroll_offset); (void)set_scroll_offset(new_scroll_offset);
@ -1213,7 +1213,7 @@ TraversalDecision PaintableBox::hit_test(CSSPixelPoint position, HitTestType typ
if (hit_test_scrollbars(position, callback) == TraversalDecision::Break) if (hit_test_scrollbars(position, callback) == TraversalDecision::Break)
return TraversalDecision::Break; return TraversalDecision::Break;
if (is_viewport()) { if (is_viewport_paintable()) {
auto& viewport_paintable = const_cast<ViewportPaintable&>(static_cast<ViewportPaintable const&>(*this)); auto& viewport_paintable = const_cast<ViewportPaintable&>(static_cast<ViewportPaintable const&>(*this));
viewport_paintable.build_stacking_context_tree_if_needed(); viewport_paintable.build_stacking_context_tree_if_needed();
viewport_paintable.document().update_paint_and_hit_testing_properties_if_needed(); viewport_paintable.document().update_paint_and_hit_testing_properties_if_needed();
@ -1279,7 +1279,7 @@ Optional<HitTestResult> PaintableBox::hit_test(CSSPixelPoint position, HitTestTy
TraversalDecision PaintableBox::hit_test_children(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const TraversalDecision PaintableBox::hit_test_children(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
{ {
for (auto const* child = last_child(); child; child = child->previous_sibling()) { for (auto const* child = last_child(); child; child = child->previous_sibling()) {
if (child->layout_node().is_positioned() && child->computed_values().z_index().value_or(0) == 0) if (child->is_positioned() && child->computed_values().z_index().value_or(0) == 0)
continue; continue;
if (child->has_stacking_context()) if (child->has_stacking_context())
continue; continue;
@ -1390,7 +1390,7 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy
} }
} }
if (!stacking_context() && is_visible() && (!layout_node().is_anonymous() || layout_node().is_positioned()) if (!stacking_context() && is_visible() && (!layout_node().is_anonymous() || is_positioned())
&& absolute_border_box_rect().contains(offset_position_adjusted_by_scroll_offset)) { && absolute_border_box_rect().contains(offset_position_adjusted_by_scroll_offset)) {
if (callback(HitTestResult { const_cast<PaintableWithLines&>(*this) }) == TraversalDecision::Break) if (callback(HitTestResult { const_cast<PaintableWithLines&>(*this) }) == TraversalDecision::Break)
return TraversalDecision::Break; return TraversalDecision::Break;

View file

@ -213,8 +213,6 @@ public:
Optional<CSSPixelRect> get_clip_rect() const; Optional<CSSPixelRect> get_clip_rect() const;
bool is_viewport() const { return layout_node_with_style_and_box_metrics().is_viewport(); }
virtual bool wants_mouse_events() const override; virtual bool wants_mouse_events() const override;
CSSPixelRect transform_box_rect() const; CSSPixelRect transform_box_rect() const;

View file

@ -24,11 +24,6 @@ SVGPathPaintable::SVGPathPaintable(Layout::SVGGraphicsBox const& layout_box)
{ {
} }
Layout::SVGGraphicsBox const& SVGPathPaintable::layout_box() const
{
return static_cast<Layout::SVGGraphicsBox const&>(layout_node());
}
TraversalDecision SVGPathPaintable::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const TraversalDecision SVGPathPaintable::hit_test(CSSPixelPoint position, HitTestType type, Function<TraversalDecision(HitTestResult)> const& callback) const
{ {
if (!computed_path().has_value()) if (!computed_path().has_value())
@ -55,8 +50,7 @@ void SVGPathPaintable::resolve_paint_properties()
{ {
Base::resolve_paint_properties(); Base::resolve_paint_properties();
auto& graphics_element = layout_box().dom_node(); auto& graphics_element = dom_node();
m_stroke_thickness = graphics_element.stroke_width().value_or(1); m_stroke_thickness = graphics_element.stroke_width().value_or(1);
m_stroke_dasharray = graphics_element.stroke_dasharray(); m_stroke_dasharray = graphics_element.stroke_dasharray();
m_stroke_dashoffset = graphics_element.stroke_dashoffset().value_or(0); m_stroke_dashoffset = graphics_element.stroke_dashoffset().value_or(0);
@ -72,7 +66,7 @@ void SVGPathPaintable::paint(DisplayListRecordingContext& context, PaintPhase ph
if (phase != PaintPhase::Foreground) if (phase != PaintPhase::Foreground)
return; return;
auto& graphics_element = layout_box().dom_node(); auto& graphics_element = dom_node();
auto const* svg_node = layout_box().first_ancestor_of_type<Layout::SVGSVGBox>(); auto const* svg_node = layout_box().first_ancestor_of_type<Layout::SVGSVGBox>();
auto svg_element_rect = svg_node->paintable_box()->absolute_rect(); auto svg_element_rect = svg_node->paintable_box()->absolute_rect();

View file

@ -24,7 +24,7 @@ public:
virtual void paint(DisplayListRecordingContext&, PaintPhase) const override; virtual void paint(DisplayListRecordingContext&, PaintPhase) const override;
Layout::SVGGraphicsBox const& layout_box() const; SVG::SVGGraphicsElement const& dom_node() const { return as<SVG::SVGGraphicsElement>(*Paintable::dom_node()); }
void set_computed_path(Gfx::Path path) void set_computed_path(Gfx::Path path)
{ {

View file

@ -86,7 +86,7 @@ static PaintPhase to_paint_phase(StackingContext::StackingContextPaintPhase phas
void StackingContext::paint_node_as_stacking_context(Paintable const& paintable, DisplayListRecordingContext& context) void StackingContext::paint_node_as_stacking_context(Paintable const& paintable, DisplayListRecordingContext& context)
{ {
if (paintable.layout_node().is_svg_svg_box()) { if (paintable.is_svg_svg_paintable()) {
paint_svg(context, static_cast<PaintableBox const&>(paintable), PaintPhase::Foreground); paint_svg(context, static_cast<PaintableBox const&>(paintable), PaintPhase::Foreground);
return; return;
} }
@ -121,7 +121,7 @@ void StackingContext::paint_descendants(DisplayListRecordingContext& context, Pa
if (child.has_stacking_context()) if (child.has_stacking_context())
return IterationDecision::Continue; return IterationDecision::Continue;
if (child.layout_node().is_svg_svg_box()) { if (child.is_svg_svg_paintable()) {
paint_svg(context, static_cast<PaintableBox const&>(child), to_paint_phase(phase)); paint_svg(context, static_cast<PaintableBox const&>(child), to_paint_phase(phase));
return IterationDecision::Continue; return IterationDecision::Continue;
} }
@ -198,15 +198,15 @@ void StackingContext::paint_descendants(DisplayListRecordingContext& context, Pa
void StackingContext::paint_child(DisplayListRecordingContext& context, StackingContext const& child) void StackingContext::paint_child(DisplayListRecordingContext& context, StackingContext const& child)
{ {
VERIFY(!child.paintable_box().layout_node().is_svg_box()); VERIFY(!child.paintable_box().is_svg_paintable());
const_cast<StackingContext&>(child).set_last_paint_generation_id(context.paint_generation_id()); const_cast<StackingContext&>(child).set_last_paint_generation_id(context.paint_generation_id());
child.paint(context); child.paint(context);
} }
void StackingContext::paint_internal(DisplayListRecordingContext& context) const void StackingContext::paint_internal(DisplayListRecordingContext& context) const
{ {
VERIFY(!paintable_box().layout_node().is_svg_box()); VERIFY(!paintable_box().is_svg_paintable());
if (paintable_box().layout_node().is_svg_svg_box()) { if (paintable_box().is_svg_svg_paintable()) {
auto const& svg_svg_paintable = static_cast<SVGSVGPaintable const&>(paintable_box()); auto const& svg_svg_paintable = static_cast<SVGSVGPaintable const&>(paintable_box());
paint_node(svg_svg_paintable, context, PaintPhase::Background); paint_node(svg_svg_paintable, context, PaintPhase::Background);
paint_node(svg_svg_paintable, context, PaintPhase::Border); paint_node(svg_svg_paintable, context, PaintPhase::Border);

View file

@ -139,7 +139,7 @@ void ViewportPaintable::assign_clip_frames()
paintable_box.set_own_clip_frame(clip_frame.value()); paintable_box.set_own_clip_frame(clip_frame.value());
} }
} }
for (auto block = paintable.containing_block(); !block->is_viewport(); block = block->containing_block()) { for (auto block = paintable.containing_block(); !block->is_viewport_paintable(); block = block->containing_block()) {
if (auto clip_frame = clip_state.get(block); clip_frame.has_value()) { if (auto clip_frame = clip_state.get(block); clip_frame.has_value()) {
if (paintable.is_paintable_box()) { if (paintable.is_paintable_box()) {
auto& paintable_box = static_cast<PaintableBox&>(paintable); auto& paintable_box = static_cast<PaintableBox&>(paintable);

View file

@ -10,6 +10,7 @@
#include <LibCore/StandardPaths.h> #include <LibCore/StandardPaths.h>
#include <LibCore/System.h> #include <LibCore/System.h>
#include <LibCore/TimeZoneWatcher.h> #include <LibCore/TimeZoneWatcher.h>
#include <LibDatabase/Database.h>
#include <LibDevTools/DevToolsServer.h> #include <LibDevTools/DevToolsServer.h>
#include <LibFileSystem/FileSystem.h> #include <LibFileSystem/FileSystem.h>
#include <LibImageDecoderClient/Client.h> #include <LibImageDecoderClient/Client.h>
@ -17,7 +18,6 @@
#include <LibWeb/Loader/UserAgent.h> #include <LibWeb/Loader/UserAgent.h>
#include <LibWebView/Application.h> #include <LibWebView/Application.h>
#include <LibWebView/CookieJar.h> #include <LibWebView/CookieJar.h>
#include <LibWebView/Database.h>
#include <LibWebView/HeadlessWebView.h> #include <LibWebView/HeadlessWebView.h>
#include <LibWebView/HelperProcess.h> #include <LibWebView/HelperProcess.h>
#include <LibWebView/Menu.h> #include <LibWebView/Menu.h>
@ -117,6 +117,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
bool disable_site_isolation = false; bool disable_site_isolation = false;
bool enable_idl_tracing = false; bool enable_idl_tracing = false;
bool disable_http_cache = false; bool disable_http_cache = false;
bool enable_http_disk_cache = false;
bool enable_autoplay = false; bool enable_autoplay = false;
bool expose_internals_object = false; bool expose_internals_object = false;
bool force_cpu_painting = false; bool force_cpu_painting = false;
@ -164,6 +165,7 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
args_parser.add_option(disable_site_isolation, "Disable site isolation", "disable-site-isolation"); args_parser.add_option(disable_site_isolation, "Disable site isolation", "disable-site-isolation");
args_parser.add_option(enable_idl_tracing, "Enable IDL tracing", "enable-idl-tracing"); args_parser.add_option(enable_idl_tracing, "Enable IDL tracing", "enable-idl-tracing");
args_parser.add_option(disable_http_cache, "Disable HTTP cache", "disable-http-cache"); args_parser.add_option(disable_http_cache, "Disable HTTP cache", "disable-http-cache");
args_parser.add_option(enable_http_disk_cache, "Enable HTTP disk cache", "enable-http-disk-cache");
args_parser.add_option(enable_autoplay, "Enable multimedia autoplay", "enable-autoplay"); args_parser.add_option(enable_autoplay, "Enable multimedia autoplay", "enable-autoplay");
args_parser.add_option(expose_internals_object, "Expose internals object", "expose-internals-object"); args_parser.add_option(expose_internals_object, "Expose internals object", "expose-internals-object");
args_parser.add_option(force_cpu_painting, "Force CPU painting", "force-cpu-painting"); args_parser.add_option(force_cpu_painting, "Force CPU painting", "force-cpu-painting");
@ -228,7 +230,6 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
.urls = sanitize_urls(raw_urls, m_settings.new_tab_page_url()), .urls = sanitize_urls(raw_urls, m_settings.new_tab_page_url()),
.raw_urls = move(raw_urls), .raw_urls = move(raw_urls),
.headless_mode = headless_mode, .headless_mode = headless_mode,
.certificates = move(certificates),
.new_window = new_window ? NewWindow::Yes : NewWindow::No, .new_window = new_window ? NewWindow::Yes : NewWindow::No,
.force_new_process = force_new_process ? ForceNewProcess::Yes : ForceNewProcess::No, .force_new_process = force_new_process ? ForceNewProcess::Yes : ForceNewProcess::No,
.allow_popups = allow_popups ? AllowPopups::Yes : AllowPopups::No, .allow_popups = allow_popups ? AllowPopups::Yes : AllowPopups::No,
@ -252,6 +253,11 @@ ErrorOr<void> Application::initialize(Main::Arguments const& arguments)
if (webdriver_content_ipc_path.has_value()) if (webdriver_content_ipc_path.has_value())
m_browser_options.webdriver_content_ipc_path = *webdriver_content_ipc_path; m_browser_options.webdriver_content_ipc_path = *webdriver_content_ipc_path;
m_request_server_options = {
.certificates = move(certificates),
.enable_http_disk_cache = enable_http_disk_cache ? EnableHTTPDiskCache::Yes : EnableHTTPDiskCache::No,
};
m_web_content_options = { m_web_content_options = {
.command_line = MUST(String::join(' ', m_arguments.strings)), .command_line = MUST(String::join(' ', m_arguments.strings)),
.executable_path = MUST(String::from_byte_string(MUST(Core::System::current_executable_path()))), .executable_path = MUST(String::from_byte_string(MUST(Core::System::current_executable_path()))),
@ -347,9 +353,12 @@ ErrorOr<void> Application::launch_services()
}; };
if (m_browser_options.disable_sql_database == DisableSQLDatabase::No) { if (m_browser_options.disable_sql_database == DisableSQLDatabase::No) {
m_database = Database::create().release_value_but_fixme_should_propagate_errors(); // FIXME: Move this to a generic "Ladybird data directory" helper.
m_cookie_jar = CookieJar::create(*m_database).release_value_but_fixme_should_propagate_errors(); auto database_path = ByteString::formatted("{}/Ladybird", Core::StandardPaths::user_data_directory());
m_storage_jar = StorageJar::create(*m_database).release_value_but_fixme_should_propagate_errors();
m_database = TRY(Database::Database::create(database_path, "Ladybird"sv));
m_cookie_jar = TRY(CookieJar::create(*m_database));
m_storage_jar = TRY(StorageJar::create(*m_database));
} else { } else {
m_cookie_jar = CookieJar::create(); m_cookie_jar = CookieJar::create();
m_storage_jar = StorageJar::create(); m_storage_jar = StorageJar::create();
@ -815,7 +824,10 @@ void Application::initialize_actions()
m_debug_menu->add_separator(); m_debug_menu->add_separator();
m_debug_menu->add_action(Action::create("Collect Garbage"sv, ActionID::CollectGarbage, debug_request("collect-garbage"sv))); m_debug_menu->add_action(Action::create("Collect Garbage"sv, ActionID::CollectGarbage, debug_request("collect-garbage"sv)));
m_debug_menu->add_action(Action::create("Clear Cache"sv, ActionID::ClearCache, debug_request("clear-cache"sv))); m_debug_menu->add_action(Action::create("Clear Cache"sv, ActionID::ClearCache, [this, clear_memory_cache = debug_request("clear_cache")]() {
m_request_server_client->async_clear_cache();
clear_memory_cache();
}));
m_debug_menu->add_action(Action::create("Clear All Cookies"sv, ActionID::ClearCookies, [this]() { m_cookie_jar->clear_all_cookies(); })); m_debug_menu->add_action(Action::create("Clear All Cookies"sv, ActionID::ClearCookies, [this]() { m_cookie_jar->clear_all_cookies(); }));
m_debug_menu->add_separator(); m_debug_menu->add_separator();

View file

@ -12,6 +12,7 @@
#include <AK/Swift.h> #include <AK/Swift.h>
#include <LibCore/EventLoop.h> #include <LibCore/EventLoop.h>
#include <LibCore/Forward.h> #include <LibCore/Forward.h>
#include <LibDatabase/Forward.h>
#include <LibDevTools/DevToolsDelegate.h> #include <LibDevTools/DevToolsDelegate.h>
#include <LibDevTools/Forward.h> #include <LibDevTools/Forward.h>
#include <LibImageDecoderClient/Client.h> #include <LibImageDecoderClient/Client.h>
@ -47,6 +48,7 @@ public:
static Settings& settings() { return the().m_settings; } static Settings& settings() { return the().m_settings; }
static BrowserOptions const& browser_options() { return the().m_browser_options; } static BrowserOptions const& browser_options() { return the().m_browser_options; }
static RequestServerOptions const& request_server_options() { return the().m_request_server_options; }
static WebContentOptions& web_content_options() { return the().m_web_content_options; } static WebContentOptions& web_content_options() { return the().m_web_content_options; }
static Requests::RequestClient& request_server_client() { return *the().m_request_server_client; } static Requests::RequestClient& request_server_client() { return *the().m_request_server_client; }
@ -173,6 +175,7 @@ private:
Main::Arguments m_arguments; Main::Arguments m_arguments;
BrowserOptions m_browser_options; BrowserOptions m_browser_options;
RequestServerOptions m_request_server_options;
WebContentOptions m_web_content_options; WebContentOptions m_web_content_options;
RefPtr<Requests::RequestClient> m_request_server_client; RefPtr<Requests::RequestClient> m_request_server_client;
@ -181,7 +184,7 @@ private:
RefPtr<WebContentClient> m_spare_web_content_process; RefPtr<WebContentClient> m_spare_web_content_process;
bool m_has_queued_task_to_launch_spare_web_content_process { false }; bool m_has_queued_task_to_launch_spare_web_content_process { false };
RefPtr<Database> m_database; RefPtr<Database::Database> m_database;
OwnPtr<CookieJar> m_cookie_jar; OwnPtr<CookieJar> m_cookie_jar;
OwnPtr<StorageJar> m_storage_jar; OwnPtr<StorageJar> m_storage_jar;

View file

@ -7,7 +7,6 @@ set(SOURCES
BrowserProcess.cpp BrowserProcess.cpp
ConsoleOutput.cpp ConsoleOutput.cpp
CookieJar.cpp CookieJar.cpp
Database.cpp
DOMNodeProperties.cpp DOMNodeProperties.cpp
HeadlessWebView.cpp HeadlessWebView.cpp
HelperProcess.cpp HelperProcess.cpp
@ -70,16 +69,13 @@ set(GENERATED_SOURCES
) )
ladybird_lib(LibWebView webview EXPLICIT_SYMBOL_EXPORT) ladybird_lib(LibWebView webview EXPLICIT_SYMBOL_EXPORT)
target_link_libraries(LibWebView PRIVATE LibCore LibDevTools LibFileSystem LibGfx LibImageDecoderClient LibIPC LibRequests LibJS LibWeb LibUnicode LibURL LibSyntax LibTextCodec) target_link_libraries(LibWebView PRIVATE LibCore LibDatabase LibDevTools LibFileSystem LibGfx LibImageDecoderClient LibIPC LibRequests LibJS LibWeb LibUnicode LibURL LibSyntax LibTextCodec)
if (APPLE) if (APPLE)
target_link_libraries(LibWebView PRIVATE LibThreading) target_link_libraries(LibWebView PRIVATE LibThreading)
endif() endif()
# Third-party # Third-party
find_package(SQLite3 REQUIRED)
target_link_libraries(LibWebView PRIVATE SQLite::SQLite3)
if (HAS_FONTCONFIG) if (HAS_FONTCONFIG)
target_link_libraries(LibWebView PRIVATE Fontconfig::Fontconfig) target_link_libraries(LibWebView PRIVATE Fontconfig::Fontconfig)
endif() endif()

View file

@ -11,6 +11,7 @@
#include <AK/StringBuilder.h> #include <AK/StringBuilder.h>
#include <AK/Time.h> #include <AK/Time.h>
#include <AK/Vector.h> #include <AK/Vector.h>
#include <LibDatabase/Database.h>
#include <LibURL/URL.h> #include <LibURL/URL.h>
#include <LibWeb/Cookie/ParsedCookie.h> #include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWebView/CookieJar.h> #include <LibWebView/CookieJar.h>
@ -20,7 +21,7 @@ namespace WebView {
static constexpr auto DATABASE_SYNCHRONIZATION_TIMER = AK::Duration::from_seconds(30); static constexpr auto DATABASE_SYNCHRONIZATION_TIMER = AK::Duration::from_seconds(30);
ErrorOr<NonnullOwnPtr<CookieJar>> CookieJar::create(Database& database) ErrorOr<NonnullOwnPtr<CookieJar>> CookieJar::create(Database::Database& database)
{ {
Statements statements {}; Statements statements {};
@ -665,7 +666,7 @@ void CookieJar::PersistedStorage::insert_cookie(Web::Cookie::Cookie const& cooki
cookie.persistent); cookie.persistent);
} }
static Web::Cookie::Cookie parse_cookie(Database& database, Database::StatementID statement_id) static Web::Cookie::Cookie parse_cookie(Database::Database& database, Database::StatementID statement_id)
{ {
int column = 0; int column = 0;
auto convert_text = [&](auto& field) { field = database.result_column<String>(statement_id, column++); }; auto convert_text = [&](auto& field) { field = database.result_column<String>(statement_id, column++); };

View file

@ -13,10 +13,10 @@
#include <AK/StringView.h> #include <AK/StringView.h>
#include <AK/Traits.h> #include <AK/Traits.h>
#include <LibCore/Timer.h> #include <LibCore/Timer.h>
#include <LibDatabase/Forward.h>
#include <LibURL/Forward.h> #include <LibURL/Forward.h>
#include <LibWeb/Cookie/Cookie.h> #include <LibWeb/Cookie/Cookie.h>
#include <LibWeb/Forward.h> #include <LibWeb/Forward.h>
#include <LibWebView/Database.h>
#include <LibWebView/Forward.h> #include <LibWebView/Forward.h>
namespace WebView { namespace WebView {
@ -76,13 +76,13 @@ class WEBVIEW_API CookieJar {
void insert_cookie(Web::Cookie::Cookie const& cookie); void insert_cookie(Web::Cookie::Cookie const& cookie);
TransientStorage::Cookies select_all_cookies(); TransientStorage::Cookies select_all_cookies();
Database& database; Database::Database& database;
Statements statements; Statements statements;
RefPtr<Core::Timer> synchronization_timer {}; RefPtr<Core::Timer> synchronization_timer {};
}; };
public: public:
static ErrorOr<NonnullOwnPtr<CookieJar>> create(Database&); static ErrorOr<NonnullOwnPtr<CookieJar>> create(Database::Database&);
static NonnullOwnPtr<CookieJar> create(); static NonnullOwnPtr<CookieJar> create();
~CookieJar(); ~CookieJar();

View file

@ -16,7 +16,6 @@ class Action;
class Application; class Application;
class Autocomplete; class Autocomplete;
class CookieJar; class CookieJar;
class Database;
class Menu; class Menu;
class OutOfProcessWebView; class OutOfProcessWebView;
class ProcessManager; class ProcessManager;

View file

@ -201,17 +201,23 @@ ErrorOr<NonnullRefPtr<Web::HTML::WebWorkerClient>> launch_web_worker_process(Web
ErrorOr<NonnullRefPtr<Requests::RequestClient>> launch_request_server_process() ErrorOr<NonnullRefPtr<Requests::RequestClient>> launch_request_server_process()
{ {
auto const& request_server_options = Application::request_server_options();
Vector<ByteString> arguments; Vector<ByteString> arguments;
for (auto const& certificate : WebView::Application::browser_options().certificates) for (auto const& certificate : request_server_options.certificates)
arguments.append(ByteString::formatted("--certificate={}", certificate)); arguments.append(ByteString::formatted("--certificate={}", certificate));
if (request_server_options.enable_http_disk_cache == EnableHTTPDiskCache::Yes)
arguments.append("--enable-http-disk-cache"sv);
if (auto server = mach_server_name(); server.has_value()) { if (auto server = mach_server_name(); server.has_value()) {
arguments.append("--mach-server-name"sv); arguments.append("--mach-server-name"sv);
arguments.append(server.value()); arguments.append(server.value());
} }
auto client = TRY(launch_server_process<Requests::RequestClient>("RequestServer"sv, move(arguments))); auto client = TRY(launch_server_process<Requests::RequestClient>("RequestServer"sv, move(arguments)));
WebView::Application::settings().dns_settings().visit( WebView::Application::settings().dns_settings().visit(
[](WebView::SystemDNS) {}, [](WebView::SystemDNS) {},
[&](WebView::DNSOverTLS const& dns_over_tls) { [&](WebView::DNSOverTLS const& dns_over_tls) {

View file

@ -74,7 +74,6 @@ struct BrowserOptions {
Optional<HeadlessMode> headless_mode; Optional<HeadlessMode> headless_mode;
int window_width { 800 }; int window_width { 800 };
int window_height { 600 }; int window_height { 600 };
Vector<ByteString> certificates {};
NewWindow new_window { NewWindow::No }; NewWindow new_window { NewWindow::No };
ForceNewProcess force_new_process { ForceNewProcess::No }; ForceNewProcess force_new_process { ForceNewProcess::No };
AllowPopups allow_popups { AllowPopups::No }; AllowPopups allow_popups { AllowPopups::No };
@ -87,6 +86,16 @@ struct BrowserOptions {
Optional<u16> devtools_port; Optional<u16> devtools_port;
}; };
enum class EnableHTTPDiskCache {
No,
Yes,
};
struct RequestServerOptions {
Vector<ByteString> certificates;
EnableHTTPDiskCache enable_http_disk_cache { EnableHTTPDiskCache::No };
};
enum class IsLayoutTestMode { enum class IsLayoutTestMode {
No, No,
Yes, Yes,

View file

@ -6,6 +6,7 @@
#include <AK/NonnullOwnPtr.h> #include <AK/NonnullOwnPtr.h>
#include <AK/StdLibExtras.h> #include <AK/StdLibExtras.h>
#include <LibDatabase/Database.h>
#include <LibWebView/StorageJar.h> #include <LibWebView/StorageJar.h>
namespace WebView { namespace WebView {
@ -13,7 +14,7 @@ namespace WebView {
// Quota size is specified in https://storage.spec.whatwg.org/#registered-storage-endpoints // Quota size is specified in https://storage.spec.whatwg.org/#registered-storage-endpoints
static constexpr size_t LOCAL_STORAGE_QUOTA = 5 * MiB; static constexpr size_t LOCAL_STORAGE_QUOTA = 5 * MiB;
ErrorOr<NonnullOwnPtr<StorageJar>> StorageJar::create(Database& database) ErrorOr<NonnullOwnPtr<StorageJar>> StorageJar::create(Database::Database& database)
{ {
Statements statements {}; Statements statements {};

View file

@ -9,8 +9,8 @@
#include <AK/HashMap.h> #include <AK/HashMap.h>
#include <AK/String.h> #include <AK/String.h>
#include <AK/Traits.h> #include <AK/Traits.h>
#include <LibDatabase/Forward.h>
#include <LibWeb/StorageAPI/StorageEndpoint.h> #include <LibWeb/StorageAPI/StorageEndpoint.h>
#include <LibWebView/Database.h>
#include <LibWebView/Forward.h> #include <LibWebView/Forward.h>
#include <LibWebView/StorageOperationError.h> #include <LibWebView/StorageOperationError.h>
@ -31,7 +31,7 @@ class WEBVIEW_API StorageJar {
AK_MAKE_NONMOVABLE(StorageJar); AK_MAKE_NONMOVABLE(StorageJar);
public: public:
static ErrorOr<NonnullOwnPtr<StorageJar>> create(Database&); static ErrorOr<NonnullOwnPtr<StorageJar>> create(Database::Database&);
static NonnullOwnPtr<StorageJar> create(); static NonnullOwnPtr<StorageJar> create();
~StorageJar(); ~StorageJar();
@ -71,7 +71,7 @@ private:
void clear(StorageEndpointType storage_endpoint, String const& storage_key); void clear(StorageEndpointType storage_endpoint, String const& storage_key);
Vector<String> get_keys(StorageEndpointType storage_endpoint, String const& storage_key); Vector<String> get_keys(StorageEndpointType storage_endpoint, String const& storage_key);
Database& database; Database::Database& database;
Statements statements; Statements statements;
}; };

View file

@ -157,6 +157,10 @@ def map_to_path(
if source.resource.startswith("/") or not is_resource: if source.resource.startswith("/") or not is_resource:
file_path = Path(base_directory, source.resource.lstrip("/")) file_path = Path(base_directory, source.resource.lstrip("/"))
else: else:
parsed_url = urlparse(source.resource)
if parsed_url.scheme != "":
print(f"Skipping '{source.resource}'. Downloading external resources is not supported.")
continue
# Add it as a sibling path if it's a relative resource # Add it as a sibling path if it's a relative resource
sibling_location = Path(resource_path).parent sibling_location = Path(resource_path).parent
parent_directory = Path(base_directory, sibling_location) parent_directory = Path(base_directory, sibling_location)

View file

@ -3,6 +3,10 @@ set(CMAKE_AUTORCC OFF)
set(CMAKE_AUTOUIC OFF) set(CMAKE_AUTOUIC OFF)
set(SOURCES set(SOURCES
Cache/CacheEntry.cpp
Cache/CacheIndex.cpp
Cache/DiskCache.cpp
Cache/Utilities.cpp
ConnectionFromClient.cpp ConnectionFromClient.cpp
WebSocketImplCurl.cpp WebSocketImplCurl.cpp
) )
@ -33,7 +37,7 @@ target_include_directories(requestserverservice PRIVATE ${CMAKE_CURRENT_BINARY_D
target_include_directories(requestserverservice PRIVATE ${LADYBIRD_SOURCE_DIR}/Services/) target_include_directories(requestserverservice PRIVATE ${LADYBIRD_SOURCE_DIR}/Services/)
target_link_libraries(RequestServer PRIVATE requestserverservice) target_link_libraries(RequestServer PRIVATE requestserverservice)
target_link_libraries(requestserverservice PUBLIC LibCore LibDNS LibMain LibCrypto LibFileSystem LibIPC LibMain LibTLS LibWebSocket LibURL LibTextCodec LibThreading CURL::libcurl) target_link_libraries(requestserverservice PUBLIC LibCore LibDatabase LibDNS LibCrypto LibFileSystem LibIPC LibMain LibTLS LibWebSocket LibURL LibTextCodec LibThreading CURL::libcurl)
target_link_libraries(requestserverservice PRIVATE OpenSSL::Crypto OpenSSL::SSL) target_link_libraries(requestserverservice PRIVATE OpenSSL::Crypto OpenSSL::SSL)
if (${CMAKE_SYSTEM_NAME} MATCHES "SunOS") if (${CMAKE_SYSTEM_NAME} MATCHES "SunOS")

View file

@ -0,0 +1,368 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <AK/JsonArraySerializer.h>
#include <AK/JsonObject.h>
#include <AK/JsonObjectSerializer.h>
#include <AK/JsonValue.h>
#include <AK/ScopeGuard.h>
#include <LibCore/Notifier.h>
#include <LibCore/System.h>
#include <LibFileSystem/FileSystem.h>
#include <RequestServer/Cache/CacheEntry.h>
#include <RequestServer/Cache/CacheIndex.h>
#include <RequestServer/Cache/DiskCache.h>
#include <RequestServer/Cache/Utilities.h>
namespace RequestServer {
static LexicalPath path_for_cache_key(LexicalPath const& cache_directory, u64 cache_key)
{
return cache_directory.append(MUST(String::formatted("{:016x}", cache_key)));
}
ErrorOr<CacheHeader> CacheHeader::read_from_stream(Stream& stream)
{
CacheHeader header;
header.magic = TRY(stream.read_value<u32>());
header.version = TRY(stream.read_value<u32>());
header.url_size = TRY(stream.read_value<u32>());
header.url_hash = TRY(stream.read_value<u32>());
header.status_code = TRY(stream.read_value<u32>());
header.reason_phrase_size = TRY(stream.read_value<u32>());
header.reason_phrase_hash = TRY(stream.read_value<u32>());
header.headers_size = TRY(stream.read_value<u32>());
header.headers_hash = TRY(stream.read_value<u32>());
return header;
}
ErrorOr<void> CacheHeader::write_to_stream(Stream& stream) const
{
TRY(stream.write_value(magic));
TRY(stream.write_value(version));
TRY(stream.write_value(url_size));
TRY(stream.write_value(url_hash));
TRY(stream.write_value(status_code));
TRY(stream.write_value(reason_phrase_size));
TRY(stream.write_value(reason_phrase_hash));
TRY(stream.write_value(headers_size));
TRY(stream.write_value(headers_hash));
return {};
}
ErrorOr<void> CacheFooter::write_to_stream(Stream& stream) const
{
TRY(stream.write_value(data_size));
TRY(stream.write_value(crc32));
return {};
}
ErrorOr<CacheFooter> CacheFooter::read_from_stream(Stream& stream)
{
CacheFooter footer;
footer.data_size = TRY(stream.read_value<u64>());
footer.crc32 = TRY(stream.read_value<u32>());
return footer;
}
CacheEntry::CacheEntry(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, CacheHeader cache_header)
: m_disk_cache(disk_cache)
, m_index(index)
, m_cache_key(cache_key)
, m_url(move(url))
, m_path(move(path))
, m_cache_header(cache_header)
{
}
void CacheEntry::remove()
{
(void)FileSystem::remove(m_path.string(), FileSystem::RecursionMode::Disallowed);
m_index.remove_entry(m_cache_key);
}
void CacheEntry::close_and_destory_cache_entry()
{
m_disk_cache.cache_entry_closed({}, *this);
}
ErrorOr<NonnullOwnPtr<CacheEntryWriter>> CacheEntryWriter::create(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, u32 status_code, Optional<String> reason_phrase, HTTP::HeaderMap const& headers, UnixDateTime request_time)
{
auto path = path_for_cache_key(disk_cache.cache_directory(), cache_key);
auto unbuffered_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write));
auto file = TRY(Core::OutputBufferedFile::create(move(unbuffered_file)));
CacheHeader cache_header;
auto result = [&]() -> ErrorOr<void> {
StringBuilder builder;
auto headers_serializer = TRY(JsonArraySerializer<>::try_create(builder));
for (auto const& header : headers.headers()) {
if (is_header_exempted_from_storage(header.name))
continue;
auto header_serializer = TRY(headers_serializer.add_object());
TRY(header_serializer.add("name"sv, header.name));
TRY(header_serializer.add("value"sv, header.value));
TRY(header_serializer.finish());
}
TRY(headers_serializer.finish());
cache_header.url_size = url.byte_count();
cache_header.url_hash = url.hash();
cache_header.status_code = status_code;
cache_header.reason_phrase_size = reason_phrase.has_value() ? reason_phrase->byte_count() : 0;
cache_header.reason_phrase_hash = reason_phrase.has_value() ? reason_phrase->hash() : 0;
auto serialized_headers = builder.string_view();
cache_header.headers_size = serialized_headers.length();
cache_header.headers_hash = serialized_headers.hash();
TRY(file->write_value(cache_header));
TRY(file->write_until_depleted(url));
if (reason_phrase.has_value())
TRY(file->write_until_depleted(*reason_phrase));
TRY(file->write_until_depleted(serialized_headers));
return {};
}();
if (result.is_error()) {
(void)FileSystem::remove(path.string(), FileSystem::RecursionMode::Disallowed);
return result.release_error();
}
return adopt_own(*new CacheEntryWriter { disk_cache, index, cache_key, move(url), path, move(file), cache_header, request_time });
}
CacheEntryWriter::CacheEntryWriter(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, NonnullOwnPtr<Core::OutputBufferedFile> file, CacheHeader cache_header, UnixDateTime request_time)
: CacheEntry(disk_cache, index, cache_key, move(url), move(path), cache_header)
, m_file(move(file))
, m_request_time(request_time)
, m_response_time(UnixDateTime::now())
{
}
ErrorOr<void> CacheEntryWriter::write_data(ReadonlyBytes data)
{
if (m_marked_for_deletion) {
close_and_destory_cache_entry();
return Error::from_string_literal("Cache entry has been deleted");
}
if (auto result = m_file->write_until_depleted(data); result.is_error()) {
dbgln("\033[31;1mUnable to write to cache entry for{}\033[0m {}: {}", m_url, result.error());
remove();
close_and_destory_cache_entry();
return result.release_error();
}
m_cache_footer.data_size += data.size();
// FIXME: Update the crc.
dbgln("\033[36;1mSaved {} bytes for\033[0m {}", data.size(), m_url);
return {};
}
ErrorOr<void> CacheEntryWriter::flush()
{
ScopeGuard guard { [&]() { close_and_destory_cache_entry(); } };
if (m_marked_for_deletion)
return Error::from_string_literal("Cache entry has been deleted");
if (auto result = m_file->write_value(m_cache_footer); result.is_error()) {
dbgln("\033[31;1mUnable to flush cache entry for{}\033[0m {}: {}", m_url, result.error());
remove();
return result.release_error();
}
m_index.create_entry(m_cache_key, m_url, m_cache_footer.data_size, m_request_time, m_response_time);
dbgln("\033[34;1mFinished caching\033[0m {} ({} bytes)", m_url, m_cache_footer.data_size);
return {};
}
ErrorOr<NonnullOwnPtr<CacheEntryReader>> CacheEntryReader::create(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, u64 data_size)
{
auto path = path_for_cache_key(disk_cache.cache_directory(), cache_key);
auto file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Read));
auto fd = file->fd();
CacheHeader cache_header;
String url;
Optional<String> reason_phrase;
HTTP::HeaderMap headers;
auto result = [&]() -> ErrorOr<void> {
cache_header = TRY(file->read_value<CacheHeader>());
if (cache_header.magic != CacheHeader::CACHE_MAGIC)
return Error::from_string_literal("Magic value mismatch");
if (cache_header.version != CacheHeader::CACHE_VERSION)
return Error::from_string_literal("Version mismatch");
url = TRY(String::from_stream(*file, cache_header.url_size));
if (url.hash() != cache_header.url_hash)
return Error::from_string_literal("URL hash mismatch");
if (cache_header.reason_phrase_size != 0) {
reason_phrase = TRY(String::from_stream(*file, cache_header.reason_phrase_size));
if (reason_phrase->hash() != cache_header.reason_phrase_hash)
return Error::from_string_literal("Reason phrase hash mismatch");
}
auto serialized_headers = TRY(String::from_stream(*file, cache_header.headers_size));
if (serialized_headers.hash() != cache_header.headers_hash)
return Error::from_string_literal("HTTP headers hash mismatch");
auto json_headers = TRY(JsonValue::from_string(serialized_headers));
if (!json_headers.is_array())
return Error::from_string_literal("Expected HTTP headers to be a JSON array");
TRY(json_headers.as_array().try_for_each([&](JsonValue const& header) -> ErrorOr<void> {
if (!header.is_object())
return Error::from_string_literal("Expected headers entry to be a JSON object");
auto name = header.as_object().get_string("name"sv);
auto value = header.as_object().get_string("value"sv);
if (!name.has_value() || !value.has_value())
return Error::from_string_literal("Missing/invalid data in headers entry");
headers.set(name->to_byte_string(), value->to_byte_string());
return {};
}));
return {};
}();
if (result.is_error()) {
(void)FileSystem::remove(path.string(), FileSystem::RecursionMode::Disallowed);
return result.release_error();
}
auto data_offset = sizeof(CacheHeader) + cache_header.url_size + cache_header.reason_phrase_size + cache_header.headers_size;
return adopt_own(*new CacheEntryReader { disk_cache, index, cache_key, move(url), move(path), move(file), fd, cache_header, move(reason_phrase), move(headers), data_offset, data_size });
}
CacheEntryReader::CacheEntryReader(DiskCache& disk_cache, CacheIndex& index, u64 cache_key, String url, LexicalPath path, NonnullOwnPtr<Core::File> file, int fd, CacheHeader cache_header, Optional<String> reason_phrase, HTTP::HeaderMap header_map, u64 data_offset, u64 data_size)
: CacheEntry(disk_cache, index, cache_key, move(url), move(path), cache_header)
, m_file(move(file))
, m_fd(fd)
, m_reason_phrase(move(reason_phrase))
, m_headers(move(header_map))
, m_data_offset(data_offset)
, m_data_size(data_size)
{
}
void CacheEntryReader::pipe_to(int pipe_fd, Function<void(u64)> on_complete, Function<void(u64)> on_error)
{
VERIFY(m_pipe_fd == -1);
m_pipe_fd = pipe_fd;
m_on_pipe_complete = move(on_complete);
m_on_pipe_error = move(on_error);
if (m_marked_for_deletion) {
pipe_error(Error::from_string_literal("Cache entry has been deleted"));
return;
}
m_pipe_write_notifier = Core::Notifier::construct(m_pipe_fd, Core::NotificationType::Write);
m_pipe_write_notifier->set_enabled(false);
m_pipe_write_notifier->on_activation = [this]() {
m_pipe_write_notifier->set_enabled(false);
pipe_without_blocking();
};
pipe_without_blocking();
}
void CacheEntryReader::pipe_without_blocking()
{
if (m_marked_for_deletion) {
pipe_error(Error::from_string_literal("Cache entry has been deleted"));
return;
}
auto result = Core::System::transfer_file_through_pipe(m_fd, m_pipe_fd, m_data_offset + m_bytes_piped, m_data_size - m_bytes_piped);
if (result.is_error()) {
if (result.error().code() != EAGAIN && result.error().code() != EWOULDBLOCK)
pipe_error(result.release_error());
else
m_pipe_write_notifier->set_enabled(true);
return;
}
m_bytes_piped += result.value();
if (m_bytes_piped == m_data_size) {
pipe_complete();
return;
}
pipe_without_blocking();
}
void CacheEntryReader::pipe_complete()
{
if (auto result = read_and_validate_footer(); result.is_error()) {
dbgln("\033[31;1mError validating cache entry for\033[0m {}: {}", m_url, result.error());
remove();
if (m_on_pipe_error)
m_on_pipe_error(m_bytes_piped);
} else {
m_index.update_last_access_time(m_cache_key);
if (m_on_pipe_complete)
m_on_pipe_complete(m_bytes_piped);
}
close_and_destory_cache_entry();
}
void CacheEntryReader::pipe_error(Error error)
{
dbgln("\033[31;1mError transferring cache to pipe for\033[0m {}: {}", m_url, error);
if (m_on_pipe_error)
m_on_pipe_error(m_bytes_piped);
close_and_destory_cache_entry();
}
ErrorOr<void> CacheEntryReader::read_and_validate_footer()
{
TRY(m_file->seek(m_data_offset + m_data_size, SeekMode::SetPosition));
m_cache_footer = TRY(m_file->read_value<CacheFooter>());
if (m_cache_footer.data_size != m_data_size)
return Error::from_string_literal("Invalid data size in footer");
// FIXME: Validate the crc.
return {};
}
}

View file

@ -0,0 +1,135 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/LexicalPath.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Types.h>
#include <LibCore/File.h>
#include <LibHTTP/HeaderMap.h>
#include <RequestServer/Forward.h>
namespace RequestServer {
struct [[gnu::packed]] CacheHeader {
static ErrorOr<CacheHeader> read_from_stream(Stream&);
ErrorOr<void> write_to_stream(Stream&) const;
static constexpr auto CACHE_MAGIC = 0xcafef00du;
static constexpr auto CACHE_VERSION = 1;
u32 magic { CACHE_MAGIC };
u32 version { CACHE_VERSION };
u32 url_size { 0 };
u32 url_hash { 0 };
u32 status_code { 0 };
u32 reason_phrase_size { 0 };
u32 reason_phrase_hash { 0 };
u32 headers_size { 0 };
u32 headers_hash { 0 };
};
struct [[gnu::packed]] CacheFooter {
static ErrorOr<CacheFooter> read_from_stream(Stream&);
ErrorOr<void> write_to_stream(Stream&) const;
u64 data_size { 0 };
u32 crc32 { 0 };
};
// A cache entry is an amalgamation of all information needed to reconstruct HTTP responses. It is created once we have
// received the response headers for a request. The body is streamed into the entry as it is received. The cache format
// on disk is:
//
// [CacheHeader][URL][ReasonPhrase][HttpHeaders][Data][CacheFooter]
class CacheEntry {
public:
virtual ~CacheEntry() = default;
void remove();
void mark_for_deletion(Badge<DiskCache>) { m_marked_for_deletion = true; }
protected:
CacheEntry(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, CacheHeader);
void close_and_destory_cache_entry();
DiskCache& m_disk_cache;
CacheIndex& m_index;
u64 m_cache_key { 0 };
String m_url;
LexicalPath m_path;
CacheHeader m_cache_header;
CacheFooter m_cache_footer;
bool m_marked_for_deletion { false };
};
class CacheEntryWriter : public CacheEntry {
public:
static ErrorOr<NonnullOwnPtr<CacheEntryWriter>> create(DiskCache&, CacheIndex&, u64 cache_key, String url, u32 status_code, Optional<String> reason_phrase, HTTP::HeaderMap const&, UnixDateTime request_time);
virtual ~CacheEntryWriter() override = default;
ErrorOr<void> write_data(ReadonlyBytes);
ErrorOr<void> flush();
private:
CacheEntryWriter(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, NonnullOwnPtr<Core::OutputBufferedFile>, CacheHeader, UnixDateTime request_time);
NonnullOwnPtr<Core::OutputBufferedFile> m_file;
UnixDateTime m_request_time;
UnixDateTime m_response_time;
};
class CacheEntryReader : public CacheEntry {
public:
static ErrorOr<NonnullOwnPtr<CacheEntryReader>> create(DiskCache&, CacheIndex&, u64 cache_key, u64 data_size);
virtual ~CacheEntryReader() override = default;
void pipe_to(int pipe_fd, Function<void(u64 bytes_piped)> on_complete, Function<void(u64 bytes_piped)> on_error);
u32 status_code() const { return m_cache_header.status_code; }
Optional<String> const& reason_phrase() const { return m_reason_phrase; }
HTTP::HeaderMap const& headers() const { return m_headers; }
private:
CacheEntryReader(DiskCache&, CacheIndex&, u64 cache_key, String url, LexicalPath, NonnullOwnPtr<Core::File>, int fd, CacheHeader, Optional<String> reason_phrase, HTTP::HeaderMap, u64 data_offset, u64 data_size);
void pipe_without_blocking();
void pipe_complete();
void pipe_error(Error);
ErrorOr<void> read_and_validate_footer();
NonnullOwnPtr<Core::File> m_file;
int m_fd { -1 };
RefPtr<Core::Notifier> m_pipe_write_notifier;
int m_pipe_fd { -1 };
Function<void(u64)> m_on_pipe_complete;
Function<void(u64)> m_on_pipe_error;
u64 m_bytes_piped { 0 };
Optional<String> m_reason_phrase;
HTTP::HeaderMap m_headers;
u64 const m_data_offset { 0 };
u64 const m_data_size { 0 };
};
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <RequestServer/Cache/CacheIndex.h>
namespace RequestServer {
ErrorOr<CacheIndex> CacheIndex::create(Database::Database& database)
{
auto create_table = TRY(database.prepare_statement(R"#(
CREATE TABLE IF NOT EXISTS CacheIndex (
cache_key INTEGER,
url TEXT,
data_size INTEGER,
request_time INTEGER,
response_time INTEGER,
last_access_time INTEGER,
PRIMARY KEY(cache_key)
);)#"sv));
database.execute_statement(create_table, {});
Statements statements {};
statements.insert_entry = TRY(database.prepare_statement("INSERT OR REPLACE INTO CacheIndex VALUES (?, ?, ?, ?, ?, ?);"sv));
statements.remove_entry = TRY(database.prepare_statement("DELETE FROM CacheIndex WHERE cache_key = ?;"sv));
statements.remove_all_entries = TRY(database.prepare_statement("DELETE FROM CacheIndex;"sv));
statements.select_entry = TRY(database.prepare_statement("SELECT * FROM CacheIndex WHERE cache_key = ?;"sv));
statements.update_last_access_time = TRY(database.prepare_statement("UPDATE CacheIndex SET last_access_time = ? WHERE cache_key = ?;"sv));
return CacheIndex { database, statements };
}
CacheIndex::CacheIndex(Database::Database& database, Statements statements)
: m_database(database)
, m_statements(statements)
{
}
void CacheIndex::create_entry(u64 cache_key, String url, u64 data_size, UnixDateTime request_time, UnixDateTime response_time)
{
auto now = UnixDateTime::now();
Entry entry {
.cache_key = cache_key,
.url = move(url),
.data_size = data_size,
.request_time = request_time,
.response_time = response_time,
.last_access_time = now,
};
m_database.execute_statement(m_statements.insert_entry, {}, entry.cache_key, entry.url, entry.data_size, entry.request_time, entry.response_time, entry.last_access_time);
m_entries.set(cache_key, move(entry));
}
void CacheIndex::remove_entry(u64 cache_key)
{
m_database.execute_statement(m_statements.remove_entry, {}, cache_key);
m_entries.remove(cache_key);
}
void CacheIndex::remove_all_entries()
{
m_database.execute_statement(m_statements.remove_all_entries, {});
m_entries.clear();
}
void CacheIndex::update_last_access_time(u64 cache_key)
{
auto entry = m_entries.get(cache_key);
if (!entry.has_value())
return;
auto now = UnixDateTime::now();
m_database.execute_statement(m_statements.update_last_access_time, {}, now, cache_key);
entry->last_access_time = now;
}
Optional<CacheIndex::Entry&> CacheIndex::find_entry(u64 cache_key)
{
if (auto entry = m_entries.get(cache_key); entry.has_value())
return entry;
m_database.execute_statement(
m_statements.select_entry, [&](auto statement_id) {
int column = 0;
auto cache_key = m_database.result_column<u64>(statement_id, column++);
auto url = m_database.result_column<String>(statement_id, column++);
auto data_size = m_database.result_column<u64>(statement_id, column++);
auto request_time = m_database.result_column<UnixDateTime>(statement_id, column++);
auto response_time = m_database.result_column<UnixDateTime>(statement_id, column++);
auto last_access_time = m_database.result_column<UnixDateTime>(statement_id, column++);
Entry entry { cache_key, move(url), data_size, request_time, response_time, last_access_time };
m_entries.set(cache_key, move(entry));
},
cache_key);
return m_entries.get(cache_key);
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/HashMap.h>
#include <AK/Time.h>
#include <AK/Types.h>
#include <LibDatabase/Database.h>
namespace RequestServer {
// The cache index is a SQL database containing metadata about each cache entry. An entry in the index is created once
// the entire cache entry has been successfully written to disk.
class CacheIndex {
struct Entry {
u64 cache_key { 0 };
String url;
u64 data_size { 0 };
UnixDateTime request_time;
UnixDateTime response_time;
UnixDateTime last_access_time;
};
public:
static ErrorOr<CacheIndex> create(Database::Database&);
void create_entry(u64 cache_key, String url, u64 data_size, UnixDateTime request_time, UnixDateTime response_time);
void remove_entry(u64 cache_key);
void remove_all_entries();
Optional<Entry&> find_entry(u64 cache_key);
void update_last_access_time(u64 cache_key);
private:
struct Statements {
Database::StatementID insert_entry { 0 };
Database::StatementID remove_entry { 0 };
Database::StatementID remove_all_entries { 0 };
Database::StatementID select_entry { 0 };
Database::StatementID update_last_access_time { 0 };
};
CacheIndex(Database::Database&, Statements);
Database::Database& m_database;
Statements m_statements;
HashMap<u32, Entry> m_entries;
};
}

View file

@ -0,0 +1,123 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCore/DirIterator.h>
#include <LibCore/StandardPaths.h>
#include <LibFileSystem/FileSystem.h>
#include <LibURL/URL.h>
#include <RequestServer/Cache/DiskCache.h>
#include <RequestServer/Cache/Utilities.h>
namespace RequestServer {
static constexpr auto INDEX_DATABASE = "INDEX"sv;
ErrorOr<DiskCache> DiskCache::create()
{
auto cache_directory = LexicalPath::join(Core::StandardPaths::cache_directory(), "Ladybird"sv, "Cache"sv);
auto database = TRY(Database::Database::create(cache_directory.string(), INDEX_DATABASE));
auto index = TRY(CacheIndex::create(database));
return DiskCache { move(database), move(cache_directory), move(index) };
}
DiskCache::DiskCache(NonnullRefPtr<Database::Database> database, LexicalPath cache_directory, CacheIndex index)
: m_database(move(database))
, m_cache_directory(move(cache_directory))
, m_index(move(index))
{
}
Optional<CacheEntryWriter&> DiskCache::create_entry(URL::URL const& url, StringView method, u32 status_code, Optional<String> reason_phrase, HTTP::HeaderMap const& headers, UnixDateTime request_time)
{
if (!is_cacheable(method, status_code, headers))
return {};
if (auto freshness = calculate_freshness_lifetime(headers); freshness.is_negative() || freshness.is_zero())
return {};
auto serialized_url = serialize_url_for_cache_storage(url);
auto cache_key = create_cache_key(serialized_url, method);
auto cache_entry = CacheEntryWriter::create(*this, m_index, cache_key, move(serialized_url), status_code, move(reason_phrase), headers, request_time);
if (cache_entry.is_error()) {
dbgln("\033[31;1mUnable to create cache entry for\033[0m {}: {}", url, cache_entry.error());
return {};
}
dbgln("\033[32;1mCreated disk cache entry for\033[0m {}", url);
auto address = reinterpret_cast<FlatPtr>(cache_entry.value().ptr());
m_open_cache_entries.set(address, cache_entry.release_value());
return static_cast<CacheEntryWriter&>(**m_open_cache_entries.get(address));
}
Optional<CacheEntryReader&> DiskCache::open_entry(URL::URL const& url, StringView method)
{
auto serialized_url = serialize_url_for_cache_storage(url);
auto cache_key = create_cache_key(serialized_url, method);
auto index_entry = m_index.find_entry(cache_key);
if (!index_entry.has_value()) {
dbgln("\033[35;1mNo disk cache entry for\033[0m {}", url);
return {};
}
auto cache_entry = CacheEntryReader::create(*this, m_index, cache_key, index_entry->data_size);
if (cache_entry.is_error()) {
dbgln("\033[31;1mUnable to open cache entry for\033[0m {}: {}", url, cache_entry.error());
m_index.remove_entry(cache_key);
return {};
}
auto freshness_lifetime = calculate_freshness_lifetime(cache_entry.value()->headers());
auto current_age = calculate_age(cache_entry.value()->headers(), index_entry->request_time, index_entry->response_time);
if (!is_response_fresh(freshness_lifetime, current_age)) {
dbgln("\033[33;1mCache entry expired for\033[0m {} (lifetime={}s age={}s)", url, freshness_lifetime.to_seconds(), current_age.to_seconds());
cache_entry.value()->remove();
return {};
}
dbgln("\033[32;1mOpened disk cache entry for\033[0m {} (lifetime={}s age={}s) ({} bytes)", url, freshness_lifetime.to_seconds(), current_age.to_seconds(), index_entry->data_size);
auto address = reinterpret_cast<FlatPtr>(cache_entry.value().ptr());
m_open_cache_entries.set(address, cache_entry.release_value());
return static_cast<CacheEntryReader&>(**m_open_cache_entries.get(address));
}
void DiskCache::clear_cache()
{
for (auto& [_, cache_entry] : m_open_cache_entries)
cache_entry->mark_for_deletion({});
m_index.remove_all_entries();
Core::DirIterator it { m_cache_directory.string(), Core::DirIterator::SkipDots };
size_t cache_entries { 0 };
while (it.has_next()) {
auto entry = it.next_full_path();
if (LexicalPath { entry }.title() == INDEX_DATABASE)
continue;
(void)FileSystem::remove(entry, FileSystem::RecursionMode::Disallowed);
++cache_entries;
}
dbgln("Cleared {} disk cache entries", cache_entries);
}
void DiskCache::cache_entry_closed(Badge<CacheEntry>, CacheEntry const& cache_entry)
{
auto address = reinterpret_cast<FlatPtr>(&cache_entry);
m_open_cache_entries.remove(address);
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/LexicalPath.h>
#include <AK/Optional.h>
#include <AK/StringView.h>
#include <AK/Time.h>
#include <AK/Types.h>
#include <LibDatabase/Database.h>
#include <LibHTTP/HeaderMap.h>
#include <LibURL/Forward.h>
#include <RequestServer/Cache/CacheEntry.h>
#include <RequestServer/Cache/CacheIndex.h>
namespace RequestServer {
class DiskCache {
public:
static ErrorOr<DiskCache> create();
Optional<CacheEntryWriter&> create_entry(URL::URL const&, StringView method, u32 status_code, Optional<String> reason_phrase, HTTP::HeaderMap const&, UnixDateTime request_time);
Optional<CacheEntryReader&> open_entry(URL::URL const&, StringView method);
void clear_cache();
LexicalPath const& cache_directory() { return m_cache_directory; }
void cache_entry_closed(Badge<CacheEntry>, CacheEntry const&);
private:
DiskCache(NonnullRefPtr<Database::Database>, LexicalPath cache_directory, CacheIndex);
NonnullRefPtr<Database::Database> m_database;
HashMap<FlatPtr, NonnullOwnPtr<CacheEntry>> m_open_cache_entries;
LexicalPath m_cache_directory;
CacheIndex m_index;
};
}

View file

@ -0,0 +1,220 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibCrypto/Hash/SHA1.h>
#include <LibURL/URL.h>
#include <RequestServer/Cache/Utilities.h>
namespace RequestServer {
static Optional<StringView> extract_cache_control_directive(StringView cache_control, StringView directive)
{
Optional<StringView> result;
cache_control.for_each_split_view(","sv, SplitBehavior::Nothing, [&](StringView candidate) {
if (!candidate.contains(directive, CaseSensitivity::CaseInsensitive))
return IterationDecision::Continue;
auto index = candidate.find('=');
if (!index.has_value())
return IterationDecision::Continue;
result = candidate.substring_view(*index + 1);
return IterationDecision::Break;
});
return result;
}
// https://httpwg.org/specs/rfc9110.html#field.date
static Optional<UnixDateTime> parse_http_date(Optional<ByteString const&> date)
{
// <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
if (date.has_value())
return UnixDateTime::parse("%a, %d %b %Y %T GMT"sv, *date, true);
return {};
}
String serialize_url_for_cache_storage(URL::URL const& url)
{
if (!url.fragment().has_value())
return url.serialize();
auto sanitized = url;
sanitized.set_fragment({});
return sanitized.serialize();
}
u64 create_cache_key(StringView url, StringView method)
{
auto hasher = Crypto::Hash::SHA1::create();
hasher->update(url);
hasher->update(method);
auto digest = hasher->digest();
auto bytes = digest.bytes();
u64 result = 0;
result |= static_cast<u64>(bytes[0]) << 56;
result |= static_cast<u64>(bytes[1]) << 48;
result |= static_cast<u64>(bytes[2]) << 40;
result |= static_cast<u64>(bytes[3]) << 32;
result |= static_cast<u64>(bytes[4]) << 24;
result |= static_cast<u64>(bytes[5]) << 16;
result |= static_cast<u64>(bytes[6]) << 8;
result |= static_cast<u64>(bytes[7]);
return result;
}
// https://httpwg.org/specs/rfc9111.html#response.cacheability
bool is_cacheable(StringView method, u32 status_code, HTTP::HeaderMap const& headers)
{
// A cache MUST NOT store a response to a request unless:
// * the request method is understood by the cache;
if (!method.is_one_of("GET"sv, "HEAD"sv))
return false;
// * the response status code is final (see Section 15 of [HTTP]);
if (status_code < 200)
return false;
auto cache_control = headers.get("Cache-Control"sv);
if (!cache_control.has_value())
return false;
// * if the response status code is 206 or 304, or the must-understand cache directive (see Section 5.2.2.3) is
// present: the cache understands the response status code;
// * the no-store cache directive is not present in the response (see Section 5.2.2.5);
if (cache_control->contains("no-store"sv, CaseSensitivity::CaseInsensitive))
return false;
// * if the cache is shared: the private response directive is either not present or allows a shared cache to store
// a modified response; see Section 5.2.2.7);
// * if the cache is shared: the Authorization header field is not present in the request (see Section 11.6.2 of
// [HTTP]) or a response directive is present that explicitly allows shared caching (see Section 3.5); and
// * the response contains at least one of the following:
// - a public response directive (see Section 5.2.2.9);
// - a private response directive, if the cache is not shared (see Section 5.2.2.7);
// - an Expires header field (see Section 5.3);
// - a max-age response directive (see Section 5.2.2.1);
// - if the cache is shared: an s-maxage response directive (see Section 5.2.2.10);
// - a cache extension that allows it to be cached (see Section 5.2.3); or
// - a status code that is defined as heuristically cacheable (see Section 4.2.2).
// FIXME: Implement cache revalidation.
if (cache_control->contains("no-cache"sv, CaseSensitivity::CaseInsensitive))
return false;
if (cache_control->contains("revalidate"sv, CaseSensitivity::CaseInsensitive))
return false;
return true;
}
// https://httpwg.org/specs/rfc9111.html#storing.fields
bool is_header_exempted_from_storage(StringView name)
{
// Caches MUST include all received response header fields — including unrecognized ones — when storing a response;
// this assures that new HTTP header fields can be successfully deployed. However, the following exceptions are made:
return name.is_one_of_ignoring_ascii_case(
// * The Connection header field and fields whose names are listed in it are required by Section 7.6.1 of [HTTP]
// to be removed before forwarding the message. This MAY be implemented by doing so before storage.
"Connection"sv,
"Keep-Alive"sv,
"Proxy-Connection"sv,
"TE"sv,
"Transfer-Encoding"sv,
"Upgrade"sv
// * Likewise, some fields' semantics require them to be removed before forwarding the message, and this MAY be
// implemented by doing so before storage; see Section 7.6.1 of [HTTP] for some examples.
// * The no-cache (Section 5.2.2.4) and private (Section 5.2.2.7) cache directives can have arguments that
// prevent storage of header fields by all caches and shared caches, respectively.
// * Header fields that are specific to the proxy that a cache uses when forwarding a request MUST NOT be stored,
// unless the cache incorporates the identity of the proxy into the cache key. Effectively, this is limited to
// Proxy-Authenticate (Section 11.7.1 of [HTTP]), Proxy-Authentication-Info (Section 11.7.3 of [HTTP]), and
// Proxy-Authorization (Section 11.7.2 of [HTTP]).
);
}
// https://httpwg.org/specs/rfc9111.html#calculating.freshness.lifetime
AK::Duration calculate_freshness_lifetime(HTTP::HeaderMap const& headers)
{
// A cache can calculate the freshness lifetime (denoted as freshness_lifetime) of a response by evaluating the
// following rules and using the first match:
// * If the cache is shared and the s-maxage response directive (Section 5.2.2.10) is present, use its value, or
// * If the max-age response directive (Section 5.2.2.1) is present, use its value, or
if (auto cache_control = headers.get("Cache-Control"sv); cache_control.has_value()) {
if (auto max_age = extract_cache_control_directive(*cache_control, "max-age"sv); max_age.has_value()) {
if (auto seconds = max_age->to_number<i64>(); seconds.has_value())
return AK::Duration::from_seconds(*seconds);
}
}
// * If the Expires response header field (Section 5.3) is present, use its value minus the value of the Date response
// header field (using the time the message was received if it is not present, as per Section 6.6.1 of [HTTP]), or
if (auto expires = parse_http_date(headers.get("Expires"sv)); expires.has_value()) {
auto date = parse_http_date(headers.get("Date"sv)).value_or_lazy_evaluated([]() {
return UnixDateTime::now();
});
return *expires - date;
}
// * Otherwise, no explicit expiration time is present in the response. A heuristic freshness lifetime might be
// applicable; see Section 4.2.2.
return {};
}
// https://httpwg.org/specs/rfc9111.html#age.calculations
AK::Duration calculate_age(HTTP::HeaderMap const& headers, UnixDateTime request_time, UnixDateTime response_time)
{
// The term "age_value" denotes the value of the Age header field (Section 5.1), in a form appropriate for arithmetic
// operation; or 0, if not available.
AK::Duration age_value;
if (auto age = headers.get("Age"sv); age.has_value()) {
if (auto seconds = age->to_number<i64>(); seconds.has_value())
age_value = AK::Duration::from_seconds(*seconds);
}
// The term "now" means the current value of this implementation's clock (Section 5.6.7 of [HTTP]).
auto now = UnixDateTime::now();
// The term "date_value" denotes the value of the Date header field, in a form appropriate for arithmetic operations.
// See Section 6.6.1 of [HTTP] for the definition of the Date header field and for requirements regarding responses
// without it.
auto date_value = parse_http_date(headers.get("Date"sv)).value_or(now);
auto apparent_age = max(0LL, (response_time - date_value).to_seconds());
auto response_delay = response_time - request_time;
auto corrected_age_value = age_value + response_delay;
auto corrected_initial_age = max(apparent_age, corrected_age_value.to_seconds());
auto resident_time = (now - response_time).to_seconds();
auto current_age = corrected_initial_age + resident_time;
return AK::Duration::from_seconds(current_age);
}
// https://httpwg.org/specs/rfc9111.html#expiration.model
bool is_response_fresh(AK::Duration freshness_lifetime, AK::Duration current_age)
{
return freshness_lifetime > current_age;
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/StringView.h>
#include <AK/Time.h>
#include <AK/Types.h>
#include <LibHTTP/HeaderMap.h>
#include <LibURL/Forward.h>
namespace RequestServer {
String serialize_url_for_cache_storage(URL::URL const&);
u64 create_cache_key(StringView url, StringView method);
bool is_cacheable(StringView method, u32 status_code, HTTP::HeaderMap const&);
bool is_header_exempted_from_storage(StringView name);
AK::Duration calculate_freshness_lifetime(HTTP::HeaderMap const&);
AK::Duration calculate_age(HTTP::HeaderMap const&, UnixDateTime request_time, UnixDateTime response_time);
bool is_response_fresh(AK::Duration freshness_lifetime, AK::Duration current_age);
}

View file

@ -20,12 +20,15 @@
#include <LibTextCodec/Decoder.h> #include <LibTextCodec/Decoder.h>
#include <LibWebSocket/ConnectionInfo.h> #include <LibWebSocket/ConnectionInfo.h>
#include <LibWebSocket/Message.h> #include <LibWebSocket/Message.h>
#include <RequestServer/Cache/DiskCache.h>
#include <RequestServer/ConnectionFromClient.h> #include <RequestServer/ConnectionFromClient.h>
#include <RequestServer/RequestClientEndpoint.h> #include <RequestServer/RequestClientEndpoint.h>
#ifdef AK_OS_WINDOWS #ifdef AK_OS_WINDOWS
// needed because curl.h includes winsock2.h // needed because curl.h includes winsock2.h
# include <AK/Windows.h> # include <AK/Windows.h>
#endif #endif
#include <curl/curl.h> #include <curl/curl.h>
namespace RequestServer { namespace RequestServer {
@ -42,6 +45,8 @@ static struct {
bool validate_dnssec_locally = false; bool validate_dnssec_locally = false;
} g_dns_info; } g_dns_info;
Optional<DiskCache> g_disk_cache;
static WeakPtr<Resolver> s_resolver {}; static WeakPtr<Resolver> s_resolver {};
static NonnullRefPtr<Resolver> default_resolver() static NonnullRefPtr<Resolver> default_resolver()
{ {
@ -116,13 +121,17 @@ struct ConnectionFromClient::ActiveRequest : public Weakable<ActiveRequest> {
bool got_all_headers { false }; bool got_all_headers { false };
bool is_connect_only { false }; bool is_connect_only { false };
size_t downloaded_so_far { 0 }; size_t downloaded_so_far { 0 };
String url; URL::URL url;
ByteString method;
Optional<String> reason_phrase; Optional<String> reason_phrase;
ByteBuffer body; ByteBuffer body;
AllocatingMemoryStream send_buffer; AllocatingMemoryStream send_buffer;
NonnullRefPtr<Core::Notifier> write_notifier; NonnullRefPtr<Core::Notifier> write_notifier;
bool done_fetching { false }; bool done_fetching { false };
Optional<CacheEntryWriter&> cache_entry;
UnixDateTime request_start_time;
ActiveRequest(ConnectionFromClient& client, CURLM* multi, CURL* easy, i32 request_id, int writer_fd) ActiveRequest(ConnectionFromClient& client, CURLM* multi, CURL* easy, i32 request_id, int writer_fd)
: multi(multi) : multi(multi)
, easy(easy) , easy(easy)
@ -130,6 +139,7 @@ struct ConnectionFromClient::ActiveRequest : public Weakable<ActiveRequest> {
, client(client) , client(client)
, writer_fd(writer_fd) , writer_fd(writer_fd)
, write_notifier(Core::Notifier::construct(writer_fd, Core::NotificationType::Write)) , write_notifier(Core::Notifier::construct(writer_fd, Core::NotificationType::Write))
, request_start_time(UnixDateTime::now())
{ {
write_notifier->set_enabled(false); write_notifier->set_enabled(false);
write_notifier->on_activation = [this] { write_notifier->on_activation = [this] {
@ -163,6 +173,13 @@ struct ConnectionFromClient::ActiveRequest : public Weakable<ActiveRequest> {
return {}; return {};
} }
if (cache_entry.has_value()) {
auto bytes_sent = bytes_to_send.span().slice(0, result.value());
if (cache_entry->write_data(bytes_sent).is_error())
cache_entry.clear();
}
MUST(send_buffer.discard(result.value())); MUST(send_buffer.discard(result.value()));
write_notifier->set_enabled(!send_buffer.is_eof()); write_notifier->set_enabled(!send_buffer.is_eof());
if (send_buffer.is_eof() && done_fetching) if (send_buffer.is_eof() && done_fetching)
@ -193,6 +210,9 @@ struct ConnectionFromClient::ActiveRequest : public Weakable<ActiveRequest> {
for (auto* string_list : curl_string_lists) for (auto* string_list : curl_string_lists)
curl_slist_free_all(string_list); curl_slist_free_all(string_list);
if (cache_entry.has_value())
(void)cache_entry->flush();
} }
void flush_headers_if_needed() void flush_headers_if_needed()
@ -204,6 +224,9 @@ struct ConnectionFromClient::ActiveRequest : public Weakable<ActiveRequest> {
auto result = curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &http_status_code); auto result = curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &http_status_code);
VERIFY(result == CURLE_OK); VERIFY(result == CURLE_OK);
client->async_headers_became_available(request_id, headers, http_status_code, reason_phrase); client->async_headers_became_available(request_id, headers, http_status_code, reason_phrase);
if (g_disk_cache.has_value())
cache_entry = g_disk_cache->create_entry(url, method, http_status_code, reason_phrase, headers, request_start_time);
} }
}; };
@ -464,6 +487,33 @@ void ConnectionFromClient::start_request(i32, ByteString, URL::URL, HTTP::Header
void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL::URL url, HTTP::HeaderMap request_headers, ByteBuffer request_body, Core::ProxyData proxy_data) void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL::URL url, HTTP::HeaderMap request_headers, ByteBuffer request_body, Core::ProxyData proxy_data)
{ {
dbgln_if(REQUESTSERVER_DEBUG, "RequestServer: start_request({}, {})", request_id, url); dbgln_if(REQUESTSERVER_DEBUG, "RequestServer: start_request({}, {})", request_id, url);
if (g_disk_cache.has_value()) {
if (auto cache_entry = g_disk_cache->open_entry(url, method); cache_entry.has_value()) {
auto fds = MUST(Core::System::pipe2(O_NONBLOCK));
auto writer_fd = fds[1];
auto reader_fd = fds[0];
async_request_started(request_id, IPC::File::adopt_fd(reader_fd));
async_headers_became_available(request_id, cache_entry->headers(), cache_entry->status_code(), cache_entry->reason_phrase());
cache_entry->pipe_to(
writer_fd,
[this, request_id, writer_fd](auto bytes_sent) {
// FIXME: Implement timing info for cache hits.
async_request_finished(request_id, bytes_sent, {}, {});
MUST(Core::System::close(writer_fd));
},
[this, request_id, writer_fd](auto bytes_sent) {
// FIXME: We should switch to a network request automatically if reading from cache has failed.
async_request_finished(request_id, bytes_sent, {}, Requests::NetworkError::CacheReadFailed);
(void)Core::System::close(writer_fd);
});
return;
}
}
auto host = url.serialized_host().to_byte_string(); auto host = url.serialized_host().to_byte_string();
m_resolver->dns.lookup(host, DNS::Messages::Class::IN, { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA }, { .validate_dnssec_locally = g_dns_info.validate_dnssec_locally }) m_resolver->dns.lookup(host, DNS::Messages::Class::IN, { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA }, { .validate_dnssec_locally = g_dns_info.validate_dnssec_locally })
@ -500,7 +550,8 @@ void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL:
async_request_started(request_id, IPC::File::adopt_fd(reader_fd)); async_request_started(request_id, IPC::File::adopt_fd(reader_fd));
auto request = make<ActiveRequest>(*this, m_curl_multi, easy, request_id, writer_fd); auto request = make<ActiveRequest>(*this, m_curl_multi, easy, request_id, writer_fd);
request->url = url.to_string(); request->url = url;
request->method = method;
auto set_option = [easy](auto option, auto value) { auto set_option = [easy](auto option, auto value) {
auto result = curl_easy_setopt(easy, option, value); auto result = curl_easy_setopt(easy, option, value);
@ -760,8 +811,6 @@ Messages::RequestServer::SetCertificateResponse ConnectionFromClient::set_certif
void ConnectionFromClient::ensure_connection(URL::URL url, ::RequestServer::CacheLevel cache_level) void ConnectionFromClient::ensure_connection(URL::URL url, ::RequestServer::CacheLevel cache_level)
{ {
auto const url_string_value = url.to_string();
if (cache_level == CacheLevel::CreateConnection) { if (cache_level == CacheLevel::CreateConnection) {
auto* easy = curl_easy_init(); auto* easy = curl_easy_init();
if (!easy) { if (!easy) {
@ -781,11 +830,11 @@ void ConnectionFromClient::ensure_connection(URL::URL url, ::RequestServer::Cach
auto connect_only_request_id = get_random<i32>(); auto connect_only_request_id = get_random<i32>();
auto request = make<ActiveRequest>(*this, m_curl_multi, easy, connect_only_request_id, 0); auto request = make<ActiveRequest>(*this, m_curl_multi, easy, connect_only_request_id, 0);
request->url = url_string_value; request->url = url;
request->is_connect_only = true; request->is_connect_only = true;
set_option(CURLOPT_PRIVATE, request.ptr()); set_option(CURLOPT_PRIVATE, request.ptr());
set_option(CURLOPT_URL, url_string_value.to_byte_string().characters()); set_option(CURLOPT_URL, url.to_byte_string().characters());
set_option(CURLOPT_PORT, url.port_or_default()); set_option(CURLOPT_PORT, url.port_or_default());
set_option(CURLOPT_CONNECTTIMEOUT, s_connect_timeout_seconds); set_option(CURLOPT_CONNECTTIMEOUT, s_connect_timeout_seconds);
set_option(CURLOPT_CONNECT_ONLY, 1L); set_option(CURLOPT_CONNECT_ONLY, 1L);
@ -812,6 +861,12 @@ void ConnectionFromClient::ensure_connection(URL::URL url, ::RequestServer::Cach
} }
} }
void ConnectionFromClient::clear_cache()
{
if (g_disk_cache.has_value())
g_disk_cache->clear_cache();
}
void ConnectionFromClient::websocket_connect(i64 websocket_id, URL::URL url, ByteString origin, Vector<ByteString> protocols, Vector<ByteString> extensions, HTTP::HeaderMap additional_request_headers) void ConnectionFromClient::websocket_connect(i64 websocket_id, URL::URL url, ByteString origin, Vector<ByteString> protocols, Vector<ByteString> extensions, HTTP::HeaderMap additional_request_headers)
{ {
auto host = url.serialized_host().to_byte_string(); auto host = url.serialized_host().to_byte_string();

View file

@ -49,6 +49,8 @@ private:
virtual Messages::RequestServer::SetCertificateResponse set_certificate(i32, ByteString, ByteString) override; virtual Messages::RequestServer::SetCertificateResponse set_certificate(i32, ByteString, ByteString) override;
virtual void ensure_connection(URL::URL url, ::RequestServer::CacheLevel cache_level) override; virtual void ensure_connection(URL::URL url, ::RequestServer::CacheLevel cache_level) override;
virtual void clear_cache() override;
virtual void websocket_connect(i64 websocket_id, URL::URL, ByteString, Vector<ByteString>, Vector<ByteString>, HTTP::HeaderMap) override; virtual void websocket_connect(i64 websocket_id, URL::URL, ByteString, Vector<ByteString>, Vector<ByteString>, HTTP::HeaderMap) override;
virtual void websocket_send(i64 websocket_id, bool, ByteBuffer) override; virtual void websocket_send(i64 websocket_id, bool, ByteBuffer) override;
virtual void websocket_close(i64 websocket_id, u16, ByteString) override; virtual void websocket_close(i64 websocket_id, u16, ByteString) override;

View file

@ -0,0 +1,17 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
namespace RequestServer {
class CacheEntry;
class CacheEntryReader;
class CacheEntryWriter;
class CacheIndex;
class DiskCache;
}

View file

@ -22,6 +22,8 @@ endpoint RequestServer
ensure_connection(URL::URL url, ::RequestServer::CacheLevel cache_level) =| ensure_connection(URL::URL url, ::RequestServer::CacheLevel cache_level) =|
clear_cache() =|
// Websocket Connection API // Websocket Connection API
websocket_connect(i64 websocket_id, URL::URL url, ByteString origin, Vector<ByteString> protocols, Vector<ByteString> extensions, HTTP::HeaderMap additional_request_headers) =| websocket_connect(i64 websocket_id, URL::URL url, ByteString origin, Vector<ByteString> protocols, Vector<ByteString> extensions, HTTP::HeaderMap additional_request_headers) =|
websocket_send(i64 websocket_id, bool is_text, ByteBuffer data) =| websocket_send(i64 websocket_id, bool is_text, ByteBuffer data) =|

View file

@ -14,6 +14,7 @@
#include <LibCore/Process.h> #include <LibCore/Process.h>
#include <LibIPC/SingleServer.h> #include <LibIPC/SingleServer.h>
#include <LibMain/Main.h> #include <LibMain/Main.h>
#include <RequestServer/Cache/DiskCache.h>
#include <RequestServer/ConnectionFromClient.h> #include <RequestServer/ConnectionFromClient.h>
#if defined(AK_OS_MACOS) #if defined(AK_OS_MACOS)
@ -23,6 +24,7 @@
namespace RequestServer { namespace RequestServer {
extern ByteString g_default_certificate_path; extern ByteString g_default_certificate_path;
extern Optional<DiskCache> g_disk_cache;
} }
@ -32,11 +34,13 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
Vector<ByteString> certificates; Vector<ByteString> certificates;
StringView mach_server_name; StringView mach_server_name;
bool enable_http_disk_cache = false;
bool wait_for_debugger = false; bool wait_for_debugger = false;
Core::ArgsParser args_parser; Core::ArgsParser args_parser;
args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate"); args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate");
args_parser.add_option(mach_server_name, "Mach server name", "mach-server-name", 0, "mach_server_name"); args_parser.add_option(mach_server_name, "Mach server name", "mach-server-name", 0, "mach_server_name");
args_parser.add_option(enable_http_disk_cache, "Enable HTTP disk cache", "enable-http-disk-cache");
args_parser.add_option(wait_for_debugger, "Wait for debugger", "wait-for-debugger"); args_parser.add_option(wait_for_debugger, "Wait for debugger", "wait-for-debugger");
args_parser.parse(arguments); args_parser.parse(arguments);
@ -54,6 +58,13 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
Core::Platform::register_with_mach_server(mach_server_name); Core::Platform::register_with_mach_server(mach_server_name);
#endif #endif
if (enable_http_disk_cache) {
if (auto cache = RequestServer::DiskCache::create(); cache.is_error())
warnln("Unable to create disk cache: {}", cache.error());
else
RequestServer::g_disk_cache = cache.release_value();
}
auto client = TRY(IPC::take_over_accepted_client_from_system_server<RequestServer::ConnectionFromClient>()); auto client = TRY(IPC::take_over_accepted_client_from_system_server<RequestServer::ConnectionFromClient>());
return event_loop.exec(); return event_loop.exec();

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<style>
div {
height: 100px;
width: 100px;
background-color: green;
}
</style>
<div></div>

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<link rel="match" href="../expected/position-absolute-fixed-nested-static-position.html" />
<style>
div {
height: 100px;
width: 100px;
}
.absolute { position: absolute; }
.fixed { position: fixed; }
.outside { background-color: red; }
.inside { background-color: orange; }
.deep { background-color: yellow; }
.deeper { background-color: green; }
</style>
<div class="outside">
<div class="fixed inside">
<div class="deep">
<div class="absolute deeper"></div>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show more