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();