0ae2a6a36d7b4c77e8e7d7df2a1049f4412e7099
max
  Fri Sep 18 07:03:44 2026 -0700
hgHubConnect: let a mirror hand out its own API keys, and stop swallowing links to another host

#Preview2 week - bugs introduced now will need a build patch to fix
Two of the three problems QA found on #38323.

The link in the mirror-only Hub Upload message did not go anywhere. The tab
handler in hgHubConnect.js catches every hgHubConnect link with a hash and turns
it into a tab switch, and 'Go to Hub Upload on genome.ucsc.edu' has a hash that
names a tab on the mirror too, so the click just reopened the tab the reader was
already on. It now only intercepts links to the page itself.

The API key section was decoupled from storeUserFiles, but only for display: the
Generate and Revoke buttons are cartJson requests, and both the javascript that
sends them and the code in main() that routes them were still inside the
storeUserFiles gate. A site with showHubApiKey on and hubSpace off therefore drew
two dead buttons. The key functions move out of hgMyData.js into a new
hubApiKey.js that the Hub Development tab includes on its own, main() routes a
cartJson request when either setting is on, and the hubSpace file commands stay
registered only when hubSpace is actually running.

The request also goes to this host's hgHubConnect now rather than to the login
host. Keys live in the central database of the server that issues them, so a key
made on genome-euro belongs in genome-euro's table.

refs #38323

