Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IndieAuth Client Discovery using MF2 and Bump Version #260

Merged
merged 5 commits into from
Dec 22, 2023
Merged
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
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