diff --git a/Tests/LibWeb/Text/expected/HTML/Window-opener.txt b/Tests/LibWeb/Text/expected/HTML/Window-opener.txt
new file mode 100644
index 0000000000..588c4b7669
--- /dev/null
+++ b/Tests/LibWeb/Text/expected/HTML/Window-opener.txt
@@ -0,0 +1,5 @@
+ window.opener initial value: null
+window.opener after setting to "test": test
+iframe contentWindow.opener initial value is the current window object: true
+iframe contentWindow.opener after setting to null: null
+iframe contentWindow.opener after setting to "test": test
diff --git a/Tests/LibWeb/Text/input/HTML/Window-opener.html b/Tests/LibWeb/Text/input/HTML/Window-opener.html
new file mode 100644
index 0000000000..f47b6262b9
--- /dev/null
+++ b/Tests/LibWeb/Text/input/HTML/Window-opener.html
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/Userland/Libraries/LibWeb/HTML/Window.cpp b/Userland/Libraries/LibWeb/HTML/Window.cpp
index 657aa7fccf..c80f5affb3 100644
--- a/Userland/Libraries/LibWeb/HTML/Window.cpp
+++ b/Userland/Libraries/LibWeb/HTML/Window.cpp
@@ -946,6 +946,41 @@ JS::GCPtr Window::top() const
return navigable->top_level_traversable()->active_window_proxy();
}
+// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-opener
+JS::GCPtr Window::opener() const
+{
+ // 1. Let current be this's browsing context.
+ auto const* current = browsing_context();
+
+ // 2. If current is null, then return null.
+ if (!current)
+ return {};
+
+ // 3. If current's opener browsing context is null, then return null.
+ auto opener_browsing_context = current->opener_browsing_context();
+ if (!opener_browsing_context)
+ return {};
+
+ // 4. Return current's opener browsing context's WindowProxy object.
+ return opener_browsing_context->window_proxy();
+}
+
+WebIDL::ExceptionOr Window::set_opener(JS::Value value)
+{
+ // 1. If the given value is null and this's browsing context is non-null, then set this's browsing context's opener browsing context to null.
+ auto* browsing_context = this->browsing_context();
+ if (value.is_null() && browsing_context)
+ browsing_context->set_opener_browsing_context(nullptr);
+
+ // 2. If the given value is non-null, then perform ? DefinePropertyOrThrow(this, "opener", { [[Value]]: the given value, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }).
+ if (!value.is_null()) {
+ static JS::PropertyKey opener_property_key { "opener", JS::PropertyKey::StringMayBeNumber::No };
+ TRY(define_property_or_throw(opener_property_key, { .value = value, .writable = true, .enumerable = true, .configurable = true }));
+ }
+
+ return {};
+}
+
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-parent
JS::GCPtr Window::parent() const
{
diff --git a/Userland/Libraries/LibWeb/HTML/Window.h b/Userland/Libraries/LibWeb/HTML/Window.h
index 9fae9fb118..6f44b4a2e7 100644
--- a/Userland/Libraries/LibWeb/HTML/Window.h
+++ b/Userland/Libraries/LibWeb/HTML/Window.h
@@ -144,6 +144,8 @@ public:
JS::NonnullGCPtr frames() const;
u32 length();
JS::GCPtr top() const;
+ JS::GCPtr opener() const;
+ WebIDL::ExceptionOr set_opener(JS::Value);
JS::GCPtr parent() const;
JS::GCPtr frame_element() const;
WebIDL::ExceptionOr> open(Optional const& url, Optional const& target, Optional const& features);
diff --git a/Userland/Libraries/LibWeb/HTML/Window.idl b/Userland/Libraries/LibWeb/HTML/Window.idl
index a78bb54cca..48d8969648 100644
--- a/Userland/Libraries/LibWeb/HTML/Window.idl
+++ b/Userland/Libraries/LibWeb/HTML/Window.idl
@@ -35,6 +35,7 @@ interface Window : EventTarget {
[Replaceable] readonly attribute WindowProxy frames;
[Replaceable] readonly attribute unsigned long length;
[LegacyUnforgeable] readonly attribute WindowProxy? top;
+ attribute any opener;
[Replaceable] readonly attribute WindowProxy? parent;
readonly attribute Element? frameElement;
WindowProxy? open(optional USVString url = "", optional DOMString target = "_blank", optional [LegacyNullToEmptyString] DOMString features = "");