/src/serenity/Userland/Libraries/LibWeb/HTML/SharedResourceRequest.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2023, Andreas Kling <kling@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/HashTable.h> |
8 | | #include <LibGfx/Bitmap.h> |
9 | | #include <LibWeb/Fetch/Fetching/Fetching.h> |
10 | | #include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h> |
11 | | #include <LibWeb/Fetch/Infrastructure/FetchController.h> |
12 | | #include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h> |
13 | | #include <LibWeb/Fetch/Infrastructure/HTTP/Statuses.h> |
14 | | #include <LibWeb/HTML/AnimatedBitmapDecodedImageData.h> |
15 | | #include <LibWeb/HTML/DecodedImageData.h> |
16 | | #include <LibWeb/HTML/SharedResourceRequest.h> |
17 | | #include <LibWeb/Page/Page.h> |
18 | | #include <LibWeb/Platform/ImageCodecPlugin.h> |
19 | | #include <LibWeb/SVG/SVGDecodedImageData.h> |
20 | | |
21 | | namespace Web::HTML { |
22 | | |
23 | | JS_DEFINE_ALLOCATOR(SharedResourceRequest); |
24 | | |
25 | | JS::NonnullGCPtr<SharedResourceRequest> SharedResourceRequest::get_or_create(JS::Realm& realm, JS::NonnullGCPtr<Page> page, URL::URL const& url) |
26 | 0 | { |
27 | 0 | auto document = Bindings::host_defined_environment_settings_object(realm).responsible_document(); |
28 | 0 | VERIFY(document); |
29 | 0 | auto& shared_resource_requests = document->shared_resource_requests(); |
30 | 0 | if (auto it = shared_resource_requests.find(url); it != shared_resource_requests.end()) |
31 | 0 | return *it->value; |
32 | 0 | auto request = realm.heap().allocate<SharedResourceRequest>(realm, page, url, *document); |
33 | 0 | shared_resource_requests.set(url, request); |
34 | 0 | return request; |
35 | 0 | } |
36 | | |
37 | | SharedResourceRequest::SharedResourceRequest(JS::NonnullGCPtr<Page> page, URL::URL url, JS::NonnullGCPtr<DOM::Document> document) |
38 | 0 | : m_page(page) |
39 | 0 | , m_url(move(url)) |
40 | 0 | , m_document(document) |
41 | 0 | { |
42 | 0 | } |
43 | | |
44 | 0 | SharedResourceRequest::~SharedResourceRequest() = default; |
45 | | |
46 | | void SharedResourceRequest::finalize() |
47 | 0 | { |
48 | 0 | Base::finalize(); |
49 | 0 | auto& shared_resource_requests = m_document->shared_resource_requests(); |
50 | 0 | shared_resource_requests.remove(m_url); |
51 | 0 | } |
52 | | |
53 | | void SharedResourceRequest::visit_edges(JS::Cell::Visitor& visitor) |
54 | 0 | { |
55 | 0 | Base::visit_edges(visitor); |
56 | 0 | visitor.visit(m_fetch_controller); |
57 | 0 | visitor.visit(m_document); |
58 | 0 | visitor.visit(m_page); |
59 | 0 | for (auto& callback : m_callbacks) { |
60 | 0 | visitor.visit(callback.on_finish); |
61 | 0 | visitor.visit(callback.on_fail); |
62 | 0 | } |
63 | 0 | visitor.visit(m_image_data); |
64 | 0 | } |
65 | | |
66 | | JS::GCPtr<DecodedImageData> SharedResourceRequest::image_data() const |
67 | 0 | { |
68 | 0 | return m_image_data; |
69 | 0 | } |
70 | | |
71 | | JS::GCPtr<Fetch::Infrastructure::FetchController> SharedResourceRequest::fetch_controller() |
72 | 0 | { |
73 | 0 | return m_fetch_controller.ptr(); |
74 | 0 | } |
75 | | |
76 | | void SharedResourceRequest::set_fetch_controller(JS::GCPtr<Fetch::Infrastructure::FetchController> fetch_controller) |
77 | 0 | { |
78 | 0 | m_fetch_controller = move(fetch_controller); |
79 | 0 | } |
80 | | |
81 | | void SharedResourceRequest::fetch_resource(JS::Realm& realm, JS::NonnullGCPtr<Fetch::Infrastructure::Request> request) |
82 | 0 | { |
83 | 0 | Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {}; |
84 | 0 | fetch_algorithms_input.process_response = [this, &realm, request](JS::NonnullGCPtr<Fetch::Infrastructure::Response> response) { |
85 | | // FIXME: If the response is CORS cross-origin, we must use its internal response to query any of its data. See: |
86 | | // https://github.com/whatwg/html/issues/9355 |
87 | 0 | response = response->unsafe_response(); |
88 | |
|
89 | 0 | auto process_body = JS::create_heap_function(heap(), [this, request, response](ByteBuffer data) { |
90 | 0 | auto extracted_mime_type = response->header_list()->extract_mime_type(); |
91 | 0 | auto mime_type = extracted_mime_type.has_value() ? extracted_mime_type.value().essence().bytes_as_string_view() : StringView {}; |
92 | 0 | handle_successful_fetch(request->url(), mime_type, move(data)); |
93 | 0 | }); |
94 | 0 | auto process_body_error = JS::create_heap_function(heap(), [this](JS::Value) { |
95 | 0 | handle_failed_fetch(); |
96 | 0 | }); |
97 | | |
98 | | // Check for failed fetch response |
99 | 0 | if (!Fetch::Infrastructure::is_ok_status(response->status()) || !response->body()) { |
100 | 0 | handle_failed_fetch(); |
101 | 0 | return; |
102 | 0 | } |
103 | | |
104 | 0 | response->body()->fully_read(realm, process_body, process_body_error, JS::NonnullGCPtr { realm.global_object() }); |
105 | 0 | }; |
106 | |
|
107 | 0 | m_state = State::Fetching; |
108 | |
|
109 | 0 | auto fetch_controller = Fetch::Fetching::fetch( |
110 | 0 | realm, |
111 | 0 | request, |
112 | 0 | Fetch::Infrastructure::FetchAlgorithms::create(realm.vm(), move(fetch_algorithms_input))) |
113 | 0 | .release_value_but_fixme_should_propagate_errors(); |
114 | |
|
115 | 0 | set_fetch_controller(fetch_controller); |
116 | 0 | } |
117 | | |
118 | | void SharedResourceRequest::add_callbacks(Function<void()> on_finish, Function<void()> on_fail) |
119 | 0 | { |
120 | 0 | if (m_state == State::Finished) { |
121 | 0 | if (on_finish) |
122 | 0 | on_finish(); |
123 | 0 | return; |
124 | 0 | } |
125 | | |
126 | 0 | if (m_state == State::Failed) { |
127 | 0 | if (on_fail) |
128 | 0 | on_fail(); |
129 | 0 | return; |
130 | 0 | } |
131 | | |
132 | 0 | Callbacks callbacks; |
133 | 0 | if (on_finish) |
134 | 0 | callbacks.on_finish = JS::create_heap_function(vm().heap(), move(on_finish)); |
135 | 0 | if (on_fail) |
136 | 0 | callbacks.on_fail = JS::create_heap_function(vm().heap(), move(on_fail)); |
137 | |
|
138 | 0 | m_callbacks.append(move(callbacks)); |
139 | 0 | } |
140 | | |
141 | | void SharedResourceRequest::handle_successful_fetch(URL::URL const& url_string, StringView mime_type, ByteBuffer data) |
142 | 0 | { |
143 | | // AD-HOC: At this point, things gets very ad-hoc. |
144 | | // FIXME: Bring this closer to spec. |
145 | |
|
146 | 0 | bool const is_svg_image = mime_type == "image/svg+xml"sv || url_string.basename().ends_with(".svg"sv); |
147 | |
|
148 | 0 | if (is_svg_image) { |
149 | 0 | auto result = SVG::SVGDecodedImageData::create(m_document->realm(), m_page, url_string, data); |
150 | 0 | if (result.is_error()) { |
151 | 0 | handle_failed_fetch(); |
152 | 0 | } else { |
153 | 0 | m_image_data = result.release_value(); |
154 | 0 | handle_successful_resource_load(); |
155 | 0 | } |
156 | 0 | return; |
157 | 0 | } |
158 | | |
159 | 0 | auto handle_successful_bitmap_decode = [strong_this = JS::Handle(*this)](Web::Platform::DecodedImage& result) -> ErrorOr<void> { |
160 | 0 | Vector<AnimatedBitmapDecodedImageData::Frame> frames; |
161 | 0 | for (auto& frame : result.frames) { |
162 | 0 | frames.append(AnimatedBitmapDecodedImageData::Frame { |
163 | 0 | .bitmap = Gfx::ImmutableBitmap::create(*frame.bitmap), |
164 | 0 | .duration = static_cast<int>(frame.duration), |
165 | 0 | }); |
166 | 0 | } |
167 | 0 | strong_this->m_image_data = AnimatedBitmapDecodedImageData::create(strong_this->m_document->realm(), move(frames), result.loop_count, result.is_animated).release_value_but_fixme_should_propagate_errors(); |
168 | 0 | strong_this->handle_successful_resource_load(); |
169 | 0 | return {}; |
170 | 0 | }; |
171 | |
|
172 | 0 | auto handle_failed_decode = [strong_this = JS::Handle(*this)](Error&) -> void { |
173 | 0 | strong_this->handle_failed_fetch(); |
174 | 0 | }; |
175 | |
|
176 | 0 | (void)Web::Platform::ImageCodecPlugin::the().decode_image(data.bytes(), move(handle_successful_bitmap_decode), move(handle_failed_decode)); |
177 | 0 | } |
178 | | |
179 | | void SharedResourceRequest::handle_failed_fetch() |
180 | 0 | { |
181 | 0 | m_state = State::Failed; |
182 | 0 | for (auto& callback : m_callbacks) { |
183 | 0 | if (callback.on_fail) |
184 | 0 | callback.on_fail->function()(); |
185 | 0 | } |
186 | 0 | m_callbacks.clear(); |
187 | 0 | } |
188 | | |
189 | | void SharedResourceRequest::handle_successful_resource_load() |
190 | 0 | { |
191 | 0 | m_state = State::Finished; |
192 | 0 | for (auto& callback : m_callbacks) { |
193 | 0 | if (callback.on_finish) |
194 | 0 | callback.on_finish->function()(); |
195 | 0 | } |
196 | 0 | m_callbacks.clear(); |
197 | 0 | } |
198 | | |
199 | | bool SharedResourceRequest::needs_fetching() const |
200 | 0 | { |
201 | 0 | return m_state == State::New; |
202 | 0 | } |
203 | | |
204 | | bool SharedResourceRequest::is_fetching() const |
205 | 0 | { |
206 | 0 | return m_state == State::Fetching; |
207 | 0 | } |
208 | | |
209 | | } |