diff --git a/NEWS b/NEWS
index 39dd522e..b12c63ad 100644
--- a/NEWS
+++ b/NEWS
@@ -19,6 +19,7 @@ NEWS ( CHANGELOG and HISTORY ) HTMLPurifier
- Deleted some asserts to avoid linters from choking (#97)
- Rework Serializer cache behavior to avoid chmod'ing if possible (#32)
- Embedded semicolons in strings in CSS are now handled correctly!
+! Added %HTML.Noopener to add rel="noopener" to external links.
4.8.0, released 2016-07-16
# By default, when a link has a target attribute associated
diff --git a/configdoc/usage.xml b/configdoc/usage.xml
index d80ab51e..67020803 100644
--- a/configdoc/usage.xml
+++ b/configdoc/usage.xml
@@ -222,14 +222,19 @@
268
-
+
271
+
+
+ 274
+
+
- 276
+ 279
diff --git a/library/HTMLPurifier.includes.php b/library/HTMLPurifier.includes.php
index b1131ef9..0a4c6096 100644
--- a/library/HTMLPurifier.includes.php
+++ b/library/HTMLPurifier.includes.php
@@ -132,6 +132,7 @@ require 'HTMLPurifier/AttrTransform/Length.php';
require 'HTMLPurifier/AttrTransform/Name.php';
require 'HTMLPurifier/AttrTransform/NameSync.php';
require 'HTMLPurifier/AttrTransform/Nofollow.php';
+require 'HTMLPurifier/AttrTransform/Noopener.php';
require 'HTMLPurifier/AttrTransform/SafeEmbed.php';
require 'HTMLPurifier/AttrTransform/SafeObject.php';
require 'HTMLPurifier/AttrTransform/SafeParam.php';
@@ -163,6 +164,7 @@ require 'HTMLPurifier/HTMLModule/Legacy.php';
require 'HTMLPurifier/HTMLModule/List.php';
require 'HTMLPurifier/HTMLModule/Name.php';
require 'HTMLPurifier/HTMLModule/Nofollow.php';
+require 'HTMLPurifier/HTMLModule/Noopener.php';
require 'HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php';
require 'HTMLPurifier/HTMLModule/Object.php';
require 'HTMLPurifier/HTMLModule/Presentation.php';
diff --git a/library/HTMLPurifier.safe-includes.php b/library/HTMLPurifier.safe-includes.php
index fe587c78..a81ea5c6 100644
--- a/library/HTMLPurifier.safe-includes.php
+++ b/library/HTMLPurifier.safe-includes.php
@@ -126,6 +126,7 @@ require_once $__dir . '/HTMLPurifier/AttrTransform/Length.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/Name.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/NameSync.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/Nofollow.php';
+require_once $__dir . '/HTMLPurifier/AttrTransform/Noopener.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/SafeEmbed.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/SafeObject.php';
require_once $__dir . '/HTMLPurifier/AttrTransform/SafeParam.php';
@@ -157,6 +158,7 @@ require_once $__dir . '/HTMLPurifier/HTMLModule/Legacy.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/List.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/Name.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/Nofollow.php';
+require_once $__dir . '/HTMLPurifier/HTMLModule/Noopener.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/Object.php';
require_once $__dir . '/HTMLPurifier/HTMLModule/Presentation.php';
diff --git a/library/HTMLPurifier/AttrTransform/Noopener.php b/library/HTMLPurifier/AttrTransform/Noopener.php
new file mode 100644
index 00000000..fb72d307
--- /dev/null
+++ b/library/HTMLPurifier/AttrTransform/Noopener.php
@@ -0,0 +1,52 @@
+parser = new HTMLPurifier_URIParser();
+ }
+
+ /**
+ * @param array $attr
+ * @param HTMLPurifier_Config $config
+ * @param HTMLPurifier_Context $context
+ * @return array
+ */
+ public function transform($attr, $config, $context)
+ {
+ if (!isset($attr['href'])) {
+ return $attr;
+ }
+
+ // XXX Kind of inefficient
+ $url = $this->parser->parse($attr['href']);
+ $scheme = $url->getSchemeObj($config, $context);
+
+ if ($scheme->browsable && !$url->isLocal($config, $context)) {
+ if (isset($attr['rel'])) {
+ $rels = explode(' ', $attr['rel']);
+ if (!in_array('noopener', $rels)) {
+ $rels[] = 'noopener';
+ }
+ $attr['rel'] = implode(' ', $rels);
+ } else {
+ $attr['rel'] = 'noopener';
+ }
+ }
+ return $attr;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/library/HTMLPurifier/ConfigSchema/schema.ser b/library/HTMLPurifier/ConfigSchema/schema.ser
index 0def14c8..1dc1b510 100644
Binary files a/library/HTMLPurifier/ConfigSchema/schema.ser and b/library/HTMLPurifier/ConfigSchema/schema.ser differ
diff --git a/library/HTMLPurifier/ConfigSchema/schema/HTML.Noopener.txt b/library/HTMLPurifier/ConfigSchema/schema/HTML.Noopener.txt
new file mode 100644
index 00000000..59245688
--- /dev/null
+++ b/library/HTMLPurifier/ConfigSchema/schema/HTML.Noopener.txt
@@ -0,0 +1,7 @@
+HTML.Noopener
+TYPE: bool
+VERSION: 4.9.0
+DEFAULT: FALSE
+--DESCRIPTION--
+If enabled, noopener rel attributes are added to all outgoing links.
+--# vim: et sw=4 sts=4
diff --git a/library/HTMLPurifier/HTMLModule/Noopener.php b/library/HTMLPurifier/HTMLModule/Noopener.php
new file mode 100644
index 00000000..1fa629ba
--- /dev/null
+++ b/library/HTMLPurifier/HTMLModule/Noopener.php
@@ -0,0 +1,25 @@
+addBlankElement('a');
+ $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Noopener();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/library/HTMLPurifier/HTMLModuleManager.php b/library/HTMLPurifier/HTMLModuleManager.php
index 2546c043..4d0fc6ea 100644
--- a/library/HTMLPurifier/HTMLModuleManager.php
+++ b/library/HTMLPurifier/HTMLModuleManager.php
@@ -268,6 +268,9 @@ class HTMLPurifier_HTMLModuleManager
if ($config->get('HTML.Nofollow')) {
$modules[] = 'Nofollow';
}
+ if ($config->get('HTML.Noopener')) {
+ $modules[] = 'Noopener';
+ }
if ($config->get('HTML.TargetBlank')) {
$modules[] = 'TargetBlank';
}
diff --git a/tests/HTMLPurifier/HTMLModule/NoopenerTest.php b/tests/HTMLPurifier/HTMLModule/NoopenerTest.php
new file mode 100644
index 00000000..a53fb64e
--- /dev/null
+++ b/tests/HTMLPurifier/HTMLModule/NoopenerTest.php
@@ -0,0 +1,30 @@
+config->set('HTML.Noopener', true);
+ $this->config->set('Attr.AllowedRel', array("noopener", "blah"));
+ }
+
+ public function testNoopener()
+ {
+ $this->assertResult(
+ 'xabc',
+ 'xabc'
+ );
+ }
+
+ public function testNoopenerDupe()
+ {
+ $this->assertResult(
+ 'xabc'
+ );
+ }
+
+}
+
+// vim: et sw=4 sts=4