Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/wp-admin/options-general.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@
<p class="description" id="tagline-description"><?php echo $tagline_description; ?></p></td>
</tr>

<tr>
<th scope="row"><label for="atproto_did"><?php _e( 'AT Protocol DID' ); ?></label></th>
<td><input name="atproto_did" type="text" id="atproto_did" aria-describedby="atproto-did-description" value="<?php form_option( 'atproto_did' ); ?>" class="regular-text code" />
<p class="description" id="atproto-did-description">
<?php
printf(
/* translators: %s: .well-known URL path. */
__( 'Used to verify this domain as an AT Protocol handle. WordPress publishes it at %s.' ),
'<code>/.well-known/atproto-did</code>'
);
?>
</p></td>
</tr>

<?php if ( current_user_can( 'upload_files' ) ) : ?>
<tr class="hide-if-no-js site-icon-section">
<th scope="row"><?php _e( 'Site Icon' ); ?></th>
Expand Down
1 change: 1 addition & 0 deletions src/wp-admin/options.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
'general' => array(
'blogname',
'blogdescription',
'atproto_did',
'site_icon',
'gmt_offset',
'date_format',
Expand Down
83 changes: 83 additions & 0 deletions src/wp-includes/atproto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php
/**
* AT Protocol functions.
*
* @package WordPress
* @subpackage ATProto
*/

/**
* Determines whether a value is a valid AT Protocol DID.
*
* @since 7.1.0
*
* @param mixed $did The value to check.
* @return bool Whether the value is a valid AT Protocol DID.
*/
function wp_is_atproto_did( $did ) {
if ( ! is_string( $did ) ) {
return false;
}

$did = trim( $did );

if ( '' === $did || str_contains( $did, "\n" ) || str_contains( $did, "\r" ) ) {
return false;
}

if ( preg_match( '/^did:plc:[a-z2-7]{24}$/', $did ) ) {
return true;
}

if ( preg_match( '/^did:web:localhost(?:%3[Aa][0-9]+)?$/', $did ) ) {
return true;
}

return (bool) preg_match( '/^did:web:(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/', $did );
}

/**
* Retrieves the AT Protocol DID for this site.
*
* @since 7.1.0
*
* @return string The AT Protocol DID, or an empty string if one is not configured.
*/
function get_atproto_did() {
$did = get_option( 'atproto_did', '' );

/**
* Filters the AT Protocol DID for this site.
*
* Returning an empty string disables the AT Protocol DID endpoint.
*
* @since 7.1.0
*
* @param string $did The configured AT Protocol DID.
*/
$did = apply_filters( 'atproto_did', $did );

if ( ! wp_is_atproto_did( $did ) ) {
return '';
}

return trim( $did );
}

/**
* Displays the AT Protocol DID document.
*
* @since 7.1.0
*/
function do_atproto_did() {
$did = get_atproto_did();

if ( '' === $did ) {
status_header( 404 );
nocache_headers();
return;
}

header( 'Content-Type: text/plain; charset=utf-8' );
echo $did;
}
3 changes: 2 additions & 1 deletion src/wp-includes/canonical.php
Original file line number Diff line number Diff line change
Expand Up @@ -705,8 +705,9 @@ function redirect_canonical( $requested_url = null, $do_redirect = true ) {
$redirect['path'] = trailingslashit( $redirect['path'] );
}

// Remove trailing slash for robots.txt or sitemap requests.
// Remove trailing slash for robots.txt, sitemap, or AT Protocol DID requests.
if ( is_robots()
|| is_atproto_did()
|| ! empty( get_query_var( 'sitemap' ) ) || ! empty( get_query_var( 'sitemap-stylesheet' ) )
) {
$redirect['path'] = untrailingslashit( $redirect['path'] );
Expand Down
24 changes: 23 additions & 1 deletion src/wp-includes/class-wp-query.php
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,14 @@ class WP_Query {
*/
public $is_favicon = false;

/**
* Signifies whether the current query is for the AT Protocol DID document.
*
* @since 7.1.0
* @var bool
*/
public $is_atproto_did = false;

/**
* Signifies whether the current query is for the page_for_posts page.
*
Expand Down Expand Up @@ -519,6 +527,7 @@ private function init_query_flags() {
$this->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;
}
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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.
*
Expand Down
7 changes: 5 additions & 2 deletions src/wp-includes/class-wp-rewrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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 );
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/wp-includes/class-wp.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
7 changes: 7 additions & 0 deletions src/wp-includes/formatting.php
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
12 changes: 12 additions & 0 deletions src/wp-includes/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
20 changes: 20 additions & 0 deletions src/wp-includes/query.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
8 changes: 8 additions & 0 deletions src/wp-includes/template-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/wp-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
72 changes: 72 additions & 0 deletions tests/phpunit/tests/atproto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php
/**
* Tests for AT Protocol functions.
*
* @package WordPress
*
* @group atproto
*/
class Tests_ATProto extends WP_UnitTestCase {

public function tear_down() {
delete_option( 'atproto_did' );
remove_all_filters( 'atproto_did' );
parent::tear_down();
}

/**
* @dataProvider data_wp_is_atproto_did
*
* @covers ::wp_is_atproto_did
*/
public function test_wp_is_atproto_did( $did, $expected ) {
$this->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() );
}
}
Loading
Loading