summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibWeb/Loader
diff options
context:
space:
mode:
authorAndreas Kling <kling@serenityos.org>2021-01-12 12:17:30 +0100
committerAndreas Kling <kling@serenityos.org>2021-01-12 12:17:46 +0100
commit13d7c09125f8eec703d0a43a9a87fc8aa08f7319 (patch)
tree70fd643c429cea5c1f9362c2674511d17a53f3b5 /Userland/Libraries/LibWeb/Loader
parentdc28c07fa526841e05e16161c74a6c23984f1dd5 (diff)
downloadserenity-13d7c09125f8eec703d0a43a9a87fc8aa08f7319.zip
Libraries: Move to Userland/Libraries/
Diffstat (limited to 'Userland/Libraries/LibWeb/Loader')
-rw-r--r--Userland/Libraries/LibWeb/Loader/ContentFilter.cpp68
-rw-r--r--Userland/Libraries/LibWeb/Loader/ContentFilter.h51
-rw-r--r--Userland/Libraries/LibWeb/Loader/FrameLoader.cpp296
-rw-r--r--Userland/Libraries/LibWeb/Loader/FrameLoader.h66
-rw-r--r--Userland/Libraries/LibWeb/Loader/ImageLoader.cpp164
-rw-r--r--Userland/Libraries/LibWeb/Loader/ImageLoader.h79
-rw-r--r--Userland/Libraries/LibWeb/Loader/ImageResource.cpp103
-rw-r--r--Userland/Libraries/LibWeb/Loader/ImageResource.h66
-rw-r--r--Userland/Libraries/LibWeb/Loader/LoadRequest.h94
-rw-r--r--Userland/Libraries/LibWeb/Loader/Resource.cpp168
-rw-r--r--Userland/Libraries/LibWeb/Loader/Resource.h117
-rw-r--r--Userland/Libraries/LibWeb/Loader/ResourceLoader.cpp235
-rw-r--r--Userland/Libraries/LibWeb/Loader/ResourceLoader.h69
13 files changed, 1576 insertions, 0 deletions
diff --git a/Userland/Libraries/LibWeb/Loader/ContentFilter.cpp b/Userland/Libraries/LibWeb/Loader/ContentFilter.cpp
new file mode 100644
index 0000000000..b4633b5d01
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/ContentFilter.cpp
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/StringBuilder.h>
+#include <LibWeb/Loader/ContentFilter.h>
+
+namespace Web {
+
+ContentFilter& ContentFilter::the()
+{
+ static ContentFilter* filter = new ContentFilter;
+ return *filter;
+}
+
+ContentFilter::ContentFilter()
+{
+}
+
+ContentFilter::~ContentFilter()
+{
+}
+
+bool ContentFilter::is_filtered(const URL& url) const
+{
+ auto url_string = url.to_string();
+
+ for (auto& pattern : m_patterns) {
+ if (url_string.matches(pattern.text, CaseSensitivity::CaseSensitive))
+ return true;
+ }
+ return false;
+}
+
+void ContentFilter::add_pattern(const String& pattern)
+{
+ StringBuilder builder;
+ if (!pattern.starts_with('*'))
+ builder.append('*');
+ builder.append(pattern);
+ if (!pattern.ends_with('*'))
+ builder.append('*');
+ m_patterns.empend(builder.to_string());
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/ContentFilter.h b/Userland/Libraries/LibWeb/Loader/ContentFilter.h
new file mode 100644
index 0000000000..964c3b3fa2
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/ContentFilter.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/URL.h>
+#include <AK/Vector.h>
+
+namespace Web {
+
+class ContentFilter {
+public:
+ static ContentFilter& the();
+
+ bool is_filtered(const URL&) const;
+ void add_pattern(const String&);
+
+private:
+ ContentFilter();
+ ~ContentFilter();
+
+ struct Pattern {
+ String text;
+ };
+ Vector<Pattern> m_patterns;
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/FrameLoader.cpp b/Userland/Libraries/LibWeb/Loader/FrameLoader.cpp
new file mode 100644
index 0000000000..172a9e1b2a
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/FrameLoader.cpp
@@ -0,0 +1,296 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/LexicalPath.h>
+#include <LibGemini/Document.h>
+#include <LibGfx/ImageDecoder.h>
+#include <LibMarkdown/Document.h>
+#include <LibWeb/DOM/Document.h>
+#include <LibWeb/DOM/ElementFactory.h>
+#include <LibWeb/DOM/Text.h>
+#include <LibWeb/HTML/HTMLIFrameElement.h>
+#include <LibWeb/HTML/Parser/HTMLDocumentParser.h>
+#include <LibWeb/Loader/FrameLoader.h>
+#include <LibWeb/Loader/ResourceLoader.h>
+#include <LibWeb/Namespace.h>
+#include <LibWeb/Page/Frame.h>
+#include <LibWeb/Page/Page.h>
+
+//#define GEMINI_DEBUG 1
+
+namespace Web {
+
+FrameLoader::FrameLoader(Frame& frame)
+ : m_frame(frame)
+{
+}
+
+FrameLoader::~FrameLoader()
+{
+}
+
+static bool build_markdown_document(DOM::Document& document, const ByteBuffer& data)
+{
+ auto markdown_document = Markdown::Document::parse(data);
+ if (!markdown_document)
+ return false;
+
+ HTML::HTMLDocumentParser parser(document, markdown_document->render_to_html(), "utf-8");
+ parser.run(document.url());
+ return true;
+}
+
+static bool build_text_document(DOM::Document& document, const ByteBuffer& data)
+{
+ auto html_element = document.create_element("html");
+ document.append_child(html_element);
+
+ auto head_element = document.create_element("head");
+ html_element->append_child(head_element);
+ auto title_element = document.create_element("title");
+ head_element->append_child(title_element);
+
+ auto title_text = document.create_text_node(document.url().basename());
+ title_element->append_child(title_text);
+
+ auto body_element = document.create_element("body");
+ html_element->append_child(body_element);
+
+ auto pre_element = document.create_element("pre");
+ body_element->append_child(pre_element);
+
+ pre_element->append_child(document.create_text_node(String::copy(data)));
+ return true;
+}
+
+static bool build_image_document(DOM::Document& document, const ByteBuffer& data)
+{
+ auto image_decoder = Gfx::ImageDecoder::create(data.data(), data.size());
+ auto bitmap = image_decoder->bitmap();
+ if (!bitmap)
+ return false;
+
+ auto html_element = document.create_element("html");
+ document.append_child(html_element);
+
+ auto head_element = document.create_element("head");
+ html_element->append_child(head_element);
+ auto title_element = document.create_element("title");
+ head_element->append_child(title_element);
+
+ auto basename = LexicalPath(document.url().path()).basename();
+ auto title_text = adopt(*new DOM::Text(document, String::formatted("{} [{}x{}]", basename, bitmap->width(), bitmap->height())));
+ title_element->append_child(title_text);
+
+ auto body_element = document.create_element("body");
+ html_element->append_child(body_element);
+
+ auto image_element = document.create_element("img");
+ image_element->set_attribute(HTML::AttributeNames::src, document.url().to_string());
+ body_element->append_child(image_element);
+
+ return true;
+}
+
+static bool build_gemini_document(DOM::Document& document, const ByteBuffer& data)
+{
+ StringView gemini_data { data };
+ auto gemini_document = Gemini::Document::parse(gemini_data, document.url());
+ String html_data = gemini_document->render_to_html();
+
+#ifdef GEMINI_DEBUG
+ dbgln("Gemini data:\n\"\"\"{}\"\"\"", gemini_data);
+ dbgln("Converted to HTML:\n\"\"\"{}\"\"\"", html_data);
+#endif
+
+ HTML::HTMLDocumentParser parser(document, html_data, "utf-8");
+ parser.run(document.url());
+ return true;
+}
+
+bool FrameLoader::parse_document(DOM::Document& document, const ByteBuffer& data)
+{
+ auto& mime_type = document.content_type();
+ if (mime_type == "text/html" || mime_type == "image/svg+xml") {
+ HTML::HTMLDocumentParser parser(document, data, document.encoding());
+ parser.run(document.url());
+ return true;
+ }
+ if (mime_type.starts_with("image/"))
+ return build_image_document(document, data);
+ if (mime_type == "text/plain")
+ return build_text_document(document, data);
+ if (mime_type == "text/markdown")
+ return build_markdown_document(document, data);
+ if (mime_type == "text/gemini")
+ return build_gemini_document(document, data);
+
+ return false;
+}
+
+bool FrameLoader::load(const LoadRequest& request, Type type)
+{
+ if (!request.is_valid()) {
+ load_error_page(request.url(), "Invalid request");
+ return false;
+ }
+
+ auto& url = request.url();
+
+ set_resource(ResourceLoader::the().load_resource(Resource::Type::Generic, request));
+
+ if (type == Type::Navigation) {
+ if (auto* page = frame().page())
+ page->client().page_did_start_loading(url);
+ }
+
+ if (type == Type::IFrame)
+ return true;
+
+ if (url.protocol() == "http" || url.protocol() == "https") {
+ URL favicon_url;
+ favicon_url.set_protocol(url.protocol());
+ favicon_url.set_host(url.host());
+ favicon_url.set_port(url.port());
+ favicon_url.set_path("/favicon.ico");
+
+ ResourceLoader::the().load(
+ favicon_url,
+ [this, favicon_url](auto data, auto&) {
+ dbg() << "Favicon downloaded, " << data.size() << " bytes from " << favicon_url;
+ auto decoder = Gfx::ImageDecoder::create(data.data(), data.size());
+ auto bitmap = decoder->bitmap();
+ if (!bitmap) {
+ dbg() << "Could not decode favicon " << favicon_url;
+ return;
+ }
+ dbg() << "Decoded favicon, " << bitmap->size();
+ if (auto* page = frame().page())
+ page->client().page_did_change_favicon(*bitmap);
+ });
+ }
+
+ return true;
+}
+
+bool FrameLoader::load(const URL& url, Type type)
+{
+ dbg() << "FrameLoader::load: " << url;
+
+ if (!url.is_valid()) {
+ load_error_page(url, "Invalid URL");
+ return false;
+ }
+
+ LoadRequest request;
+ request.set_url(url);
+
+ return load(request, type);
+}
+
+void FrameLoader::load_html(const StringView& html, const URL& url)
+{
+ auto document = DOM::Document::create(url);
+ HTML::HTMLDocumentParser parser(document, html, "utf-8");
+ parser.run(url);
+ frame().set_document(&parser.document());
+}
+
+// FIXME: Use an actual templating engine (our own one when it's built, preferably
+// with a way to check these usages at compile time)
+
+void FrameLoader::load_error_page(const URL& failed_url, const String& error)
+{
+ auto error_page_url = "file:///res/html/error.html";
+ ResourceLoader::the().load(
+ error_page_url,
+ [this, failed_url, error](auto data, auto&) {
+ ASSERT(!data.is_null());
+#pragma GCC diagnostic ignored "-Wformat-nonliteral"
+ auto html = String::format(
+ String::copy(data).characters(),
+ escape_html_entities(failed_url.to_string()).characters(),
+ escape_html_entities(error).characters());
+#pragma GCC diagnostic pop
+ auto document = HTML::parse_html_document(html, failed_url, "utf-8");
+ ASSERT(document);
+ frame().set_document(document);
+ },
+ [](auto error) {
+ dbg() << "Failed to load error page: " << error;
+ ASSERT_NOT_REACHED();
+ });
+}
+
+void FrameLoader::resource_did_load()
+{
+ auto url = resource()->url();
+
+ if (!resource()->has_encoded_data()) {
+ load_error_page(url, "No data");
+ return;
+ }
+
+ // FIXME: Also check HTTP status code before redirecting
+ auto location = resource()->response_headers().get("Location");
+ if (location.has_value()) {
+ load(url.complete_url(location.value()), FrameLoader::Type::Navigation);
+ return;
+ }
+
+ dbgln("I believe this content has MIME type '{}', , encoding '{}'", resource()->mime_type(), resource()->encoding());
+
+ auto document = DOM::Document::create();
+ document->set_url(url);
+ document->set_encoding(resource()->encoding());
+ document->set_content_type(resource()->mime_type());
+
+ frame().set_document(document);
+
+ if (!parse_document(*document, resource()->encoded_data())) {
+ load_error_page(url, "Failed to parse content.");
+ return;
+ }
+
+ if (!url.fragment().is_empty())
+ frame().scroll_to_anchor(url.fragment());
+
+ if (auto* host_element = frame().host_element()) {
+ // FIXME: Perhaps in the future we'll have a better common base class for <frame> and <iframe>
+ ASSERT(is<HTML::HTMLIFrameElement>(*host_element));
+ downcast<HTML::HTMLIFrameElement>(*host_element).content_frame_did_load({});
+ }
+
+ if (auto* page = frame().page())
+ page->client().page_did_finish_loading(url);
+}
+
+void FrameLoader::resource_did_fail()
+{
+ load_error_page(resource()->url(), resource()->error());
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/FrameLoader.h b/Userland/Libraries/LibWeb/Loader/FrameLoader.h
new file mode 100644
index 0000000000..f0c3d0bf56
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/FrameLoader.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Forward.h>
+#include <LibWeb/Forward.h>
+#include <LibWeb/Loader/Resource.h>
+
+namespace Web {
+
+class FrameLoader final
+ : public ResourceClient {
+public:
+ enum class Type {
+ Navigation,
+ Reload,
+ IFrame,
+ };
+
+ explicit FrameLoader(Frame&);
+ ~FrameLoader();
+
+ bool load(const URL&, Type);
+ bool load(const LoadRequest&, Type);
+
+ void load_html(const StringView&, const URL&);
+
+ Frame& frame() { return m_frame; }
+ const Frame& frame() const { return m_frame; }
+
+private:
+ // ^ResourceClient
+ virtual void resource_did_load() override;
+ virtual void resource_did_fail() override;
+
+ void load_error_page(const URL& failed_url, const String& error_message);
+ bool parse_document(DOM::Document&, const ByteBuffer& data);
+
+ Frame& m_frame;
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/ImageLoader.cpp b/Userland/Libraries/LibWeb/Loader/ImageLoader.cpp
new file mode 100644
index 0000000000..b56ab98abd
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/ImageLoader.cpp
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <LibCore/Timer.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/ImageDecoder.h>
+#include <LibWeb/Loader/ImageLoader.h>
+#include <LibWeb/Loader/ResourceLoader.h>
+
+namespace Web {
+
+ImageLoader::ImageLoader()
+ : m_timer(Core::Timer::construct())
+{
+}
+
+void ImageLoader::load(const URL& url)
+{
+ m_loading_state = LoadingState::Loading;
+ LoadRequest request;
+ request.set_url(url);
+ set_resource(ResourceLoader::the().load_resource(Resource::Type::Image, request));
+}
+
+void ImageLoader::set_visible_in_viewport(bool visible_in_viewport) const
+{
+ if (m_visible_in_viewport == visible_in_viewport)
+ return;
+ m_visible_in_viewport = visible_in_viewport;
+
+ // FIXME: Don't update volatility every time. If we're here, we're probably scanning through
+ // the whole document, updating "is visible in viewport" flags, and this could lead
+ // to the same bitmap being marked volatile back and forth unnecessarily.
+ if (resource())
+ const_cast<ImageResource*>(resource())->update_volatility();
+}
+
+void ImageLoader::resource_did_load()
+{
+ ASSERT(resource());
+
+ if (!resource()->mime_type().starts_with("image/")) {
+ m_loading_state = LoadingState::Failed;
+ if (on_fail)
+ on_fail();
+ return;
+ }
+
+ m_loading_state = LoadingState::Loaded;
+
+#ifdef IMAGE_LOADER_DEBUG
+ if (!resource()->has_encoded_data()) {
+ dbg() << "ImageLoader: Resource did load, no encoded data. URL: " << resource()->url();
+ } else {
+ dbg() << "ImageLoader: Resource did load, has encoded data. URL: " << resource()->url();
+ }
+#endif
+
+ if (resource()->should_decode_in_process()) {
+ auto& decoder = resource()->ensure_decoder();
+
+ if (decoder.is_animated() && decoder.frame_count() > 1) {
+ const auto& first_frame = decoder.frame(0);
+ m_timer->set_interval(first_frame.duration);
+ m_timer->on_timeout = [this] { animate(); };
+ m_timer->start();
+ }
+ }
+
+ if (on_load)
+ on_load();
+}
+
+void ImageLoader::animate()
+{
+ if (!m_visible_in_viewport)
+ return;
+
+ auto& decoder = resource()->ensure_decoder();
+
+ m_current_frame_index = (m_current_frame_index + 1) % decoder.frame_count();
+ const auto& current_frame = decoder.frame(m_current_frame_index);
+
+ if (current_frame.duration != m_timer->interval()) {
+ m_timer->restart(current_frame.duration);
+ }
+
+ if (m_current_frame_index == decoder.frame_count() - 1) {
+ ++m_loops_completed;
+ if (m_loops_completed > 0 && m_loops_completed == decoder.loop_count()) {
+ m_timer->stop();
+ }
+ }
+
+ if (on_animate)
+ on_animate();
+}
+
+void ImageLoader::resource_did_fail()
+{
+ dbg() << "ImageLoader: Resource did fail. URL: " << resource()->url();
+ m_loading_state = LoadingState::Failed;
+ if (on_fail)
+ on_fail();
+}
+
+bool ImageLoader::has_image() const
+{
+ if (!resource())
+ return false;
+ if (resource()->should_decode_in_process())
+ return const_cast<ImageResource*>(resource())->ensure_decoder().bitmap();
+ return true;
+}
+
+unsigned ImageLoader::width() const
+{
+ if (!resource())
+ return 0;
+ if (resource()->should_decode_in_process())
+ return const_cast<ImageResource*>(resource())->ensure_decoder().width();
+ return bitmap() ? bitmap()->width() : 0;
+}
+
+unsigned ImageLoader::height() const
+{
+ if (!resource())
+ return 0;
+ if (resource()->should_decode_in_process())
+ return const_cast<ImageResource*>(resource())->ensure_decoder().height();
+ return bitmap() ? bitmap()->height() : 0;
+}
+
+const Gfx::Bitmap* ImageLoader::bitmap() const
+{
+ if (!resource())
+ return nullptr;
+ return resource()->bitmap(m_current_frame_index);
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/ImageLoader.h b/Userland/Libraries/LibWeb/Loader/ImageLoader.h
new file mode 100644
index 0000000000..f07b6e3055
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/ImageLoader.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <LibCore/Timer.h>
+#include <LibWeb/Loader/ImageResource.h>
+
+namespace Web {
+
+class ImageLoader : public ImageResourceClient {
+public:
+ ImageLoader();
+
+ void load(const URL&);
+
+ const Gfx::Bitmap* bitmap() const;
+
+ bool has_image() const;
+
+ bool has_loaded_or_failed() const { return m_loading_state != LoadingState::Loading; }
+
+ void set_visible_in_viewport(bool) const;
+
+ unsigned width() const;
+ unsigned height() const;
+
+ Function<void()> on_load;
+ Function<void()> on_fail;
+ Function<void()> on_animate;
+
+private:
+ // ^ImageResourceClient
+ virtual void resource_did_load() override;
+ virtual void resource_did_fail() override;
+ virtual bool is_visible_in_viewport() const override { return m_visible_in_viewport; }
+
+ void animate();
+
+ enum class LoadingState {
+ None,
+ Loading,
+ Loaded,
+ Failed,
+ };
+
+ mutable bool m_visible_in_viewport { false };
+
+ size_t m_current_frame_index { 0 };
+ size_t m_loops_completed { 0 };
+ LoadingState m_loading_state { LoadingState::Loading };
+ NonnullRefPtr<Core::Timer> m_timer;
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/ImageResource.cpp b/Userland/Libraries/LibWeb/Loader/ImageResource.cpp
new file mode 100644
index 0000000000..3daa284deb
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/ImageResource.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Function.h>
+#include <LibGfx/Bitmap.h>
+#include <LibGfx/ImageDecoder.h>
+#include <LibImageDecoderClient/Client.h>
+#include <LibWeb/Loader/ImageResource.h>
+
+namespace Web {
+
+ImageResource::ImageResource(const LoadRequest& request)
+ : Resource(Type::Image, request)
+{
+}
+
+ImageResource::~ImageResource()
+{
+}
+
+bool ImageResource::should_decode_in_process() const
+{
+ return mime_type() == "image/gif";
+}
+
+Gfx::ImageDecoder& ImageResource::ensure_decoder()
+{
+ if (!m_decoder)
+ m_decoder = Gfx::ImageDecoder::create(encoded_data());
+ return *m_decoder;
+}
+
+const Gfx::Bitmap* ImageResource::bitmap(size_t frame_index) const
+{
+ if (!has_encoded_data())
+ return nullptr;
+
+ if (should_decode_in_process()) {
+ if (!m_decoder)
+ return nullptr;
+ if (m_decoder->is_animated())
+ m_decoded_image = m_decoder->frame(frame_index).image;
+ else
+ m_decoded_image = m_decoder->bitmap();
+ } else if (!m_decoded_image && !m_has_attempted_decode) {
+ auto image_decoder_client = ImageDecoderClient::Client::construct();
+ m_decoded_image = image_decoder_client->decode_image(encoded_data());
+ m_has_attempted_decode = true;
+ }
+ return m_decoded_image;
+}
+
+void ImageResource::update_volatility()
+{
+ if (!m_decoder)
+ return;
+
+ bool visible_in_viewport = false;
+ for_each_client([&](auto& client) {
+ if (static_cast<const ImageResourceClient&>(client).is_visible_in_viewport())
+ visible_in_viewport = true;
+ });
+
+ if (!visible_in_viewport) {
+ m_decoder->set_volatile();
+ return;
+ }
+
+ bool still_has_decoded_image = m_decoder->set_nonvolatile();
+ if (still_has_decoded_image)
+ return;
+
+ m_decoder = nullptr;
+}
+
+ImageResourceClient::~ImageResourceClient()
+{
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/ImageResource.h b/Userland/Libraries/LibWeb/Loader/ImageResource.h
new file mode 100644
index 0000000000..0e66785a70
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/ImageResource.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibWeb/Loader/Resource.h>
+
+namespace Web {
+
+class ImageResource final : public Resource {
+ friend class Resource;
+
+public:
+ virtual ~ImageResource() override;
+ Gfx::ImageDecoder& ensure_decoder();
+ const Gfx::Bitmap* bitmap(size_t frame_index = 0) const;
+
+ bool should_decode_in_process() const;
+
+ void update_volatility();
+
+private:
+ explicit ImageResource(const LoadRequest&);
+ RefPtr<Gfx::ImageDecoder> m_decoder;
+ mutable RefPtr<Gfx::Bitmap> m_decoded_image;
+ mutable bool m_has_attempted_decode { false };
+};
+
+class ImageResourceClient : public ResourceClient {
+public:
+ virtual ~ImageResourceClient();
+
+ virtual bool is_visible_in_viewport() const { return false; }
+
+protected:
+ ImageResource* resource() { return static_cast<ImageResource*>(ResourceClient::resource()); }
+ const ImageResource* resource() const { return static_cast<const ImageResource*>(ResourceClient::resource()); }
+
+private:
+ virtual Resource::Type client_type() const override { return Resource::Type::Image; }
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/LoadRequest.h b/Userland/Libraries/LibWeb/Loader/LoadRequest.h
new file mode 100644
index 0000000000..149ce2537c
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/LoadRequest.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/ByteBuffer.h>
+#include <AK/HashMap.h>
+#include <AK/URL.h>
+#include <LibWeb/Forward.h>
+
+namespace Web {
+
+class LoadRequest {
+public:
+ LoadRequest()
+ {
+ }
+
+ bool is_valid() const { return m_url.is_valid(); }
+
+ const URL& url() const { return m_url; }
+ void set_url(const URL& url) { m_url = url; }
+
+ const String& method() const { return m_method; }
+ void set_method(const String& method) { m_method = method; }
+
+ const ByteBuffer& body() const { return m_body; }
+ void set_body(const ByteBuffer& body) { m_body = body; }
+
+ unsigned hash() const
+ {
+ // FIXME: Include headers in the hash as well
+ return pair_int_hash(pair_int_hash(m_url.to_string().hash(), m_method.hash()), string_hash((const char*)m_body.data(), m_body.size()));
+ }
+
+ bool operator==(const LoadRequest& other) const
+ {
+ if (m_headers.size() != other.m_headers.size())
+ return false;
+ for (auto& it : m_headers) {
+ auto jt = other.m_headers.find(it.key);
+ if (jt == other.m_headers.end())
+ return false;
+ if (it.value != jt->value)
+ return false;
+ }
+ return m_url == other.m_url && m_method == other.m_method && m_body == other.m_body;
+ }
+
+ void set_header(const String& name, const String& value) { m_headers.set(name, value); }
+ String header(const String& name) const { return m_headers.get(name).value_or({}); }
+
+ const HashMap<String, String>& headers() const { return m_headers; }
+
+private:
+ URL m_url;
+ String m_method { "GET" };
+ HashMap<String, String> m_headers;
+ ByteBuffer m_body;
+};
+
+}
+
+namespace AK {
+
+template<>
+struct Traits<Web::LoadRequest> : public GenericTraits<Web::LoadRequest> {
+ static unsigned hash(const Web::LoadRequest& request) { return request.hash(); }
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/Resource.cpp b/Userland/Libraries/LibWeb/Loader/Resource.cpp
new file mode 100644
index 0000000000..2cb6ebc52d
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/Resource.cpp
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Function.h>
+#include <LibCore/MimeData.h>
+#include <LibWeb/HTML/HTMLImageElement.h>
+#include <LibWeb/Loader/Resource.h>
+
+namespace Web {
+
+NonnullRefPtr<Resource> Resource::create(Badge<ResourceLoader>, Type type, const LoadRequest& request)
+{
+ if (type == Type::Image)
+ return adopt(*new ImageResource(request));
+ return adopt(*new Resource(type, request));
+}
+
+Resource::Resource(Type type, const LoadRequest& request)
+ : m_request(request)
+ , m_type(type)
+{
+}
+
+Resource::~Resource()
+{
+}
+
+void Resource::for_each_client(Function<void(ResourceClient&)> callback)
+{
+ Vector<WeakPtr<ResourceClient>, 16> clients_copy;
+ clients_copy.ensure_capacity(m_clients.size());
+ for (auto* client : m_clients)
+ clients_copy.append(client->make_weak_ptr());
+ for (auto client : clients_copy) {
+ if (client)
+ callback(*client);
+ }
+}
+
+static String encoding_from_content_type(const String& content_type)
+{
+ auto offset = content_type.index_of("charset=");
+ if (offset.has_value()) {
+ auto encoding = content_type.substring(offset.value() + 8, content_type.length() - offset.value() - 8).to_lowercase();
+ if (encoding.length() >= 2 && encoding.starts_with('"') && encoding.ends_with('"'))
+ return encoding.substring(1, encoding.length() - 2);
+ if (encoding.length() >= 2 && encoding.starts_with('\'') && encoding.ends_with('\''))
+ return encoding.substring(1, encoding.length() - 2);
+ return encoding;
+ }
+
+ return "utf-8";
+}
+
+static String mime_type_from_content_type(const String& content_type)
+{
+ auto offset = content_type.index_of(";");
+ if (offset.has_value())
+ return content_type.substring(0, offset.value()).to_lowercase();
+
+ return content_type;
+}
+
+void Resource::did_load(Badge<ResourceLoader>, ReadonlyBytes data, const HashMap<String, String, CaseInsensitiveStringTraits>& headers)
+{
+ ASSERT(!m_loaded);
+ m_encoded_data = ByteBuffer::copy(data);
+ m_response_headers = headers;
+ m_loaded = true;
+
+ auto content_type = headers.get("Content-Type");
+ if (content_type.has_value()) {
+#ifdef RESOURCE_DEBUG
+ dbgln("Content-Type header: '{}'", content_type.value());
+#endif
+ m_encoding = encoding_from_content_type(content_type.value());
+ m_mime_type = mime_type_from_content_type(content_type.value());
+ } else if (url().protocol() == "data" && !url().data_mime_type().is_empty()) {
+#ifdef RESOURCE_DEBUG
+ dbg() << "This is a data URL with mime-type _" << url().data_mime_type() << "_";
+#endif
+ m_encoding = "utf-8"; // FIXME: This doesn't seem nice.
+ m_mime_type = url().data_mime_type();
+ } else {
+#ifdef RESOURCE_DEBUG
+ dbgln("No Content-Type header to go on! Guessing based on filename...");
+#endif
+ m_encoding = "utf-8"; // FIXME: This doesn't seem nice.
+ m_mime_type = Core::guess_mime_type_based_on_filename(url().path());
+ }
+
+ for_each_client([](auto& client) {
+ client.resource_did_load();
+ });
+}
+
+void Resource::did_fail(Badge<ResourceLoader>, const String& error)
+{
+ m_error = error;
+ m_failed = true;
+
+ for_each_client([](auto& client) {
+ client.resource_did_fail();
+ });
+}
+
+void Resource::register_client(Badge<ResourceClient>, ResourceClient& client)
+{
+ ASSERT(!m_clients.contains(&client));
+ m_clients.set(&client);
+}
+
+void Resource::unregister_client(Badge<ResourceClient>, ResourceClient& client)
+{
+ ASSERT(m_clients.contains(&client));
+ m_clients.remove(&client);
+}
+
+void ResourceClient::set_resource(Resource* resource)
+{
+ if (m_resource)
+ m_resource->unregister_client({}, *this);
+ m_resource = resource;
+ if (m_resource) {
+ ASSERT(resource->type() == client_type());
+
+ m_resource->register_client({}, *this);
+
+ // Make sure that reused resources also have their load callback fired.
+ if (resource->is_loaded())
+ resource_did_load();
+
+ // Make sure that reused resources also have their fail callback fired.
+ if (resource->is_failed())
+ resource_did_fail();
+ }
+}
+
+ResourceClient::~ResourceClient()
+{
+ if (m_resource)
+ m_resource->unregister_client({}, *this);
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/Resource.h b/Userland/Libraries/LibWeb/Loader/Resource.h
new file mode 100644
index 0000000000..9838071c65
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/Resource.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/ByteBuffer.h>
+#include <AK/HashMap.h>
+#include <AK/HashTable.h>
+#include <AK/Noncopyable.h>
+#include <AK/RefCounted.h>
+#include <AK/URL.h>
+#include <AK/WeakPtr.h>
+#include <AK/Weakable.h>
+#include <LibGfx/Forward.h>
+#include <LibWeb/Forward.h>
+#include <LibWeb/Loader/LoadRequest.h>
+
+namespace Web {
+
+class ResourceClient;
+
+class Resource : public RefCounted<Resource> {
+ AK_MAKE_NONCOPYABLE(Resource);
+ AK_MAKE_NONMOVABLE(Resource);
+
+public:
+ enum class Type {
+ Generic,
+ Image,
+ };
+
+ static NonnullRefPtr<Resource> create(Badge<ResourceLoader>, Type, const LoadRequest&);
+ virtual ~Resource();
+
+ Type type() const { return m_type; }
+
+ bool is_loaded() const { return m_loaded; }
+
+ bool is_failed() const { return m_failed; }
+ const String& error() const { return m_error; }
+
+ bool has_encoded_data() const { return !m_encoded_data.is_null(); }
+
+ const URL& url() const { return m_request.url(); }
+ const ByteBuffer& encoded_data() const { return m_encoded_data; }
+
+ const HashMap<String, String, CaseInsensitiveStringTraits>& response_headers() const { return m_response_headers; }
+
+ void register_client(Badge<ResourceClient>, ResourceClient&);
+ void unregister_client(Badge<ResourceClient>, ResourceClient&);
+
+ const String& encoding() const { return m_encoding; }
+ const String& mime_type() const { return m_mime_type; }
+
+ void for_each_client(Function<void(ResourceClient&)>);
+
+ void did_load(Badge<ResourceLoader>, ReadonlyBytes data, const HashMap<String, String, CaseInsensitiveStringTraits>& headers);
+ void did_fail(Badge<ResourceLoader>, const String& error);
+
+protected:
+ explicit Resource(Type, const LoadRequest&);
+
+private:
+ LoadRequest m_request;
+ ByteBuffer m_encoded_data;
+ Type m_type { Type::Generic };
+ bool m_loaded { false };
+ bool m_failed { false };
+ String m_error;
+ String m_encoding;
+ String m_mime_type;
+ HashMap<String, String, CaseInsensitiveStringTraits> m_response_headers;
+ HashTable<ResourceClient*> m_clients;
+};
+
+class ResourceClient : public Weakable<ResourceClient> {
+public:
+ virtual ~ResourceClient();
+
+ virtual void resource_did_load() { }
+ virtual void resource_did_fail() { }
+
+protected:
+ virtual Resource::Type client_type() const { return Resource::Type::Generic; }
+
+ Resource* resource() { return m_resource; }
+ const Resource* resource() const { return m_resource; }
+ void set_resource(Resource*);
+
+private:
+ RefPtr<Resource> m_resource;
+};
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/ResourceLoader.cpp b/Userland/Libraries/LibWeb/Loader/ResourceLoader.cpp
new file mode 100644
index 0000000000..74ec1194d4
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/ResourceLoader.cpp
@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/Base64.h>
+#include <AK/JsonObject.h>
+#include <AK/SharedBuffer.h>
+#include <LibCore/EventLoop.h>
+#include <LibCore/File.h>
+#include <LibProtocol/Client.h>
+#include <LibProtocol/Download.h>
+#include <LibWeb/Loader/ContentFilter.h>
+#include <LibWeb/Loader/LoadRequest.h>
+#include <LibWeb/Loader/Resource.h>
+#include <LibWeb/Loader/ResourceLoader.h>
+
+//#define CACHE_DEBUG
+
+namespace Web {
+
+ResourceLoader& ResourceLoader::the()
+{
+ static ResourceLoader* s_the;
+ if (!s_the)
+ s_the = &ResourceLoader::construct().leak_ref();
+ return *s_the;
+}
+
+ResourceLoader::ResourceLoader()
+ : m_protocol_client(Protocol::Client::construct())
+ , m_user_agent("Mozilla/4.0 (SerenityOS; x86) LibWeb+LibJS (Not KHTML, nor Gecko) LibWeb")
+{
+}
+
+void ResourceLoader::load_sync(const URL& url, Function<void(ReadonlyBytes, const HashMap<String, String, CaseInsensitiveStringTraits>& response_headers)> success_callback, Function<void(const String&)> error_callback)
+{
+ Core::EventLoop loop;
+
+ load(
+ url,
+ [&](auto data, auto& response_headers) {
+ success_callback(data, response_headers);
+ loop.quit(0);
+ },
+ [&](auto& string) {
+ if (error_callback)
+ error_callback(string);
+ loop.quit(0);
+ });
+
+ loop.exec();
+}
+
+static HashMap<LoadRequest, NonnullRefPtr<Resource>> s_resource_cache;
+
+RefPtr<Resource> ResourceLoader::load_resource(Resource::Type type, const LoadRequest& request)
+{
+ if (!request.is_valid())
+ return nullptr;
+
+ auto it = s_resource_cache.find(request);
+ if (it != s_resource_cache.end()) {
+ if (it->value->type() != type) {
+ dbg() << "FIXME: Not using cached resource for " << request.url() << " since there's a type mismatch.";
+ } else {
+#ifdef CACHE_DEBUG
+ dbg() << "Reusing cached resource for: " << request.url();
+#endif
+ return it->value;
+ }
+ }
+
+ auto resource = Resource::create({}, type, request);
+
+ s_resource_cache.set(request, resource);
+
+ load(
+ request,
+ [=](auto data, auto& headers) {
+ const_cast<Resource&>(*resource).did_load({}, data, headers);
+ },
+ [=](auto& error) {
+ const_cast<Resource&>(*resource).did_fail({}, error);
+ });
+
+ return resource;
+}
+
+void ResourceLoader::load(const LoadRequest& request, Function<void(ReadonlyBytes, const HashMap<String, String, CaseInsensitiveStringTraits>& response_headers)> success_callback, Function<void(const String&)> error_callback)
+{
+ auto& url = request.url();
+
+ if (is_port_blocked(url.port())) {
+ dbg() << "ResourceLoader::load: Error: blocked port " << url.port() << " for URL: " << url;
+ return;
+ }
+
+ if (ContentFilter::the().is_filtered(url)) {
+ dbgln("\033[32;1mResourceLoader::load: URL was filtered! {}\033[0m", url);
+ error_callback("URL was filtered");
+ return;
+ }
+
+ if (url.protocol() == "about") {
+ dbg() << "Loading about: URL " << url;
+ deferred_invoke([success_callback = move(success_callback)](auto&) {
+ success_callback(String::empty().to_byte_buffer(), {});
+ });
+ return;
+ }
+
+ if (url.protocol() == "data") {
+ dbg() << "ResourceLoader loading a data URL with mime-type: '" << url.data_mime_type() << "', base64=" << url.data_payload_is_base64() << ", payload='" << url.data_payload() << "'";
+
+ ByteBuffer data;
+ if (url.data_payload_is_base64())
+ data = decode_base64(url.data_payload());
+ else
+ data = url.data_payload().to_byte_buffer();
+
+ deferred_invoke([data = move(data), success_callback = move(success_callback)](auto&) {
+ success_callback(data, {});
+ });
+ return;
+ }
+
+ if (url.protocol() == "file") {
+ auto f = Core::File::construct();
+ f->set_filename(url.path());
+ if (!f->open(Core::IODevice::OpenMode::ReadOnly)) {
+ dbg() << "ResourceLoader::load: Error: " << f->error_string();
+ if (error_callback)
+ error_callback(f->error_string());
+ return;
+ }
+
+ auto data = f->read_all();
+ deferred_invoke([data = move(data), success_callback = move(success_callback)](auto&) {
+ success_callback(data, {});
+ });
+ return;
+ }
+
+ if (url.protocol() == "http" || url.protocol() == "https" || url.protocol() == "gemini") {
+ HashMap<String, String> headers;
+ headers.set("User-Agent", m_user_agent);
+ headers.set("Accept-Encoding", "gzip");
+
+ for (auto& it : request.headers()) {
+ headers.set(it.key, it.value);
+ }
+
+ auto download = protocol_client().start_download(request.method(), url.to_string(), headers, request.body());
+ if (!download) {
+ if (error_callback)
+ error_callback("Failed to initiate load");
+ return;
+ }
+ download->on_buffered_download_finish = [this, success_callback = move(success_callback), error_callback = move(error_callback), download](bool success, auto, auto& response_headers, auto status_code, ReadonlyBytes payload) {
+ if (status_code.has_value() && status_code.value() >= 400 && status_code.value() <= 499) {
+ if (error_callback)
+ error_callback(String::formatted("HTTP error ({})", status_code.value()));
+ return;
+ }
+ --m_pending_loads;
+ if (on_load_counter_change)
+ on_load_counter_change();
+ if (!success) {
+ if (error_callback)
+ error_callback("HTTP load failed");
+ return;
+ }
+ deferred_invoke([download](auto&) {
+ // Clear circular reference of `download` captured by copy
+ const_cast<Protocol::Download&>(*download).on_buffered_download_finish = nullptr;
+ });
+ success_callback(payload, response_headers);
+ };
+ download->set_should_buffer_all_input(true);
+ download->on_certificate_requested = []() -> Protocol::Download::CertificateAndKey {
+ return {};
+ };
+ ++m_pending_loads;
+ if (on_load_counter_change)
+ on_load_counter_change();
+ return;
+ }
+
+ if (error_callback)
+ error_callback(String::formatted("Protocol not implemented: {}", url.protocol()));
+}
+
+void ResourceLoader::load(const URL& url, Function<void(ReadonlyBytes, const HashMap<String, String, CaseInsensitiveStringTraits>& response_headers)> success_callback, Function<void(const String&)> error_callback)
+{
+ LoadRequest request;
+ request.set_url(url);
+ load(request, move(success_callback), move(error_callback));
+}
+
+bool ResourceLoader::is_port_blocked(int port)
+{
+ int ports[] { 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42,
+ 43, 53, 77, 79, 87, 95, 101, 102, 103, 104, 109, 110, 111, 113,
+ 115, 117, 119, 123, 135, 139, 143, 179, 389, 465, 512, 513, 514,
+ 515, 526, 530, 531, 532, 540, 556, 563, 587, 601, 636, 993, 995,
+ 2049, 3659, 4045, 6000, 6379, 6665, 6666, 6667, 6668, 6669, 9000 };
+ for (auto blocked_port : ports)
+ if (port == blocked_port)
+ return true;
+ return false;
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/Loader/ResourceLoader.h b/Userland/Libraries/LibWeb/Loader/ResourceLoader.h
new file mode 100644
index 0000000000..5fe23adc8a
--- /dev/null
+++ b/Userland/Libraries/LibWeb/Loader/ResourceLoader.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/Function.h>
+#include <AK/URL.h>
+#include <LibCore/Object.h>
+#include <LibWeb/Loader/Resource.h>
+
+namespace Protocol {
+class Client;
+}
+
+namespace Web {
+
+class ResourceLoader : public Core::Object {
+ C_OBJECT(ResourceLoader)
+public:
+ static ResourceLoader& the();
+
+ RefPtr<Resource> load_resource(Resource::Type, const LoadRequest&);
+
+ void load(const LoadRequest&, Function<void(ReadonlyBytes, const HashMap<String, String, CaseInsensitiveStringTraits>& response_headers)> success_callback, Function<void(const String&)> error_callback = nullptr);
+ void load(const URL&, Function<void(ReadonlyBytes, const HashMap<String, String, CaseInsensitiveStringTraits>& response_headers)> success_callback, Function<void(const String&)> error_callback = nullptr);
+ void load_sync(const URL&, Function<void(ReadonlyBytes, const HashMap<String, String, CaseInsensitiveStringTraits>& response_headers)> success_callback, Function<void(const String&)> error_callback = nullptr);
+
+ Function<void()> on_load_counter_change;
+
+ int pending_loads() const { return m_pending_loads; }
+
+ Protocol::Client& protocol_client() { return *m_protocol_client; }
+
+ const String& user_agent() const { return m_user_agent; }
+
+private:
+ ResourceLoader();
+ static bool is_port_blocked(int port);
+
+ int m_pending_loads { 0 };
+
+ RefPtr<Protocol::Client> m_protocol_client;
+ String m_user_agent;
+};
+
+}