diff options
author | Andreas Kling <kling@serenityos.org> | 2021-01-12 12:17:30 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-01-12 12:17:46 +0100 |
commit | 13d7c09125f8eec703d0a43a9a87fc8aa08f7319 (patch) | |
tree | 70fd643c429cea5c1f9362c2674511d17a53f3b5 /Userland/Libraries/LibWeb/Loader | |
parent | dc28c07fa526841e05e16161c74a6c23984f1dd5 (diff) | |
download | serenity-13d7c09125f8eec703d0a43a9a87fc8aa08f7319.zip |
Libraries: Move to Userland/Libraries/
Diffstat (limited to 'Userland/Libraries/LibWeb/Loader')
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/ContentFilter.cpp | 68 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/ContentFilter.h | 51 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/FrameLoader.cpp | 296 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/FrameLoader.h | 66 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/ImageLoader.cpp | 164 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/ImageLoader.h | 79 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/ImageResource.cpp | 103 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/ImageResource.h | 66 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/LoadRequest.h | 94 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/Resource.cpp | 168 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/Resource.h | 117 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/ResourceLoader.cpp | 235 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Loader/ResourceLoader.h | 69 |
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; +}; + +} |