diff --git src/hg/js/hgHubConnect.js src/hg/js/hgHubConnect.js
index e5ed93ecf7a..2d3e0296a9a 100644
--- src/hg/js/hgHubConnect.js
+++ src/hg/js/hgHubConnect.js
@@ -1,261 +1,266 @@
 // "use strict";
 // Don't complain about line break before '||' etc:
 /* jshint -W014 */
 /* jshint esversion: 8 */
 function makeIframe(ev) {
     /* It's unusual to show script output in an iframe. But this solution has a few advantages:
      * - We can show a "waiting" message while the data loads
      * - The user knows where the results will appear, it looks like a dialog box and covers the page
      */
     ev.stopPropagation();
     var validateText = document.getElementById('validateHubUrl');
     validateText.value=$.trim(validateText.value);
     var hubUrl = $('#validateHubUrl').val();
     if(!validateUrl(hubUrl)) { 
         alert('Invalid hub URL');
         return;
     }
 
     hgsid = document.querySelector("input[name='hgsid']").value;
     var myUrl = window.location.href.split("#")[0].split("?")[0]; // strip off hgsid and tab-name
     var waitUrl =  myUrl + '?hgsid=' + hgsid + '&hgHub_do_hubCheck=1';
     var node = document.createElement('iframe'); 
     node.setAttribute('src', waitUrl);
     node.setAttribute('width', document.documentElement.clientWidth-100+'px');
     node.setAttribute('height', document.documentElement.clientHeight-100+'px');
     node.style.position = 'absolute'; 
     node.style.top = '50px'; 
     node.style.left = '50px'; 
     node.style.border = '3px solid darkgrey'; 
     node.id = 'checkerFrame';
     // first show the loading page
     document.body.appendChild(node);
 
 
     // when the waiting page has finished loading, load the hub checker page
     var finalUrl = waitUrl + '&validateHubUrl='+encodeURIComponent(hubUrl);
     var alreadyRun = false;
     node.addEventListener("load", function() {
         if (! alreadyRun)
             node.setAttribute('src', finalUrl);
         alreadyRun = true; // because 'load' fires again when finalUrl is loaded
         this.contentWindow.focus(); // activate keyboard event handlers of the iframe
     });
     return false;
 }
 
 function closeIframe() {
     var theFrame = window.parent.document.getElementById('checkerFrame');
     theFrame.parentNode.removeChild(theFrame);
 }
 
 function reloadIframe() {
     document.getElementById("content").innerHTML = "Re-loading hub...";
     window.parent.document.getElementById('checkerFrame').src += '';
 }
 
 // hover effect to highlight table rows
 $(function() {
     $(".hubList tr").hover(
 
     function() {
         $(this).addClass("hoverRow");
     }, function() {
         $(this).removeClass("hoverRow");
     });
 });
 
 
 // initializes the tabs - with cookie option
 // cookie option requires jquery.cookie.js
 $(function() {
   // maps a URL hash to the index of the matching tab, or -1 if none matches
   function tabIndexForHash(hash) {
       if (hash === "#conn") { hash = "#unlistedHubs"; }
       if (hash === "#dev")  { hash = "#hubDeveloper"; }
       return $("#tabs > ul > li > a[href='" + hash + "']").parent().index();
   }
 
   $("#tabs").tabs({
       active: localStorage.getItem("hubTab") !== null ? localStorage.getItem("hubTab") : 0,
       activate: function(event, ui) {
           localStorage.setItem("hubTab", ui.newTab.index());
       },
   });
   // activate the tab named by the current URL hash
   var initialIndex = tabIndexForHash(window.location.hash);
   if (initialIndex >= 0)
       $("#tabs").tabs("option", "active", initialIndex);
 
   // menubar links point to hgHubConnect#<tab>. When already on this page a same-page
   // hash link only scrolls; intercept and switch tabs instead.
   $(document).on("click", "a[href*='hgHubConnect'][href*='#']", function(ev) {
+      // only same-page links are tab switches. A mirror sends people to the hgHubConnect
+      // of the login host, and that hash names a tab here too, so without this check the
+      // link is swallowed and reopens the tab the reader is already looking at.
+      if (this.host !== window.location.host)
+          return;
       var index = tabIndexForHash(this.hash);
       if (index >= 0) {
           ev.preventDefault();
           $("#tabs").tabs("option", "active", index);
           // we didn't navigate, so close the hover menu holding this link
           var $menuParent = $(this).closest("li.menuparent");
           $menuParent.hideSuperfishUl();
           $menuParent.find("> button, > a").attr("aria-expanded", "false");
       }
   });
 
   $("#tabs").tabs().on("tabsactivate", function(event, ui) {
     const  newHash = ui.newTab.find("a").attr("href");
     if (newHash) {
       history.replaceState(null, null, newHash);
     }
     if (newHash === "#hubUpload") {
         hubCreate.init();
     }
   });
 });
 
 // creates keyup event; listening for return key press
 $(document).ready(function() {
     $('#loadSampleHub').bind('click', function(e) {
         $('#validateHubUrl').val("https://genome.ucsc.edu/goldenPath/help/examples/hubDirectory/hub.txt");
 
     });
     $('#hubUrl').bind('keypress', function(e) {  // binds listener to url field
         if (e.which === 13) {  // listens for return key
              e.preventDefault();   // prevents return from also submitting whole form
              if (validateUrl($('#hubUrl').val()))
                  $('input[name="hubAddButton"]').focus().click(); // clicks AddHub button
         }
     });
     $('#validateHubUrl').bind('keypress', function(e) {  // binds listener to url field
         if (e.which === 13) {  // listens for return key
              e.preventDefault();   // prevents return from also submitting whole form
              if (validateUrl($('#validateHubUrl').val()))
                  $('input[name="hubValidateButton"]').focus().click(); // clicks Validate Url button
         }
     });
     $('#hubSearchTerms').bind('keypress', function(e) {  // binds listener to text field
         if (e.which === 13) {  // listens for return key
             e.preventDefault();   // prevents return from also submitting whole form
             $('input[name="hubSearchButton"]').focus().click(); // clicks search button
         }
     });
     $('#hubDbFilter').bind('keypress', function(e) {  // binds listener to text field
         if (e.which === 13) {  // listens for return key
             e.preventDefault();   // prevents return from also submitting whole form
             $('input[name="hubSearchButton"]').focus().click(); // clicks db filter button
         }
     });
     $('.pasteIcon').bind('click', function(e) {
         // The hgTracks link is in the <A> element two elements before the icon SVG:
         // <td>
         // <a class='hgTracksLink' href="hgTracks?hubUrl=https://hgwdev-kent.gi.ucsc.edu/~kent/t2t/hub/hub2.txt&amp;genome=hub_25068_GCA_009914755&amp;position=lastDbPos">GCA_009914755</a>
         // <input type="hidden" value="https://hgwdev-kent.gi.ucsc.edu/~kent/t2t/hub/hub2.txt">
         // <svg class="pasteIcon">...</svg>    <--- this is e.target of the click handler
         // </td>
         var inputEl = e.target.closest("svg").previousSibling;
         var connectUrl = inputEl.previousSibling.href;
 
         // the url is in the <input> element just before the SVG
         var oldVal = inputEl.value;
         // display:none does not work,
         // see https://stackoverflow.com/questions/31593297/using-execcommand-javascript-to-copy-hidden-text-to-clipboard
         inputEl.style = "position: absolute; left: -1000px; top: -1000px";
         inputEl.value = connectUrl;
         inputEl.type = 'text';
         inputEl.select();
         inputEl.setSelectionRange(0, 99999); /* For mobile devices */
         document.execCommand('copy');
 
         inputEl.type = 'hidden';
         inputEl.value = oldVal;
         alert("Copied Genome Browser hub connection URL to clipboard");
     });
 
     $('.shortPlus').bind('click', function(ev) {
         ev.target.parentElement.style.display = 'none';
         ev.target.parentElement.nextSibling.style.display = 'inline';
     });
     $('.fullMinus').bind('click', function(ev) {
         ev.target.parentElement.style.display = 'none';
         ev.target.parentElement.previousSibling.style.display = 'inline';
     });
 
 
 });
 
 var hubSearchTree = (function() {
     var treeDiv;        // Points to div we live in
 
     function hubSearchTreeContextMenuHandler (node, callback) {
         var nodeType = node.li_attr.nodetype;
         if (nodeType == 'track') {
             callback({
                 'openConfig': {
                     'label' : 'Configure this track',
                     'action' : function () {
                         window.open(node.li_attr.configlink, '_blank');
                     }
                 }
             });
         }
         else if (nodeType == 'assembly') {
             callback({
                 'openConfig': {
                     'label' : 'Open this assembly',
                     'action' : function () {
                         window.open(node.li_attr.assemblylink, '_blank');
                     }
                 }
             });
         }
     }
 
     function buildTracks(node, cb) {
         // called when jstree wants data to open a node for the tracks tree
         cb.call(this, trackData[node.id]);
     }
 
     function init(searching) {
         $.jstree.defaults.core.themes.icons = false;
         $.jstree.defaults.core.themes.dots = true;
         $.jstree.defaults.contextmenu.show_at_node = false;
         if (searching === true) {
             $.jstree.defaults.contextmenu.items = hubSearchTreeContextMenuHandler;
 
             $('div[id^="tracks"]').each(function(i, obj) {
                 treeDiv = obj;
                 var hubId = treeDiv.id.slice(6);
                 arrId = '#_' + hubId;
                 $(treeDiv).jstree({
                     'plugins' : ['contextmenu'],
                     'core' : {
                         'data': function(node, cb) {
                             if (node.id === '#') {
                                 cb([{"text" : "Search details ...", "id": arrId, "children": true}]);
                             } else {
                                 cb(trackData[""+node.id]);
                             }
                         },
                         'dbclick_toggle': false
                     }
                 });
                 $(treeDiv).on("select_node.jstree", function (e, data)  {
                     $(e.target).jstree("open_node", data.node);
                 }); // jstree
             }); // each div
         } else { // validating hub, no contextmenu and easier tree building
             treeDiv = $('#validateHubResult');
             treeDiv.jstree({
                 'core' : {
                     'data' : buildTracks,
                     'dbclick_toggle': false
                 }
             });
             treeDiv.on('select_node.jstree', function(e, data) {
                     $(e.target).jstree("open_node", data.node);
             });
         }
     } // init
     return {
         init: init
     };
 }());