Skip to content

Commit 29419ca

Browse files
authored
Merge pull request #148 from dshanske/multiuser
Fixes
2 parents a93ed0c + 94361e4 commit 29419ca

12 files changed

+357
-123
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ matrix:
2929
- php: 5.6
3030
env: WP_PLUGIN_DEPLOY=1
3131
- php: 5.5
32-
env: WP_VERSION=4.7 WP_MULTISITE=0
32+
env: WP_VERSION=4.9.9 WP_MULTISITE=0
3333
- php: 5.4
34-
env: WP_VERSION=4.7 WP_MULTISITE=0
34+
env: WP_VERSION=4.9.9 WP_MULTISITE=0
3535
before_script:
3636
- |
3737
# Remove Xdebug for a huge performance increase:

includes/class-indieauth-admin.php

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,121 @@ public function __construct() {
1111
add_action( 'init', array( $this, 'settings' ) );
1212
add_action( 'login_form_authdiag', array( $this, 'login_form_authdiag' ) );
1313
add_action( 'admin_menu', array( $this, 'admin_menu' ) );
14+
add_filter( 'wp_pre_insert_user_data', array( $this, 'unique_user_url' ), 10, 3 );
15+
add_filter( 'manage_users_columns', array( $this, 'add_user_url_column' ) );
16+
add_filter( 'manage_users_custom_column', array( $this, 'user_url_column' ), 10, 3 );
17+
add_filter( 'site_status_tests', array( $this, 'add_indieauth_test' ) );
18+
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
19+
}
20+
21+
22+
public function admin_notices() {
23+
if ( ! get_option( 'indieauth_header_check', 0 ) ) {
24+
echo '<div class="notice notice-warning"><p>';
25+
esc_html_e( 'In order to ensure IndieAuth tokens will work please visit the settings page to check:', 'indieauth' );
26+
printf( ' <a href="%1s">%2$s</a>', esc_url( menu_page_url( 'indieauth', false ) ), esc_html__( 'Visit Settings Page', 'indieauth' ) );
27+
echo '</p></div>';
28+
}
29+
$screen = get_current_screen();
30+
if ( ( 'users' === $screen->id ) && $this->check_dupe_user_urls() ) {
31+
echo '<div class="notice notice-error"><p>';
32+
esc_html_e( 'Multiple user accounts have the same URL set. This is not permitted as this value is used by IndieAuth for login. Please resolve', 'indieauth' );
33+
echo '</p></div>';
34+
}
35+
}
36+
37+
public function add_indieauth_test( $tests ) {
38+
$tests['direct']['indieauth_plugin'] = array(
39+
'label' => __( 'IndieAuth Test', 'indieauth' ),
40+
'test' => array( $this, 'site_health_test' ),
41+
);
42+
return $tests;
43+
}
44+
45+
public function site_health_test() {
46+
$result = array(
47+
'label' => __( 'Authorization Header Passed', 'indieauth' ),
48+
'status' => 'good',
49+
'badge' => array(
50+
'label' => __( 'IndieAuth', 'indieauth' ),
51+
'color' => 'green',
52+
),
53+
'description' => sprintf(
54+
'<p>%s</p>',
55+
__( 'Your hosting provider allows authorization headers to pass so IndieAuth should work', 'indieauth' )
56+
),
57+
'actions' => '',
58+
'test' => 'indieauth_headers',
59+
);
60+
61+
if ( ! self::test_auth() ) {
62+
$result['status'] = 'critical';
63+
$result['label'] = __( 'Authorization Test Failed', 'indieauth' );
64+
$result['description'] = sprintf(
65+
'<p>%s</p>',
66+
__( 'Authorization Headers are being blocked by your hosting provider. This will cause IndieAuth to fail.', 'indieauth' )
67+
);
68+
$result['actions'] = sprintf( '<a href="%1$s" >%2$s</a>', menu_page_url( 'indieauth', false ), __( 'Visit the Settings page for guidance on how to resolve.', 'indieauth' ) );
69+
}
70+
71+
return $result;
72+
}
73+
74+
public function user_url_column( $val, $column_id, $user_id ) {
75+
if ( 'user_url' === $column_id ) {
76+
$user = get_user_by( 'id', $user_id );
77+
if ( ! wp_http_validate_url( $user->user_url ) ) {
78+
return __( 'None', 'indieauth' );
79+
}
80+
$url = esc_url( $user->user_url );
81+
return sprintf( '<a href="%1s">%1$s</a>', $url );
82+
}
83+
return $val;
84+
}
85+
86+
public function add_user_url_column( $column ) {
87+
$column['user_url'] = __( 'Website', 'indieauth' );
88+
return $column;
89+
}
90+
91+
/**
92+
* Ensure all user URL fields are unique
93+
*
94+
*/
95+
public function unique_user_url( $data, $update, $id ) {
96+
if ( empty( $data['user_url'] ) ) {
97+
return $data;
98+
}
99+
$data['user_url'] = normalize_url( $data['user_url'] );
100+
$users = get_users(
101+
array(
102+
'search' => '*' . wp_parse_url( $data['user_url'], PHP_URL_HOST ) . '*',
103+
'search_columns' => array( 'user_url' ),
104+
'fields' => array( 'ID', 'user_url' ),
105+
)
106+
);
107+
108+
$url = normalize_url( $data['user_url'], true );
109+
foreach ( $users as $user ) {
110+
if ( ( normalize_url( $user->user_url, true ) === $url ) && $user->ID !== $id ) {
111+
$data['user_url'] = '';
112+
}
113+
}
114+
return $data;
115+
}
116+
117+
public function check_dupe_user_urls() {
118+
$urls = get_users(
119+
array(
120+
'fields' => array( 'user_url' ),
121+
)
122+
);
123+
$urls = array_filter( wp_list_pluck( $urls, 'user_url' ) );
124+
$urls = array_map( 'normalize_url', $urls );
125+
if ( count( array_unique( $urls ) ) === count( $urls ) ) {
126+
return false;
127+
}
128+
return true;
14129
}
15130

