@@ -22,6 +22,22 @@ class Admin_Bar_Magic_Links {
2222
2323 use \WP_Ultimo \Traits \Singleton;
2424
25+ /**
26+ * Admin-post action used to lazily resolve a dashboard link.
27+ *
28+ * @since 2.0.0
29+ * @var string
30+ */
31+ const ADMIN_POST_ACTION = 'wu_admin_bar_magic_link ' ;
32+
33+ /**
34+ * Query argument containing the requested site ID.
35+ *
36+ * @since 2.0.0
37+ * @var string
38+ */
39+ const SITE_ID_QUERY_ARG = 'wu_site_id ' ;
40+
2541 /**
2642 * Initialize hooks.
2743 *
@@ -33,6 +49,9 @@ public function init(): void {
3349 // Hook late to modify the URLs after WordPress core adds them.
3450 add_action ('admin_bar_menu ' , array ($ this , 'modify_my_sites_menu ' ), 999 );
3551
52+ // Resolve dashboard URLs only after the user selects a site.
53+ add_action ('admin_post_ ' . self ::ADMIN_POST_ACTION , array ($ this , 'handle_admin_bar_magic_link ' ));
54+
3655 // Hook early into admin_page_access_denied to show magic links.
3756 add_action ('admin_page_access_denied ' , array ($ this , 'show_access_denied_with_magic_links ' ), 5 );
3857 }
@@ -42,7 +61,8 @@ public function init(): void {
4261 *
4362 * This function hooks into the admin bar after WordPress core has
4463 * added all the My Sites menu items, and replaces dashboard URLs
45- * with magic links for sites that have custom domains.
64+ * with same-origin lazy redirect URLs. Magic links are generated only
65+ * after the user selects a dashboard link.
4666 *
4767 * @since 2.0.0
4868 *
@@ -56,27 +76,160 @@ public function modify_my_sites_menu($wp_admin_bar): void {
5676 return ;
5777 }
5878
59- // Process each node.
79+ // Process each dashboard node without resolving its destination .
6080 foreach ($ wp_admin_bar ->get_nodes () as $ node ) {
61- $ parts = explode ('- ' , $ node ->id );
62- if (count ($ parts ) >= 3 && 'blog ' === $ parts [0 ] && is_numeric ($ parts [1 ]) && 'd ' === $ parts [2 ]) {
63- $ site_id = (int ) $ parts [1 ];
64- } else {
81+ if ( ! preg_match ('/^blog-(\d+)-d$/ ' , $ node ->id , $ matches )) {
6582 continue ;
6683 }
6784
68- // Generate magic link.
69- $ magic_link = wu_get_admin_url ($ site_id );
85+ $ site_id = (int ) $ matches [1 ];
7086
71- if ( ! $ magic_link ) {
72- continue ;
87+ // Keep the click on this authenticated origin until it is validated.
88+ $ node ->href = $ this ->get_admin_bar_action_url ($ site_id );
89+
90+ $ wp_admin_bar ->add_node ($ node );
91+ }
92+ }
93+
94+ /**
95+ * Build the same-origin action URL for a site's dashboard link.
96+ *
97+ * @since 2.0.0
98+ *
99+ * @param int $site_id Site ID.
100+ * @return string
101+ */
102+ public function get_admin_bar_action_url ($ site_id ) {
103+
104+ $ site_id = absint ($ site_id );
105+
106+ return add_query_arg (
107+ [
108+ 'action ' => self ::ADMIN_POST_ACTION ,
109+ self ::SITE_ID_QUERY_ARG => $ site_id ,
110+ '_wpnonce ' => wp_create_nonce ($ this ->get_admin_bar_nonce_action ($ site_id )),
111+ ],
112+ admin_url ('admin-post.php ' )
113+ );
114+ }
115+
116+ /**
117+ * Resolve a selected dashboard link and redirect to its validated destination.
118+ *
119+ * @since 2.0.0
120+ * @return void
121+ */
122+ public function handle_admin_bar_magic_link (): void {
123+
124+ if ( ! is_user_logged_in () ) {
125+ wp_die (esc_html__ ('You do not have permission to access this site. ' , 'ultimate-multisite ' ), 403 );
126+ }
127+
128+ $ site_id = $ this ->get_requested_site_id ();
129+ $ nonce = $ this ->get_requested_nonce ();
130+
131+ if ( ! $ site_id || ! $ nonce || ! wp_verify_nonce ($ nonce , $ this ->get_admin_bar_nonce_action ($ site_id )) ) {
132+ wp_die (esc_html__ ('The requested site link is invalid. ' , 'ultimate-multisite ' ), 403 );
133+ }
134+
135+ $ destination = $ this ->get_site_dashboard_url ($ site_id );
136+
137+ if ( ! $ destination ) {
138+ wp_die (esc_html__ ('You do not have permission to access this site. ' , 'ultimate-multisite ' ), 403 );
139+ }
140+
141+ $ destination_host = wp_parse_url ($ destination , PHP_URL_HOST );
142+
143+ if ( ! is_string ($ destination_host ) || '' === $ destination_host ) {
144+ wp_die (esc_html__ ('The requested site link is invalid. ' , 'ultimate-multisite ' ), 403 );
145+ }
146+
147+ $ allow_destination_host = static function ($ allowed_hosts , $ host ) use ($ destination_host ) {
148+ if (0 === strcasecmp ($ destination_host , $ host )) {
149+ $ allowed_hosts [] = $ destination_host ;
73150 }
74151
75- // Update the node with the magic link.
76- $ node -> href = $ magic_link ;
152+ return $ allowed_hosts ;
153+ } ;
77154
78- $ wp_admin_bar ->add_node ($ node );
155+ add_filter ('allowed_redirect_hosts ' , $ allow_destination_host , 100 , 2 );
156+ $ redirected = wp_safe_redirect ($ destination , 302 , 'Ultimate-Multisite ' );
157+ remove_filter ('allowed_redirect_hosts ' , $ allow_destination_host , 100 );
158+
159+ if ( ! $ redirected ) {
160+ wp_die (esc_html__ ('The requested site link is invalid. ' , 'ultimate-multisite ' ), 403 );
161+ }
162+
163+ exit ;
164+ }
165+
166+ /**
167+ * Return the verified dashboard destination for a site.
168+ *
169+ * @since 2.0.0
170+ *
171+ * @param int $site_id Site ID.
172+ * @return false|string
173+ */
174+ public function get_site_dashboard_url ($ site_id ) {
175+
176+ $ site_id = absint ($ site_id );
177+ $ site = get_site ($ site_id );
178+
179+ if (
180+ ! $ site instanceof \WP_Site
181+ || $ site ->deleted
182+ || $ site ->spam
183+ || $ site ->archived
184+ || (! is_super_admin () && ! is_user_member_of_blog (get_current_user_id (), $ site_id ))
185+ ) {
186+ return false ;
187+ }
188+
189+ return wu_get_admin_url ($ site_id );
190+ }
191+
192+ /**
193+ * Get the nonce action for a site's dashboard link.
194+ *
195+ * @since 2.0.0
196+ *
197+ * @param int $site_id Site ID.
198+ * @return string
199+ */
200+ protected function get_admin_bar_nonce_action ($ site_id ) {
201+
202+ return self ::ADMIN_POST_ACTION . '_ ' . absint ($ site_id );
203+ }
204+
205+ /**
206+ * Get a validated site ID from the request.
207+ *
208+ * @since 2.0.0
209+ * @return false|int
210+ */
211+ protected function get_requested_site_id () {
212+
213+ $ site_id = wu_request (self ::SITE_ID_QUERY_ARG );
214+
215+ if ( ! is_string ($ site_id ) || ! ctype_digit ($ site_id ) || ! absint ($ site_id ) ) {
216+ return false ;
79217 }
218+
219+ return absint ($ site_id );
220+ }
221+
222+ /**
223+ * Get a nonce string from the request.
224+ *
225+ * @since 2.0.0
226+ * @return false|string
227+ */
228+ protected function get_requested_nonce () {
229+
230+ $ nonce = wu_request ('_wpnonce ' );
231+
232+ return is_string ($ nonce ) ? $ nonce : false ;
80233 }
81234
82235 /**
0 commit comments