Skip to content
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
179 changes: 166 additions & 13 deletions inc/sso/class-admin-bar-magic-links.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ class Admin_Bar_Magic_Links {

use \WP_Ultimo\Traits\Singleton;

/**
* Admin-post action used to lazily resolve a dashboard link.
*
* @since 2.0.0
* @var string
*/
const ADMIN_POST_ACTION = 'wu_admin_bar_magic_link';

/**
* Query argument containing the requested site ID.
*
* @since 2.0.0
* @var string
*/
const SITE_ID_QUERY_ARG = 'wu_site_id';

/**
* Initialize hooks.
*
Expand All @@ -33,6 +49,9 @@ public function init(): void {
// Hook late to modify the URLs after WordPress core adds them.
add_action('admin_bar_menu', array($this, 'modify_my_sites_menu'), 999);

// Resolve dashboard URLs only after the user selects a site.
add_action('admin_post_' . self::ADMIN_POST_ACTION, array($this, 'handle_admin_bar_magic_link'));

// Hook early into admin_page_access_denied to show magic links.
add_action('admin_page_access_denied', array($this, 'show_access_denied_with_magic_links'), 5);
}
Expand All @@ -42,7 +61,8 @@ public function init(): void {
*
* This function hooks into the admin bar after WordPress core has
* added all the My Sites menu items, and replaces dashboard URLs
* with magic links for sites that have custom domains.
* with same-origin lazy redirect URLs. Magic links are generated only
* after the user selects a dashboard link.
*
* @since 2.0.0
*
Expand All @@ -56,27 +76,160 @@ public function modify_my_sites_menu($wp_admin_bar): void {
return;
}

// Process each node.
// Process each dashboard node without resolving its destination.
foreach ($wp_admin_bar->get_nodes() as $node) {
$parts = explode('-', $node->id);
if (count($parts) >= 3 && 'blog' === $parts[0] && is_numeric($parts[1]) && 'd' === $parts[2]) {
$site_id = (int) $parts[1];
} else {
if ( ! preg_match('/^blog-(\d+)-d$/', $node->id, $matches)) {
continue;
}

// Generate magic link.
$magic_link = wu_get_admin_url($site_id);
$site_id = (int) $matches[1];

if ( ! $magic_link ) {
continue;
// Keep the click on this authenticated origin until it is validated.
$node->href = $this->get_admin_bar_action_url($site_id);

$wp_admin_bar->add_node($node);
}
}

/**
* Build the same-origin action URL for a site's dashboard link.
*
* @since 2.0.0
*
* @param int $site_id Site ID.
* @return string
*/
public function get_admin_bar_action_url($site_id) {

$site_id = absint($site_id);

return add_query_arg(
[
'action' => self::ADMIN_POST_ACTION,
self::SITE_ID_QUERY_ARG => $site_id,
'_wpnonce' => wp_create_nonce($this->get_admin_bar_nonce_action($site_id)),
],
admin_url('admin-post.php')
);
}

/**
* Resolve a selected dashboard link and redirect to its validated destination.
*
* @since 2.0.0
* @return void
*/
public function handle_admin_bar_magic_link(): void {

if ( ! is_user_logged_in() ) {
wp_die(esc_html__('You do not have permission to access this site.', 'ultimate-multisite'), 403);
}

$site_id = $this->get_requested_site_id();
$nonce = $this->get_requested_nonce();

if ( ! $site_id || ! $nonce || ! wp_verify_nonce($nonce, $this->get_admin_bar_nonce_action($site_id)) ) {
wp_die(esc_html__('The requested site link is invalid.', 'ultimate-multisite'), 403);
}

$destination = $this->get_site_dashboard_url($site_id);

if ( ! $destination ) {
wp_die(esc_html__('You do not have permission to access this site.', 'ultimate-multisite'), 403);
}

$destination_host = wp_parse_url($destination, PHP_URL_HOST);

if ( ! is_string($destination_host) || '' === $destination_host ) {
wp_die(esc_html__('The requested site link is invalid.', 'ultimate-multisite'), 403);
}

$allow_destination_host = static function ($allowed_hosts, $host) use ($destination_host) {
if (0 === strcasecmp($destination_host, $host)) {
$allowed_hosts[] = $destination_host;
}

// Update the node with the magic link.
$node->href = $magic_link;
return $allowed_hosts;
};

$wp_admin_bar->add_node($node);
add_filter('allowed_redirect_hosts', $allow_destination_host, 100, 2);
$redirected = wp_safe_redirect($destination, 302, 'Ultimate-Multisite');
remove_filter('allowed_redirect_hosts', $allow_destination_host, 100);

if ( ! $redirected ) {
wp_die(esc_html__('The requested site link is invalid.', 'ultimate-multisite'), 403);
}

exit;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Return the verified dashboard destination for a site.
*
* @since 2.0.0
*
* @param int $site_id Site ID.
* @return false|string
*/
public function get_site_dashboard_url($site_id) {

$site_id = absint($site_id);
$site = get_site($site_id);

if (
! $site instanceof \WP_Site
|| $site->deleted
|| $site->spam
|| $site->archived
|| (! is_super_admin() && ! is_user_member_of_blog(get_current_user_id(), $site_id))
) {
return false;
}

return wu_get_admin_url($site_id);
}

/**
* Get the nonce action for a site's dashboard link.
*
* @since 2.0.0
*
* @param int $site_id Site ID.
* @return string
*/
protected function get_admin_bar_nonce_action($site_id) {

return self::ADMIN_POST_ACTION . '_' . absint($site_id);
}

/**
* Get a validated site ID from the request.
*
* @since 2.0.0
* @return false|int
*/
protected function get_requested_site_id() {

$site_id = wu_request(self::SITE_ID_QUERY_ARG);

if ( ! is_string($site_id) || ! ctype_digit($site_id) || ! absint($site_id) ) {
return false;
}

return absint($site_id);
}

/**
* Get a nonce string from the request.
*
* @since 2.0.0
* @return false|string
*/
protected function get_requested_nonce() {

$nonce = wu_request('_wpnonce');

return is_string($nonce) ? $nonce : false;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion inc/sso/class-magic-link.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ protected function verify_user_site_access($user_id, $site_id) {
return false;
}

if (is_user_member_of_blog($user_id, $site_id)) {
if (is_super_admin($user_id) || is_user_member_of_blog($user_id, $site_id)) {
return true;
}
// Check if the site is the dashboard site in WP Frontend Admin which the user would not be a member of.
Expand Down
Loading
Loading