Skip to content

Commit

Permalink
Merge pull request #260 from dshanske/21dec23fixes
Browse files Browse the repository at this point in the history
Add IndieAuth Client Discovery using MF2 and Bump Version
  • Loading branch information
dshanske authored Dec 22, 2023
2 parents 47ce29f + e42bb8a commit b9d63a9
Show file tree
Hide file tree
Showing 11 changed files with 3,305 additions and 165 deletions.
10 changes: 8 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"phpcompatibility/phpcompatibility-wp": "*",
"sebastian/phpcpd": "^3.0 || ^4.0 || ^6.0",
"yoast/phpunit-polyfills": "^2.0"
"yoast/phpunit-polyfills": "^2.0",
"mf2/mf2": "^0.5.0"
},
"scripts": {
"install-codestandards": [
"Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run"
],
"post-install-cmd": [
"@install-codestandard"
"@install-codestandard",
"@copy-files"
],
"setup-local-tests": "bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 latest",
"phpunit": "./vendor/bin/phpunit",
Expand All @@ -49,6 +51,10 @@
"bin/install-wp-tests.sh wordpress wordpress wordpress",
"vendor/bin/phpunit"
],
"copy-files": [
"cp -u -r vendor/mf2/mf2/Mf2/Parser.php lib/mf2",
"cp -u -r vendor/mf2/mf2/*.md lib/mf2"
],
"lint": [
"./vendor/bin/phpcs -n -p",
"@phpcpd"
Expand Down
216 changes: 113 additions & 103 deletions includes/class-indieauth-client-discovery.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<?php

class IndieAuth_Client_Discovery {
protected $html = array();
protected $rels = array();
protected $manifest = array();
protected $html = array();
protected $mf2 = array();
public $client_id = '';
public $client_name = '';
public $client_icon = '';
Expand All @@ -13,37 +15,39 @@ public function __construct( $client_id ) {
if ( defined( 'INDIEAUTH_UNIT_TESTS' ) ) {
return;
}

$this->html = self::parse( $client_id );
if ( is_wp_error( $this->html ) ) {
error_log( __( 'Failed to Retrieve IndieAuth Client Details ', 'indieauth' ) . wp_json_encode( $this->html ) ); // phpcs:ignore
return;
}
if ( isset( $this->html['manifest'] ) ) {
$this->manifest = self::get_manifest( $this->html['manifest'] );
}
$this->client_icon = $this->determine_icon();
$this->client_name = $this->ifset( $this->manifest, 'name', '' );
if ( empty( $this->client_name ) ) {
$this->client_name = $this->ifset( $this->html, array( 'application-name', 'og:title', 'title' ), '' );
}
}

private function fetch( $url ) {

// Validate if this is an IP address
$ip = filter_var( wp_parse_url( $url, PHP_URL_HOST ), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 );
$ip = filter_var( wp_parse_url( $client_id, PHP_URL_HOST ), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 );
$donotfetch = array(
'127.0.0.1',
'0000:0000:0000:0000:0000:0000:0000:0001',
'::1',
);

// If this is an IP address ion the donotfetch list then do not fetch.
if ( $ip && ! in_array( $ip, $donotfetch ) ) {
return new WP_Error( 'do_not_fetch', __( 'Client Identifier is localhost', 'indieauth' ) );
// If this is an IP address on the donotfetch list then do not fetch.
if ( ( $ip && ! in_array( $ip, $donotfetch, true ) || 'localhost' === wp_parse_url( $client_id, PHP_URL_HOST ) ) ) {
return;
}

$response = self::parse( $client_id );
if ( is_wp_error( $response ) ) {
error_log( __( 'Failed to Retrieve IndieAuth Client Details ', 'indieauth' ) . wp_json_encode( $response ) ); // phpcs:ignore
return;
}
}

public function export() {
return array(
'manifest' => $this->manifest,
'rels' => $this->rels,
'mf2' => $this->mf2,
'html' => $this->html,
'client_id' => $this->client_id,
'client_name' => $this->client_name,
'client_icon' => $this->client_icon,
);
}

private function fetch( $url ) {
$wp_version = get_bloginfo( 'version' );
$user_agent = apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ) );
$args = array(
Expand All @@ -59,33 +63,94 @@ private function fetch( $url ) {
return new WP_Error( 'retrieval_error', __( 'Failed to Retrieve Client Details', 'indieauth' ), $code );
}
}

return $response;
}

private function parse( $url ) {
$response = self::fetch( $url );

if ( is_wp_error( $response ) ) {
return $response;
}

$return = array();
// check link header
$links = wp_remote_retrieve_header( $response, 'link' );
if ( $links ) {
if ( is_string( $links ) ) {
$links = array( $links );
$content = wp_remote_retrieve_body( $response );

if ( class_exists( 'Masterminds\\HTML5' ) ) {
$domdocument = new \Masterminds\HTML5( array( 'disable_html_ns' => true ) );
$domdocument = $domdocument->loadHTML( $content );
} else {
$domdocument = new DOMDocument();
libxml_use_internal_errors( true );
if ( function_exists( 'mb_convert_encoding' ) ) {
$content = mb_convert_encoding( $content, 'HTML-ENTITIES', mb_detect_encoding( $content ) );
}
$domdocument->loadHTML( $content );
libxml_use_internal_errors( false );
}

$this->get_mf2( $domdocument, $url );
if ( ! empty( $this->mf2 ) ) {
if ( array_key_exists( 'name', $this->mf2 ) ) {
$this->client_name = $this->mf2['name'][0];
}
if ( array_key_exists( 'logo', $this->mf2 ) ) {
if ( is_string( $this->mf2['logo'][0] ) ) {
$this->client_icon = $this->mf2['logo'][0];
} else {
$this->client_icon = $this->mf2['logo'][0]['value'];
}
}
} elseif ( isset( $this->rels['manifest'] ) ) {
self::get_manifest( $this->rels['manifest'] );
$this->client_icon = $this->determine_icon( $this->manifest );
$this->client_name = $this->manifest['name'];
} else {
$this->client_icon = $this->determine_icon( $this->rels );
$this->get_html( $domdocument );
$this->client_name = $this->html['title'];
}

if ( ! empty( $this->client_icon ) ) {
$this->client_icon = WP_Http::make_absolute_url( $this->client_icon, $url );
}
}

private function get_mf2( $input, $url ) {
if ( ! class_exists( 'Mf2\Parser' ) ) {
require_once plugin_dir_path( __DIR__ ) . 'lib/mf2/Parser.php';
}
$mf = Mf2\parse( $input, $url );
if ( array_key_exists( 'rels', $mf ) ) {
$this->rels = wp_array_slice_assoc( $mf['rels'], array( 'apple-touch-icon', 'icon', 'mask-icon', 'manifest' ) );
}
if ( array_key_exists( 'items', $mf ) ) {
foreach ( $mf['items'] as $item ) {
if ( in_array( 'h-app', $item['type'], true ) ) {
$this->mf2 = $item['properties'];
return;
}
}
$return['links'] = parse_link_rels( $links, $url );
}
return array_merge( $return, self::extract_client_data_from_html( wp_remote_retrieve_body( $response ), $url ) );
}

private function get_manifest( $url ) {
if ( is_array( $url ) ) {
$url = $url[0];
}
$response = self::fetch( $url );
if ( is_wp_error( $response ) ) {
return $response;
}
return json_decode( wp_remote_retrieve_body( $response ) );
$this->manifest = json_decode( wp_remote_retrieve_body( $response ), true );
}

private function get_html( $input ) {
if ( ! $input ) {
return;
}
$xpath = new DOMXPath( $input );
$this->html['title'] = $xpath->query( '//title' )->item( 0 )->textContent;
}

private function ifset( $array, $key, $default = false ) {
Expand All @@ -108,26 +173,30 @@ public function get_name() {
}

// Separate function for possible improved size picking later
private function determine_icon() {
if ( is_wp_error( $this->html ) ) {
private function determine_icon( $input ) {
if ( ! is_array( $input ) || empty( $input ) ) {
return '';
}

$icons = array();
if ( is_array( $this->manifest ) && ! empty( $this->manifest ) && ! isset( $this->manifest['icons'] ) ) {
$icons = $this->manifest['icons'];
} elseif ( ! empty( $this->html ) ) {
if ( isset( $this->html['icon'] ) ) {
$icons = $this->html['icon'];
} elseif ( isset( $this->html['mask-icon'] ) ) {
$icons = $this->html['mask-icon'];
} elseif ( isset( $this->html['apple-touch-icon'] ) ) {
$icons = $this->html['apple-touch-icon'];
}
if ( isset( $input['icons'] ) ) {
$icons = $input['icons'];
} elseif ( isset( $input['mask-icon'] ) ) {
$icons = $input['mask-icon'];
} elseif ( isset( $input['apple-touch-icon'] ) ) {
$icons = $input['apple-touch-icon'];
} elseif ( isset( $input['icon'] ) ) {
$icons = $input['icon'];
}

if ( is_array( $icons ) && ! wp_is_numeric_array( $icons ) && isset( $icons['url'] ) ) {
return $icons['url'];
} elseif ( is_string( $icons[0] ) ) {
return $icons[0];
} elseif ( isset( $icons[0]['url'] ) ) {
return $icons[0]['url'];
} elseif ( isset( $icons[0]['src'] ) ) {
return $icons[0]['src'];
} else {
return '';
}
Expand All @@ -136,63 +205,4 @@ private function determine_icon() {
public function get_icon() {
return $this->client_icon;
}

/**
* @param array $contents HTML to parse for rel links
* @param string $url URL to use to make absolute
* @return array $rels rel values as indices to properties, empty array if no rels at all
*/
public static function extract_client_data_from_html( $contents, $url ) {
// unicode to HTML entities
$contents = mb_convert_encoding( $contents, 'HTML-ENTITIES', mb_detect_encoding( $contents ) );
libxml_use_internal_errors( true );
$doc = new DOMDocument();
$doc->loadHTML( $contents );
$xpath = new DOMXPath( $doc );
$return = array();
// check <link> and <a> elements
foreach ( $xpath->query( '//a[@rel and @href] | //link[@rel and @href]' ) as $hyperlink ) {
$rel = $hyperlink->getAttribute( 'rel' );
$temp = array();
// Try to extract icons just in case there isn't a manifest
switch ( $rel ) {
case 'icon':
case 'mask-icon':
case 'shortcut icon':
case 'apple-touch-icon-precomposed':
case 'apple-touch-icon':
$temp['url'] = WP_Http::make_absolute_url( $hyperlink->getAttribute( 'href' ), $url );
$temp['sizes'] = $hyperlink->getAttribute( 'sizes' );
$temp['type'] = $hyperlink->getAttribute( 'temp' );
$temp = array_filter( $temp );
break;
default:
$temp = WP_Http::make_absolute_url( $hyperlink->getAttribute( 'href' ), $url );
}
if ( 'shortcut icon' === $rel ) {
$rel = 'icon';
}
if ( isset( $return[ $rel ] ) ) {
if ( is_array( $return[ $rel ] ) ) {
$return[ $rel ] = $temp;
}
if ( is_string( $return[ $rel ] ) ) {
$return[ $rel ] = array( $return[ $rel ] );
$return[ $rel ][] = $temp;
}
} else {
$return[ $rel ] = $temp;
}
}
// As a fallback also retrieve OpenGraph Title and Image Properties
foreach ( $xpath->query( '//meta[@property and @content]' ) as $meta ) {
$property = $meta->getAttribute( 'property' );
if ( in_array( $property, array( 'og:title', 'og:image' ), true ) ) {
$return[ $property ] = $meta->getAttribute( 'content' );
}
}
$return['title'] = $xpath->query( '//title' )->item( 0 )->textContent;

return $return;
}
}
33 changes: 32 additions & 1 deletion includes/class-indieauth-token-ui.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public function __construct() {
add_action( 'admin_init', array( $this, 'admin_init' ) );
add_action( 'admin_menu', array( $this, 'admin_menu' ), 11 );
add_action( 'admin_action_indieauth_newtoken', array( $this, 'new_token' ) );
add_action( 'admin_action_indieauth_client_discovery', array( $this, 'client_discovery' ) );
}

/**
Expand Down Expand Up @@ -41,6 +42,25 @@ public function admin_menu() {
public function options_callback() {
}

public function client_discovery() {
if ( ! isset( $_POST['indieauth_nonce'] )
|| ! wp_verify_nonce( $_POST['indieauth_nonce'], 'indieauth_client_discovery' )
) {
esc_html_e( 'Invalid Nonce', 'indieauth' );
exit;
}
if ( empty( $_REQUEST['client_url'] ) ) {
$GLOBALS['title'] = esc_html__( 'Client Discovery', 'indieauth' ); // phpcs:ignore
esc_html_e( 'A URL must be provided', 'indieauth' );
exit;
}
header( 'Content-Type: application/json' );
$client_url = sanitize_text_field( $_REQUEST['client_url'] );
$client = new IndieAuth_Client_Discovery( $client_url );
echo wp_json_encode( $client->export(), JSON_PRETTY_PRINT );
exit;
}

public function new_token() {
if ( ! isset( $_POST['indieauth_nonce'] )
|| ! wp_verify_nonce( $_POST['indieauth_nonce'], 'indieauth_newtoken' )
Expand Down Expand Up @@ -131,7 +151,18 @@ public function options_form() {
<p><button class="button-primary"><?php esc_html_e( 'Add New Token', 'indieauth' ); ?></button></p>
</form>
</div>
<?php
<?php if ( WP_DEBUG ) { ?>
<div>
<h3><?php esc_html_e( 'Client Discovery Tester', 'indieauth' ); ?></h3>
<form method="post" action="admin.php">
<label for="client_url"><?php esc_html_e( 'Discovery Test', 'indieauth' ); ?></label><input type="url" class="widefat" id="client_url" name="client_url" />
<?php wp_nonce_field( 'indieauth_client_discovery', 'indieauth_nonce' ); ?>
<input type="hidden" name="action" id="action" value="indieauth_client_discovery" />
<p><button class="button-primary"><?php esc_html_e( 'Client Discovery', 'indieauth' ); ?></button></p>
</form>
</div>
<?php
}
}

public function scopes() {
Expand Down
2 changes: 1 addition & 1 deletion indieauth.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: IndieAuth
* Plugin URI: https://github.com/indieweb/wordpress-indieauth/
* Description: IndieAuth is a way to allow users to use their own domain to sign into other websites and services
* Version: 4.4.0
* Version: 4.4.1
* Author: IndieWebCamp WordPress Outreach Club
* Author URI: https://indieweb.org/WordPress_Outreach_Club
* License: MIT
Expand Down
Loading

0 comments on commit b9d63a9

Please sign in to comment.