diff --git a/src/wp-admin/options-general.php b/src/wp-admin/options-general.php
index 6a9a6d2f3812d..9ca8d70537687 100644
--- a/src/wp-admin/options-general.php
+++ b/src/wp-admin/options-general.php
@@ -97,6 +97,20 @@
+
+ |
+
+
+/.well-known/atproto-did'
+);
+?>
+ |
+
+
|
diff --git a/src/wp-admin/options.php b/src/wp-admin/options.php
index b45cbb00387ce..93ec2fc10320f 100644
--- a/src/wp-admin/options.php
+++ b/src/wp-admin/options.php
@@ -91,6 +91,7 @@
'general' => array(
'blogname',
'blogdescription',
+ 'atproto_did',
'site_icon',
'gmt_offset',
'date_format',
diff --git a/src/wp-includes/atproto.php b/src/wp-includes/atproto.php
new file mode 100644
index 0000000000000..19c38e23636ef
--- /dev/null
+++ b/src/wp-includes/atproto.php
@@ -0,0 +1,83 @@
+is_singular = false;
$this->is_robots = false;
$this->is_favicon = false;
+ $this->is_atproto_did = false;
$this->is_posts_page = false;
$this->is_post_type_archive = false;
}
@@ -817,6 +826,8 @@ public function parse_query( $query = '' ) {
$this->is_robots = true;
} elseif ( ! empty( $query_vars['favicon'] ) ) {
$this->is_favicon = true;
+ } elseif ( ! empty( $query_vars['atproto_did'] ) ) {
+ $this->is_atproto_did = true;
}
if ( ! is_scalar( $query_vars['p'] ) || (int) $query_vars['p'] < 0 ) {
@@ -1040,7 +1051,7 @@ public function parse_query( $query = '' ) {
if ( ! ( $this->is_singular || $this->is_archive || $this->is_search || $this->is_feed
|| ( wp_is_serving_rest_request() && $this->is_main_query() )
- || $this->is_trackback || $this->is_404 || $this->is_admin || $this->is_robots || $this->is_favicon ) ) {
+ || $this->is_trackback || $this->is_404 || $this->is_admin || $this->is_robots || $this->is_favicon || $this->is_atproto_did ) ) {
$this->is_home = true;
}
@@ -4634,6 +4645,17 @@ public function is_favicon() {
return (bool) $this->is_favicon;
}
+ /**
+ * Determines whether the query is for the AT Protocol DID document.
+ *
+ * @since 7.1.0
+ *
+ * @return bool Whether the query is for the AT Protocol DID document.
+ */
+ public function is_atproto_did() {
+ return (bool) $this->is_atproto_did;
+ }
+
/**
* Determines whether the query is for a search.
*
diff --git a/src/wp-includes/class-wp-rewrite.php b/src/wp-includes/class-wp-rewrite.php
index 8b75fa5c36d16..259d40b4cd7b1 100644
--- a/src/wp-includes/class-wp-rewrite.php
+++ b/src/wp-includes/class-wp-rewrite.php
@@ -1287,6 +1287,9 @@ public function rewrite_rules() {
// favicon.ico -- only if installed at the root.
$favicon_rewrite = ( empty( $home_path['path'] ) || '/' === $home_path['path'] ) ? array( 'favicon\.ico$' => $this->index . '?favicon=1' ) : array();
+ // .well-known/atproto-did -- only if installed at the root.
+ $atproto_did_rewrite = ( empty( $home_path['path'] ) || '/' === $home_path['path'] ) ? array( '\.well-known/atproto-did$' => $this->index . '?atproto_did=1' ) : array();
+
// sitemap.xml -- only if installed at the root.
$sitemap_rewrite = ( empty( $home_path['path'] ) || '/' === $home_path['path'] ) ? array( 'sitemap\.xml' => $this->index . '?sitemap=index' ) : array();
@@ -1452,9 +1455,9 @@ public function rewrite_rules() {
// Put them together.
if ( $this->use_verbose_page_rules ) {
- $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $page_rewrite, $post_rewrite, $this->extra_rules );
+ $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $atproto_did_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $page_rewrite, $post_rewrite, $this->extra_rules );
} else {
- $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $post_rewrite, $page_rewrite, $this->extra_rules );
+ $this->rules = array_merge( $this->extra_rules_top, $robots_rewrite, $favicon_rewrite, $atproto_did_rewrite, $sitemap_rewrite, $deprecated_files, $registration_pages, $root_rewrite, $comments_rewrite, $search_rewrite, $author_rewrite, $date_rewrite, $post_rewrite, $page_rewrite, $this->extra_rules );
}
/**
diff --git a/src/wp-includes/class-wp.php b/src/wp-includes/class-wp.php
index f1664747d4042..e9a429fc9db71 100644
--- a/src/wp-includes/class-wp.php
+++ b/src/wp-includes/class-wp.php
@@ -15,7 +15,7 @@ class WP {
* @since 2.0.0
* @var string[]
*/
- public $public_query_vars = array( 'm', 'p', 'posts', 'w', 'cat', 'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence', 'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order', 'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second', 'name', 'category_name', 'tag', 'feed', 'author_name', 'pagename', 'page_id', 'error', 'attachment', 'attachment_id', 'subpost', 'subpost_id', 'preview', 'robots', 'favicon', 'taxonomy', 'term', 'cpage', 'post_type', 'embed' );
+ public $public_query_vars = array( 'm', 'p', 'posts', 'w', 'cat', 'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence', 'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order', 'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second', 'name', 'category_name', 'tag', 'feed', 'author_name', 'pagename', 'page_id', 'error', 'attachment', 'attachment_id', 'subpost', 'subpost_id', 'preview', 'robots', 'favicon', 'atproto_did', 'taxonomy', 'term', 'cpage', 'post_type', 'embed' );
/**
* Private query variables.
@@ -746,8 +746,8 @@ public function handle_404() {
$set_404 = true;
- // Never 404 for the admin, robots, or favicon.
- if ( is_admin() || is_robots() || is_favicon() ) {
+ // Never 404 for the admin, robots, favicon, or AT Protocol DID endpoint.
+ if ( is_admin() || is_robots() || is_favicon() || is_atproto_did() ) {
$set_404 = false;
// If posts were found, check for paged content.
diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
index cf895eb748dbe..2e1c9a67dc2cc 100644
--- a/src/wp-includes/default-filters.php
+++ b/src/wp-includes/default-filters.php
@@ -550,6 +550,9 @@
// Sitemaps actions.
add_action( 'init', 'wp_sitemaps_get_server' );
+// AT Protocol actions.
+add_action( 'do_atproto_did', 'do_atproto_did' );
+
/**
* Filters formerly mixed into wp-includes.
*/
diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php
index b0ba234720d8e..3f5dec5479cb5 100644
--- a/src/wp-includes/formatting.php
+++ b/src/wp-includes/formatting.php
@@ -5003,6 +5003,13 @@ function sanitize_option( $option, $value ) {
}
break;
+ case 'atproto_did':
+ $value = is_string( $value ) ? trim( $value ) : '';
+ if ( '' !== $value && ! wp_is_atproto_did( $value ) ) {
+ $error = __( 'The AT Protocol DID you entered did not appear to be valid. Please enter a did:plc or did:web identifier.' );
+ }
+ break;
+
case 'date_format':
case 'time_format':
case 'mailserver_url':
diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php
index 7979c119a986f..08d2a57ea23fe 100644
--- a/src/wp-includes/option.php
+++ b/src/wp-includes/option.php
@@ -2765,6 +2765,18 @@ function register_initial_settings() {
)
);
+ register_setting(
+ 'general',
+ 'atproto_did',
+ array(
+ 'show_in_rest' => true,
+ 'type' => 'string',
+ 'label' => __( 'AT Protocol DID' ),
+ 'description' => __( 'A decentralized identifier for AT Protocol handle verification.' ),
+ 'default' => '',
+ )
+ );
+
if ( ! is_multisite() ) {
register_setting(
'general',
diff --git a/src/wp-includes/query.php b/src/wp-includes/query.php
index 592e70e0290a3..637a6eb1e8409 100644
--- a/src/wp-includes/query.php
+++ b/src/wp-includes/query.php
@@ -680,6 +680,26 @@ function is_favicon() {
return $wp_query->is_favicon();
}
+/**
+ * Is the query for the AT Protocol DID document?
+ *
+ * @since 7.1.0
+ *
+ * @global WP_Query $wp_query WordPress Query object.
+ *
+ * @return bool Whether the query is for the AT Protocol DID document.
+ */
+function is_atproto_did() {
+ global $wp_query;
+
+ if ( ! isset( $wp_query ) ) {
+ _doing_it_wrong( __FUNCTION__, __( 'Conditional query tags do not work before the query is run. Before then, they always return false.' ), '3.1.0' );
+ return false;
+ }
+
+ return $wp_query->is_atproto_did();
+}
+
/**
* Determines whether the query is for a search.
*
diff --git a/src/wp-includes/template-loader.php b/src/wp-includes/template-loader.php
index b3183590398b7..29c5e86918746 100644
--- a/src/wp-includes/template-loader.php
+++ b/src/wp-includes/template-loader.php
@@ -54,6 +54,14 @@
*/
do_action( 'do_favicon' );
return;
+} elseif ( is_atproto_did() ) {
+ /**
+ * Fired when the template loader determines an AT Protocol DID request.
+ *
+ * @since 7.1.0
+ */
+ do_action( 'do_atproto_did' );
+ return;
} elseif ( is_feed() ) {
do_feed();
return;
diff --git a/src/wp-settings.php b/src/wp-settings.php
index b2736bddadc3c..a65be3af6513a 100644
--- a/src/wp-settings.php
+++ b/src/wp-settings.php
@@ -115,6 +115,7 @@
require ABSPATH . WPINC . '/formatting.php';
require ABSPATH . WPINC . '/meta.php';
require ABSPATH . WPINC . '/functions.php';
+require ABSPATH . WPINC . '/atproto.php';
require ABSPATH . WPINC . '/class-wp-meta-query.php';
require ABSPATH . WPINC . '/class-wp-matchesmapregex.php';
require ABSPATH . WPINC . '/class-wp.php';
diff --git a/tests/phpunit/tests/atproto.php b/tests/phpunit/tests/atproto.php
new file mode 100644
index 0000000000000..826ec75956c89
--- /dev/null
+++ b/tests/phpunit/tests/atproto.php
@@ -0,0 +1,72 @@
+assertSame( $expected, wp_is_atproto_did( $did ) );
+ }
+
+ public function data_wp_is_atproto_did() {
+ return array(
+ 'did:plc' => array( 'did:plc:ewvi7nxzyoun6zhxrhs64oiz', true ),
+ 'did:web domain' => array( 'did:web:example.com', true ),
+ 'did:web localhost' => array( 'did:web:localhost%3A3000', true ),
+ 'empty' => array( '', false ),
+ 'invalid method' => array( 'did:key:z6MkiTBzTbWb4TLEt', false ),
+ 'invalid plc length' => array( 'did:plc:abc', false ),
+ 'did:web path' => array( 'did:web:example.com%3Ausers%3Aalice', false ),
+ 'did:web uppercase' => array( 'did:web:Example.com', false ),
+ 'newline' => array( "did:web:example.com\ninvalid", false ),
+ 'non-string' => array( 123, false ),
+ );
+ }
+
+ /**
+ * @covers ::get_atproto_did
+ */
+ public function test_get_atproto_did_returns_option_value() {
+ update_option( 'atproto_did', 'did:web:example.com' );
+
+ $this->assertSame( 'did:web:example.com', get_atproto_did() );
+ }
+
+ /**
+ * @covers ::get_atproto_did
+ */
+ public function test_get_atproto_did_applies_filter() {
+ add_filter(
+ 'atproto_did',
+ static function () {
+ return 'did:plc:ewvi7nxzyoun6zhxrhs64oiz';
+ }
+ );
+
+ $this->assertSame( 'did:plc:ewvi7nxzyoun6zhxrhs64oiz', get_atproto_did() );
+ }
+
+ /**
+ * @covers ::get_atproto_did
+ */
+ public function test_get_atproto_did_returns_empty_string_for_invalid_did() {
+ add_option( 'atproto_did', 'invalid' );
+
+ $this->assertSame( '', get_atproto_did() );
+ }
+}
diff --git a/tests/phpunit/tests/canonical/robots.php b/tests/phpunit/tests/canonical/robots.php
index f8034a8a10022..0af2b91deb54f 100644
--- a/tests/phpunit/tests/canonical/robots.php
+++ b/tests/phpunit/tests/canonical/robots.php
@@ -12,4 +12,10 @@ public function test_remove_trailing_slashes_for_robots_requests() {
$this->assertCanonical( '/robots.txt', '/robots.txt' );
$this->assertCanonical( '/robots.txt/', '/robots.txt' );
}
+
+ public function test_remove_trailing_slashes_for_atproto_did_requests() {
+ $this->set_permalink_structure( '/%postname%/' );
+ $this->assertCanonical( '/.well-known/atproto-did', '/.well-known/atproto-did' );
+ $this->assertCanonical( '/.well-known/atproto-did/', '/.well-known/atproto-did' );
+ }
}
diff --git a/tests/phpunit/tests/option/sanitizeOption.php b/tests/phpunit/tests/option/sanitizeOption.php
index 68597b8acdaea..3d04697f14b01 100644
--- a/tests/phpunit/tests/option/sanitizeOption.php
+++ b/tests/phpunit/tests/option/sanitizeOption.php
@@ -40,6 +40,9 @@ public function data_sanitize_option() {
array( 'blog_public', 1, null ),
array( 'blog_public', 1, '1' ),
array( 'blog_public', -2, '-2' ),
+ array( 'atproto_did', 'did:web:example.com', ' did:web:example.com ' ),
+ array( 'atproto_did', '', '' ),
+ array( 'atproto_did', get_option( 'atproto_did' ), 'invalid' ),
array( 'date_format', 'F j, Y', 'F j, Y' ),
array( 'date_format', 'F j, Y', 'F j, Y' ),
array( 'ping_sites', 'http://rpc.pingomatic.com/', 'http://rpc.pingomatic.com/' ),
diff --git a/tests/phpunit/tests/query/conditionals.php b/tests/phpunit/tests/query/conditionals.php
index 4b473178897ae..d3d6d1b527bf3 100644
--- a/tests/phpunit/tests/query/conditionals.php
+++ b/tests/phpunit/tests/query/conditionals.php
@@ -77,6 +77,11 @@ public function test_404() {
$this->assertQueryTrue( 'is_404' );
}
+ public function test_atproto_did() {
+ $this->go_to( '/.well-known/atproto-did' );
+ $this->assertQueryTrue( 'is_atproto_did' );
+ }
+
public function test_permalink() {
$post_id = self::factory()->post->create( array( 'post_title' => 'hello-world' ) );
$this->go_to( get_permalink( $post_id ) );
diff --git a/tests/phpunit/tests/query/vars.php b/tests/phpunit/tests/query/vars.php
index 87e7fa7bb75c6..2247ced1f19d0 100644
--- a/tests/phpunit/tests/query/vars.php
+++ b/tests/phpunit/tests/query/vars.php
@@ -62,6 +62,7 @@ public function testPublicQueryVarsAreAsExpected() {
'preview',
'robots',
'favicon',
+ 'atproto_did',
'taxonomy',
'term',
'cpage',
diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php
index e8f90b53f20f1..57782bc96e48b 100644
--- a/tests/phpunit/tests/rest-api/rest-settings-controller.php
+++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php
@@ -103,6 +103,7 @@ public function test_get_items() {
$expected = array(
'title',
'description',
+ 'atproto_did',
'timezone',
'date_format',
'time_format',
diff --git a/tests/phpunit/tests/rewrite.php b/tests/phpunit/tests/rewrite.php
index 2bb7254abfcef..b0d2d4f7f17f7 100644
--- a/tests/phpunit/tests/rewrite.php
+++ b/tests/phpunit/tests/rewrite.php
@@ -95,6 +95,16 @@ public function test_add_rule_top() {
$this->assertStringContainsString( $redirect, $extra_rules_top[ $pattern ] );
}
+ public function test_atproto_did_rewrite_rule() {
+ global $wp_rewrite;
+
+ $wp_rewrite->flush_rules();
+
+ $rewrite_rules = $wp_rewrite->rewrite_rules();
+
+ $this->assertSame( 'index.php?atproto_did=1', $rewrite_rules['\.well-known/atproto-did$'] );
+ }
+
public function test_url_to_postid() {
$id = self::factory()->post->create();