16131
public function login_form_authdiag() {
@@ -33,7 +148,7 @@ public function login_form_authdiag() {
33148
header( 'Content-Type: application/json' );
34149
$return = wp_json_encode( array( 'message' => $return ) );
35150
}
36-
echo $return;
151+
echo $return; // phpcs:ignore
37152
exit;
38153
}
39154
$args = array(
@@ -44,7 +159,6 @@ public function login_form_authdiag() {
44159
exit;
45160
}
46161

47-
48162
public function settings() {
49163
register_setting(
50164
'indieauth',
@@ -102,10 +216,7 @@ public function admin_menu() {
102216
add_action( 'load-' . $options_page, array( $this, 'add_help_tab' ) );
103217
}
104218

105-
/**
106-
* Load settings page
107-
*/
108-
public function settings_page() {
219+
public function test_auth() {
109220
$response = wp_remote_post(
110221
add_query_params_to_url(
111222
array(
@@ -123,15 +234,27 @@ public function settings_page() {
123234
);
124235
if ( ! is_wp_error( $response ) ) {
125236
$json = json_decode( wp_remote_retrieve_body( $response ) );
126-
set_query_var( 'authdiag_message', $json->message );
237+
return $json->message;
127238
} else {
128-
set_query_var( 'authdiag_message', 'Fail' );
239+
return false;
129240
}
241+
}
130242

243+
/**
244+
* Load settings page
245+
*/
246+
public function settings_page() {
247+
$response = self::test_auth();
248+
if ( ! $response ) {
249+
ob_start();
250+
include plugin_dir_path( __DIR__ ) . 'templates/authdiagfail.php';
251+
$response = ob_get_contents();
252+
ob_end_clean();
253+
}
254+
set_query_var( 'authdiag_message', $response );
131255
load_template( plugin_dir_path( __DIR__ ) . '/templates/indieauth-settings.php' );
132256
}
133257

134-
135258
public function add_help_tab() {
136259
get_current_screen()->add_help_tab(
137260
array(
@@ -150,15 +273,15 @@ public function add_help_tab() {
150273
'content' =>
151274
'<p>' . __( 'The IndieWeb is a people-focused alternative to the "corporate web".', 'indieauth' ) . '</p>' .
152275
'<p>
153-
<strong>' . __( 'Your content is yours', 'indieauth' ) . '</strong><br />' .
276+
<strong>' . __( 'Your content is yours', 'indieauth' ) . '</strong><br />' .
154277
__( 'When you post something on the web, it should belong to you, not a corporation. Too many companies have gone out of business and lost all of their users’ data. By joining the IndieWeb, your content stays yours and in your control.', 'indieauth' ) .
155278
'</p>' .
156279
'<p>
157-
<strong>' . __( 'You are better connected', 'indieauth' ) . '</strong><br />' .
280+
<strong>' . __( 'You are better connected', 'indieauth' ) . '</strong><br />' .
158281
__( 'Your articles and status messages can go to all services, not just one, allowing you to engage with everyone. Even replies and likes on other services can come back to your site so they’re all in one place.', 'indieauth' ) .
159282
'</p>' .
160283
'<p>
161-
<strong>' . __( 'You are in control', 'indieauth' ) . '</strong><br />' .
284+
<strong>' . __( 'You are in control', 'indieauth' ) . '</strong><br />' .
162285
__( 'You can post anything you want, in any format you want, with no one monitoring you. In addition, you share simple readable links such as example.com/ideas. These links are permanent and will always work.', 'indieauth' ) .
163286
'</p>',
164287
)

includes/class-indieauth-authorization-endpoint.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,7 @@ public function verify( $request ) {
179179
if ( array() === array_diff_assoc( $params, $token ) ) {
180180
$this->delete_code( $code, $token['user'] );
181181

182-
if ( ( class_exists( 'Indieweb_Plugin' ) && get_option( 'iw_single_author' ) ) || ! is_multi_author() ) {
183-
$return = array( 'me' => home_url( '/' ) );
184-
} else {
185-
// Return the user profile URL and scope
186-
$return = array( 'me' => get_author_posts_url( $user->ID ) );
187-
}
182+
$return = array( 'me' => get_url_from_url( $user->ID ) );
188183

189184
if ( isset( $token['scope'] ) ) {
190185
$return['scope'] = $token['scope'];

includes/class-indieauth-debug.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,6 @@ public function test( $request ) {
114114
}
115115
return indieauth_get_response();
116116
}
117-
118-
119117
}
120118

121119
new IndieAuth_Debug();

includes/class-indieauth-token-endpoint.php

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,9 @@ public static function verify_local_authorization_code( $post_args ) {
218218
unset( $return['code_challenge'] );
219219
unset( $return['code_challenge_method'] );
220220
}
221-
if ( ( class_exists( 'Indieweb_Plugin' ) && get_option( 'iw_single_author' ) ) || ! is_multi_author() ) {
222-
$return['me'] = home_url( '/' );
223-
} else {
224-
// Return the user profile URL and scope
225-
$return['me'] = get_author_posts_url( $return['user'] );
226-
}
227-
$user = get_user_by_identifier( $return['me'] );
221+
$return['me'] = get_url_from_user( $return['user'] );
222+
223+
$user = get_user_by( 'id', $return['user'] );
228224
if ( $user ) {
229225
$return['profile'] = indieauth_get_user( $user );
230226
}

includes/class-token-list-table.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ public function get_columns() {
2020

2121
public function get_bulk_actions() {
2222
return array(
23-
'revoke' => __( 'Revoke', 'indieauth' ),
23+
'revoke' => __( 'Revoke', 'indieauth' ),
2424
'revoke_year' => __( 'Revoke Tokens Last Accessed 1 Year Ago or Never', 'indieauth' ),
25-
'revoke_month' => __( 'Revoke Tokens Last Accessed 1 Month Ago or Never', 'indieauth' ),
25+
'revoke_month' => __( 'Revoke Tokens Last Accessed 1 Month Ago or Never', 'indieauth' ),
2626
'revoke_week' => __( 'Revoke Tokens Last Accessed 1 Week Ago or Never', 'indieauth' ),
27-
'revoke_day' => __( 'Revoke Tokens Last Accessed 1 Day Ago or Never', 'indieauth' ),
27+
'revoke_day' => __( 'Revoke Tokens Last Accessed 1 Day Ago or Never', 'indieauth' ),
2828
'revoke_hour' => __( 'Revoke Tokens Last Accessed 1 Hour Ago or Never', 'indieauth' ),
29-
'cleanup' => __( 'Clean Up Expired Tokens and Authorization Codes', 'indieauth' ),
29+
'cleanup' => __( 'Clean Up Expired Tokens and Authorization Codes', 'indieauth' ),
3030
);
3131
}
3232

includes/functions.php

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,22 @@ function parse_html_rels( $contents, $url ) {
144144
}
145145
}
146146

147+
/**
148+
* Uses the code from is_multi_author to determine the identity of the single author
149+
* @return false|int User ID of the single author if exists
150+
*/
151+
if ( ! function_exists( 'get_single_author' ) ) {
152+
function get_single_author() {
153+
global $wpdb;
154+
if ( false === ( $single_author = get_transient( 'single_author' ) ) ) {
155+
$rows = (array) $wpdb->get_col( "SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_type = 'post' AND post_status = 'publish' LIMIT 2" );
156+
$single_author = 1 === count( $rows ) ? (int) $rows[0] : false;
157+
set_transient( 'single_author', $single_author );
158+
}
159+
return $single_author;
160+
}
161+
}
162+
147163
/**
148164
* Get the user associated with the specified Identifier-URI.
149165
*
@@ -155,24 +171,30 @@ function get_user_by_identifier( $identifier ) {
155171
if ( empty( $identifier ) ) {
156172
return null;
157173
}
158-
// Ensure has trailing slash
159-
$identifier = trailingslashit( $identifier );
174+
175+
$identifier = normalize_url( $identifier );
160176
if ( ( 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ) ) && ( wp_parse_url( home_url(), PHP_URL_HOST ) === wp_parse_url( $identifier, PHP_URL_HOST ) ) ) {
161177
$identifier = set_url_scheme( $identifier, 'https' );
162178
}
163179
// Try to save the expense of a search query if the URL is the site URL
164180
if ( home_url( '/' ) === $identifier ) {
165181
// Use the Indieweb settings to set the default author
166-
if ( class_exists( 'Indieweb_Plugin' ) && ( get_option( 'iw_single_author' ) || ! is_multi_author() ) ) {
182+
if ( class_exists( 'Indieweb_Plugin' ) && get_option( 'iw_single_author' ) ) {
167183
return get_user_by( 'id', get_option( 'iw_default_author' ) );
168184
}
169-
$users = get_users( array( 'who' => 'authors' ) );
170-
if ( 1 === count( $users ) ) {
171-
return $users[0];
185+
$author = get_single_author();
186+
// If there is only a single author then they will get the root url
187+
if ( $author ) {
188+
return get_user_by( 'id', $author );
189+
} else {
190+
$users = get_users( array( 'fields' => 'ID' ) );
191+
if ( 1 === count( $users ) ) {
192+
return get_user_by( 'id', $users[0] );
193+
}
194+
return null;
172195
}
173-
return null;
174-
175196
}
197+
176198
// Check if this is a author post URL
177199
$user = url_to_author( $identifier );
178200
if ( $user instanceof WP_User ) {
@@ -186,13 +208,29 @@ function get_user_by_identifier( $identifier ) {
186208

187209
$users = get_users( $args );
188210
// check result
189-
if ( ! empty( $users ) ) {
211+
if ( is_countable( $users ) && 1 === count( $users ) ) {
190212
return $users[0];
191213
}
192214
return null;
193215
}
194216
}
195217

218+
219+
/**
220+
* Tries to make some decisions about what URL to return for a user
221+
*/
222+
if ( ! function_exists( 'get_url_from_user' ) ) {
223+
function get_url_from_user( $user_id ) {
224+
if ( class_exists( 'Indieweb_Plugin' ) && get_option( 'iw_single_author' ) ) {
225+
if ( get_option( 'iw_default_author' ) === $user_id ) {
226+
return home_url( '/' );
227+
}
228+
}
229+
$user = get_user_by( 'ID', $user_id );
230+
return get_author_posts_url( $user_id );
231+
}
232+
}
233+
196234
/**
197235
* Examine a url and try to determine the author ID it represents.
198236
*
@@ -354,6 +392,34 @@ function build_url( $parsed_url ) {
354392
}
355393
}
356394

395+
if ( ! function_exists( 'normalize_url' ) ) {
396+
// Adds slash if no path is in the URL, and convert hostname to lowercase
397+
function normalize_url( $url, $force_ssl = false ) {
398+
$parts = wp_parse_url( $url );
399+
if ( array_key_exists( 'path', $parts ) && '' === $parts['path'] ) {
400+
return false;
401+
}
402+
// wp_parse_url returns just "path" for naked domains
403+
if ( count( $parts ) === 1 && array_key_exists( 'path', $parts ) ) {
404+
$parts['host'] = $parts['path'];
405+
unset( $parts['path'] );
406+
}
407+
if ( ! array_key_exists( 'scheme', $parts ) ) {
408+
$parts['scheme'] = $force_ssl ? 'https' : 'http';
409+
} elseif ( $force_ssl ) {
410+
$parts['scheme'] = 'https';
411+
}
412+
if ( ! array_key_exists( 'path', $parts ) ) {
413+
$parts['path'] = '/';
414+
}
415+
// Invalid scheme
416+
if ( ! in_array( $parts['scheme'], array( 'http', 'https' ), true ) ) {
417+
return false;
418+
}
419+
return build_url( $parts );
420+
}
421+
}
422+
357423
/**
358424
* Get Scope
359425
*

0 commit comments

Comments
 (0)