diff --git a/src/wp-includes/post-template.php b/src/wp-includes/post-template.php
index b5c3772828..27f55fc7df 100644
--- a/src/wp-includes/post-template.php
+++ b/src/wp-includes/post-template.php
@@ -1666,7 +1666,24 @@ function wp_get_attachment_link( $post = 0, $size = 'thumbnail', $permalink = fa
$link_text = esc_html( pathinfo( get_attached_file( $_post->ID ), PATHINFO_FILENAME ) );
}
- $link_html = "$link_text";
+ /**
+ * Filters the list of attachment link attributes.
+ *
+ * @since 6.2.0
+ *
+ * @param array $attributes An array of attributes for the link markup,
+ * keyed on the attribute name.
+ * @param int $id Post ID.
+ */
+ $attributes = apply_filters( 'wp_get_attachment_link_attributes', array( 'href' => $url ), $_post->ID );
+
+ $link_attributes = '';
+ foreach ( $attributes as $name => $value ) {
+ $value = 'href' === $name ? esc_url( $value ) : esc_attr( $value );
+ $link_attributes .= ' ' . esc_attr( $name ) . "='" . $value . "'";
+ }
+
+ $link_html = "$link_text";
/**
* Filters a retrieved attachment page link.
diff --git a/tests/phpunit/tests/post/wpGetAttachmentLink.php b/tests/phpunit/tests/post/wpGetAttachmentLink.php
new file mode 100644
index 0000000000..2d658e002e
--- /dev/null
+++ b/tests/phpunit/tests/post/wpGetAttachmentLink.php
@@ -0,0 +1,106 @@
+attachment->create();
+ }
+
+ /**
+ * Tests that wp_get_attachment_link() applies the
+ * wp_get_attachment_link_attributes filter.
+ *
+ * @ticket 41574
+ *
+ * @dataProvider data_should_apply_attributes_filter
+ *
+ * @param array $attributes Attributes to return from the callback.
+ * @param string $expected The substring expected to be in the attachment link.
+ */
+ public function test_should_apply_attributes_filter( $attributes, $expected ) {
+ $expected = str_replace( 'ATTACHMENT_ID', self::$attachment, $expected );
+
+ add_filter(
+ 'wp_get_attachment_link_attributes',
+ static function( $attr ) use ( $attributes ) {
+ return array_merge( $attr, $attributes );
+ }
+ );
+
+ $this->assertStringContainsString(
+ $expected,
+ wp_get_attachment_link( self::$attachment )
+ );
+ }
+
+ /**
+ * Data provider for test_should_apply_attributes_filter().
+ *
+ * @return array[]
+ */
+ public function data_should_apply_attributes_filter() {
+ return array(
+ 'no new attributes' => array(
+ 'attributes' => array(),
+ 'expected' => "",
+ ),
+ 'one new attribute' => array(
+ 'attributes' => array(
+ 'class' => 'test-attribute-filter',
+ ),
+ 'expected' => " class='test-attribute-filter'",
+ ),
+ 'two new attributes' => array(
+ 'attributes' => array(
+ 'class' => 'test-attribute-filter',
+ 'id' => 'test-attribute-filter-1',
+ ),
+ 'expected' => " class='test-attribute-filter' id='test-attribute-filter-1'",
+ ),
+ 'an existing attribute' => array(
+ 'attributes' => array(
+ 'href' => 'http://test-attribute-filter.org',
+ ),
+ 'expected' => " href='http://test-attribute-filter.org'",
+ ),
+ 'an existing attribute and a new attribute' => array(
+ 'attributes' => array(
+ 'href' => 'http://test-attribute-filter.org',
+ 'class' => 'test-attribute-filter',
+ ),
+ 'expected' => " href='http://test-attribute-filter.org' class='test-attribute-filter'",
+ ),
+ 'an attribute name with unsafe characters' => array(
+ 'attributes' => array(
+ "> '',
+ ),
+ 'expected' => " > <script>alert('Howdy, admin!')</script> <a href=''></a=''",
+ ),
+ 'an attribute value with unsafe characters' => array(
+ 'attributes' => array(
+ 'class' => "'> ''> <script>alert('Howdy, admin!')</script> <a href=''></a',
+ ),
+ );
+ }
+}