Canonical links

Replace twitter short URLs (t.co) by real URLs. Enable HTTPS for other shorterners. Block referers. Improve privacy.

As of 14/05/2015. See the latest version.

// ==UserScript==
// @name        Canonical links
// @author      Guillaume
// @description Replace twitter short URLs (t.co) by real URLs. Enable HTTPS for other shorterners. Block referers. Improve privacy. 
// @namespace   https://greasyfork.org/fr/users/11386-guillaume
// @include     https://twitter.com/*
// @version     1.3
// @grant       none
// @run-at      document-start
// @license     CC by-sa http://creativecommons.org/licenses/by-sa/3.0/
// ==/UserScript==

// Credits:
//
// - getVisibleText, textContentVisible:
//   This is derived from a work by Ethan Brown.
//   Original license: cc-by sa
//   Source: http://stackoverflow.com/questions/19985306/get-the-innertext-of-an-element-but-exclude-hidden-children

function getVisibleText( node )
{
  if ( node.nodeType === 1 ) // Element
  {
    var rect = node.getBoundingClientRect();
    if ( rect.width === 0 || rect.height === 0 )
      return '';
  }
  else if ( node.nodeType === 3 ) // Text
  {
    return node.textContent;
  }
  var text = '';
  for( var i = 0; i < node.childNodes.length; i++ ) 
    text += getVisibleText( node.childNodes[i] );
  return text;
}

if ( ! Node.prototype.hasOwnProperty('textContentVisible') )
{
  Object.defineProperty(Node.prototype, 'textContentVisible', {
      get: function() { 
         return getVisibleText( this );
      }, 
      enumerable: true
  });
}

var hostRules = {};
hostRules['youtu.be'] = function(pathn) { return 'https://www.youtube.com/watch?v=' + pathn.substr(1); };

var hostsWithHttps = {
  'bit.ly': true, 'bc.vc': true,
  'db.tt': true,
  'goo.gl': true, 'grnpc.org': true,
  'is.gd': true, 'j.mp': true,
  'lnkd.in': true, 't.co': true,
  'tinyurl.com': true,
  'v.gd': true, 'vur.me': true,
  'x.co': true
};

var makeLinkCanonical = function(el)
{
  var i = 0;
  var limit = 3;
  while ( hostRules[el.hostname] && i < limit )
  {
    var urlFactory = hostRules[el.hostname];
    el.href = urlFactory(el.pathname, el.search, el.hash);
  }
  
  // normalize host
  el.host = el.host;
}

var makeLinkPrivate = function(el)
{  
  // use https  
  if ( el.protocol === 'http:' && hostsWithHttps[el.host] )
  {
    el.protocol = 'https:';
  }
  
  // disable referers
  el.rel = 'noreferrer';
}

var makeLinkExpanded = function(el)
{
  var text = document.createTextNode(el.textContentVisible);
  var url = el.dataset.expandedUrl;
  
  var newLink = document.createElement('a');
  newLink.href = url;
  newLink.title = el.title;
  newLink.className = el.className;
  newLink.dir = el.dir;
  newLink.appendChild(text);
  
  el.parentNode.replaceChild(newLink, el);
};

var onLink = function(el)
{
  makeLinkCanonical(el);
  makeLinkPrivate(el);
}

var onTwitterLink = function(el)
{
  makeLinkExpanded(el);
}

window.addEventListener("load", function(ev) {
  // unshadow for better debugging
  delete console.log;
  
  // initial pass
  [].slice.call(document.querySelectorAll('a[data-expanded-url]')).forEach(onTwitterLink);
  [].slice.call(document.querySelectorAll('a[href]')).forEach(onLink);
  
  // watch changes
  var observer = new MutationObserver(function(mutations)
  {
    mutations.forEach(function(mutation)
    {
      for ( var i = 0; i < mutation.addedNodes.length; i++ )
      {
        var node = mutation.addedNodes[i];
        if ( node.querySelectorAll )
        {
          [].slice.call(node.querySelectorAll('a[data-expanded-url]')).forEach(onTwitterLink);
          [].slice.call(node.querySelectorAll('a[href]')).forEach(onLink);
        }
      }
    });    
  });

  var config = { childList: true, subtree: true };
  observer.observe(document, config);
  
}, true);