From 53b4cd7d729af094cea911cdec299ff0b792409a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski-Owczarek?=
Date: Wed, 26 Mar 2025 14:25:24 +0100
Subject: [PATCH 1/2] Tabs: Support URL-based credentials
When credentials are provided directly in the URL, e.g.:
https://username:password@www.example.com/
`location.href` strips out the auth part, but anchor links contain them, making
our `isLocal` computation broken. This fixes it by only looking at `origin`,
`pathname` & `search`.
Fixes gh-2213
Closes gh-2345
---
tests/unit/tabs/core.js | 26 ++++++++++++++++++++++++++
ui/widgets/tabs.js | 33 +++++++++++++--------------------
2 files changed, 39 insertions(+), 20 deletions(-)
diff --git a/tests/unit/tabs/core.js b/tests/unit/tabs/core.js
index c2fd890488..f7515f5850 100644
--- a/tests/unit/tabs/core.js
+++ b/tests/unit/tabs/core.js
@@ -747,4 +747,30 @@ QUnit.test( "extra listeners created when tabs are added/removed (trac-15136)",
"No extra listeners after removing all the extra tabs" );
} );
+QUnit.test( "URL-based auth with local tabs (gh-2213)", function( assert ) {
+ assert.expect( 1 );
+
+ var origAjax = $.ajax,
+ element = $( "#tabs1" ),
+ anchor = element.find( "a[href='#fragment-3']" ),
+ url = new URL( anchor.prop( "href" ) );
+
+ try {
+ $.ajax = function() {
+ throw new Error( "Unexpected AJAX call; all tabs are local!" );
+ };
+
+ anchor.attr( "href", url.protocol + "//username:password@" + url.host +
+ url.pathname + url.search + url.hash );
+
+ element.tabs();
+ anchor.trigger( "click" );
+
+ assert.strictEqual( element.tabs( "option", "active" ), 2,
+ "should set the active option" );
+ } finally {
+ $.ajax = origAjax;
+ }
+} );
+
} );
diff --git a/ui/widgets/tabs.js b/ui/widgets/tabs.js
index 49468feb39..0a8efd3ca3 100644
--- a/ui/widgets/tabs.js
+++ b/ui/widgets/tabs.js
@@ -61,26 +61,19 @@ $.widget( "ui.tabs", {
load: null
},
- _isLocal: ( function() {
- var rhash = /#.*$/;
-
- return function( anchor ) {
- var anchorUrl, locationUrl;
-
- anchorUrl = anchor.href.replace( rhash, "" );
- locationUrl = location.href.replace( rhash, "" );
-
- // Decoding may throw an error if the URL isn't UTF-8 (#9518)
- try {
- anchorUrl = decodeURIComponent( anchorUrl );
- } catch ( _error ) {}
- try {
- locationUrl = decodeURIComponent( locationUrl );
- } catch ( _error ) {}
-
- return anchor.hash.length > 1 && anchorUrl === locationUrl;
- };
- } )(),
+ _isLocal: function( anchor ) {
+ var anchorUrl = new URL( anchor.href ),
+ locationUrl = new URL( location.href );
+
+ return anchor.hash.length > 1 &&
+
+ // `href` may contain a hash but also username & password;
+ // we want to ignore them, so we check the three fields
+ // below instead.
+ anchorUrl.origin === locationUrl.origin &&
+ anchorUrl.pathname === locationUrl.pathname &&
+ anchorUrl.search === locationUrl.search;
+ },
_create: function() {
var that = this,
From 8864e40cee6ec6676b94427f05ac483da26fc138 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski-Owczarek?=
Date: Wed, 26 Mar 2025 14:36:58 +0100
Subject: [PATCH 2/2] Tabs: Properly handle decoded/encoded anchor hashes &
panel IDs
Prior to jQuery UI 1.14.1, hashes in anchor hrefs were used directly. In
gh-2307, that was changed - by decoding - to support more complex IDs, e.g.
containing emojis which are automatically encoded in `anchor.hash`.
Unfortunately, that broke cases where the panel ID is decoded as well.
It turns out the spec mandates checking both. In the "scrolling to a fragment"
section of the HTML spec[^1]. That uses a concept of document's indicated
part[^2]. Slightly below there's an algorithm to compute the indicated part[^3].
The interesting parts are steps 4 to 9:
4. Let potentialIndicatedElement be the result of finding a potential
indicated element given document and fragment.
5. If potentialIndicatedElement is not null, then return
potentialIndicatedElement.
6. Let fragmentBytes be the result of percent-decoding fragment.
7. Let decodedFragment be the result of running UTF-8 decode without BOM on
fragmentBytes.
8. Set potentialIndicatedElement to the result of finding a potential indicated
element given document and decodedFragment.
9. If potentialIndicatedElement is not null, then return
potentialIndicatedElement.
First, in steps 4-5, the algorithm tries the hash as-is, without decoding. Then,
if one is not found, the same is attempted with a decoded hash.
This change replicates this logic by first trying the hash as-is and then
decoding it.
Fixes gh-2344
Closes gh-2345
Ref gh-2307
[^1]: https://html.spec.whatwg.org/#scrolling-to-a-fragment
[^2]: https://html.spec.whatwg.org/#the-indicated-part-of-the-document
[^3]: https://html.spec.whatwg.org/#select-the-indicated-part
---
tests/unit/tabs/core.js | 51 +++++++++++++++++++++++++++++++++++++++
tests/unit/tabs/tabs.html | 29 ++++++++++++++++++++++
ui/widgets/tabs.js | 36 ++++++++++++++++++++++++---
3 files changed, 112 insertions(+), 4 deletions(-)
diff --git a/tests/unit/tabs/core.js b/tests/unit/tabs/core.js
index f7515f5850..1eac3c2683 100644
--- a/tests/unit/tabs/core.js
+++ b/tests/unit/tabs/core.js
@@ -773,4 +773,55 @@ QUnit.test( "URL-based auth with local tabs (gh-2213)", function( assert ) {
}
} );
+( function() {
+ function getVerifyTab( assert, element ) {
+ return function verifyTab( index ) {
+ assert.strictEqual(
+ element.tabs( "option", "active" ),
+ index,
+ "should set the active option to " + index );
+ assert.strictEqual(
+ element.find( "[role='tabpanel']:visible" ).text().trim(),
+ "Tab " + ( index + 1 ),
+ "should set the panel to 'Tab " + ( index + 1 ) + "'" );
+ };
+ }
+
+ QUnit.test( "href encoding/decoding (gh-2344)", function( assert ) {
+ assert.expect( 12 );
+
+ location.hash = "#tabs-2";
+
+ var i,
+ element = $( "#tabs10" ).tabs(),
+ tabLinks = element.find( "> ul a" ),
+ verifyTab = getVerifyTab( assert, element );
+
+ for ( i = 0; i < tabLinks.length; i++ ) {
+ tabLinks.eq( i ).trigger( "click" );
+ verifyTab( i );
+ }
+
+ location.hash = "";
+ } );
+
+ QUnit.test( "href encoding/decoding on init (gh-2344)", function( assert ) {
+ assert.expect( 12 );
+
+ var i,
+ element = $( "#tabs10" ),
+ tabLinks = element.find( "> ul a" ),
+ verifyTab = getVerifyTab( assert, element );
+
+ for ( i = 0; i < tabLinks.length; i++ ) {
+ location.hash = tabLinks.eq( i ).attr( "href" );
+ element.tabs();
+ verifyTab( i );
+ element.tabs( "destroy" );
+ }
+
+ location.hash = "";
+ } );
+} )();
+
} );
diff --git a/tests/unit/tabs/tabs.html b/tests/unit/tabs/tabs.html
index cb4e5389f6..3f18fa015f 100644
--- a/tests/unit/tabs/tabs.html
+++ b/tests/unit/tabs/tabs.html
@@ -125,6 +125,35 @@
+
+