BDSMLR - clickable links to original high-res images

Modifies images to link to their original ("-og") version. Works for (a) the dashboard, (b) blogs displayed on right sidebar in the dashboard, (c) blog streams (xxx.bdsmlr.com) and (d) individual posts (xxx.bdsmlr.com/post/yyyyyyyyyy). It does NOT work for the archive view.

Version au 15/02/2019. Voir la dernière version.

// ==UserScript==
// @name         BDSMLR - clickable links to original high-res images
// @namespace    bdsmlr_linkify
// @version      1.8.0
// @license      GNU AGPLv3
// @description  Modifies images to link to their original ("-og") version. Works for (a) the dashboard, (b) blogs displayed on right sidebar in the dashboard, (c) blog streams (xxx.bdsmlr.com) and (d) individual posts (xxx.bdsmlr.com/post/yyyyyyyyyy). It does NOT work for the archive view.
// @author       marp
// @homepageURL  https://greasyfork.org/en/users/204542-marp
// @include      https://bdsmlr.com/
// @include      https://bdsmlr.com/dashboard
// @include      https://*.bdsmlr.com/
// @include      https://*.bdsmlr.com/post/*
// @include      https://bdsmlr.com/uploads/photos/*
// @include      https://bdsmlr.com/uploads/pictures/*
// @include      https://*.bdsmlr.com/uploads/photos/*
// @include      https://*.bdsmlr.com/uploads/pictures/*
// @include      https://bdsmlr.com//uploads/*
// @run-at document-end
// ==/UserScript==

function createImageLinks(myDoc, myContext) {

//console.info("createImageLinks: ", myContext);
  
  if (myDoc===null) myDoc = myContext;
  if (myDoc===null) return;
  if (myContext===null) myContext = myDoc;
  
  var matches;
  var tmpstr;
  var singlematch;
  var origpostlink;
  var origbloglink;
  var origblog;
  var imagematches;
  var imageurl;

  matches = myDoc.evaluate("./descendant-or-self::div[contains(@class,'postholder')] | ./descendant-or-self::div[contains(@class,'post_content')]",
                           myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  for(var i=0, el; (i<matches.snapshotLength); i++) {
    el = matches.snapshotItem(i);
    if (el) {
      try {

        // try to find info about original poster (if this is a reblog) as well as the link to the individual (potentially reblogged) post
        // both info only seem to be present on dashboard and on rightside overlay blogs - but not always on individual blogs (xxx.bdsmlr.com) or on individual blog post URLs :-(
			  singlematch = myDoc.evaluate(".//div[contains(@class,'originalposter')]/a[contains(@href,'.bdsmlr.com/post/')]",
                                     el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        origpostlink = singlematch.singleNodeValue; // xxxx.bdsmlr.com/post/yyyyyyyy
        if (origpostlink) {
          origblog = origpostlink.getAttribute("href"); //everything after and including "/post" gets truncated away later anyway
        } 
        else {
          //second method might find the originial blog URL (xxxx.bdsmlr.com)
          singlematch = myDoc.evaluate(".//div[contains(@class,'post_info')]//i[contains(@class,'retweet') or contains(@class,'rbthis')]" + 
                                       "/following-sibling::a[contains(@class,'adata') or contains(@class,'ndata')]",
                                       el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
          origbloglink = singlematch.singleNodeValue; // xxxx.bdsmlr.com
          if (origbloglink) {
          	origblog = origbloglink.getAttribute("href");
          }
          else {
            // if neither of the two above find anything then this is likely NOT a reblogged post but the original post -> get the orginial blog post URL
            singlematch = myDoc.evaluate(".//a[(contains(@class,'adata') or contains(@class,'ndata')) and contains(@href,'.bdsmlr.com/post/')]",
                                         el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
            origpostlink = singlematch.singleNodeValue; // xxxx.bdsmlr.com
            if (origpostlink) {
          	  origblog = origpostlink.getAttribute("href");
            }
            else if ( !window.location.href.startsWith("https://bdsmlr.com") ) {
              // if no link to neither original blog nor original blog post was found then we assume that this is the original blog post or blog itself (this is a rather shaky assumtion - fingers crossed...)
              origblog = window.location.href;
            }
            else {
              // however - if the current url is the dashboard then we're out of luck
              origblog = null;
            }  
          }
        }

				// iterate over all links to images (i.e. does NOT (yet) create links to images where none exist in the first place)
        // skip over items that already have a link to a "non-cdn" bdsmlr url
        imagematches = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" + 
                                             "//a[(@href='') or ((contains(@class,'magnify') or contains(@class,'image-link')) and contains(@href,'https://cdn') and contains(@href,'.bdsmlr.com'))]" +
                                               "/img",
                                          el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
        for(var j=0, image, imagelink; (j<imagematches.snapshotLength); j++) {
          image=imagematches.snapshotItem(j);
          if (image) {
            imagelink = image.parentNode;
            imageurl = imagelink.getAttribute("href");
            if (imageurl === null || imageurl.length < 5) {
              imageurl = image.getAttribute("src");
              // No idea why this is needed... DevTools inspector always shows a valid image src attribute... but at script exercution time... apparently not... seems to be some bdsmlr JavaScript post-processing...
              if (imageurl === null || imageurl.length < 5) {
                imageurl = image.getAttribute("data-echo"); 
              }  
            }
            if (imageurl && imageurl.length > 5) {
              if (origblog && origblog.length > 5) {
                // if we have info about original poster -> construct link to "-og" version of image on orig posters blog (e.g. https://<origposter>.bdsmlr.com/<....>/imagename-og.jpg)
                tmpstr = getOriginalPosterImageURL(imageurl, origblog);
                tmpstr = getOriginalImageURL(tmpstr);
              } else {
                // if no original poster info - construct link to "-og" version of image on the current blog (e.g. works for non-retweets on individual blogs)
                tmpstr = getOriginalImageURL(imageurl);
              }
              // get the link node
              image.parentNode.setAttribute("href", tmpstr);
            }
          }
        }
          
      } catch (e) { console.warn("error: ", e); }
    }
	}
}


function getOriginalPosterImageURL(imageurl, originalposter) {
  if (originalposter === null) {
    return imageurl;
  }
  var pos = imageurl.toLowerCase().indexOf(".bdsmlr.com");
  var pos2 = originalposter.toLowerCase().indexOf(".bdsmlr.com");
  if (pos > 0 && pos2 > 0) {
    return originalposter.substring(0, pos2) + imageurl.substring(pos);
  } else {
    return imageurl;
  }
}


function getOriginalImageURL(imageurl) {
  if (imageurl === null) {
    return imageurl;
  }
  var pos = imageurl.lastIndexOf(".");
  var pos2 = imageurl.lastIndexOf("-og.");
  if (pos > 0 && (pos2+3)!=pos) {
    return imageurl.substring(0, pos) + "-og" + imageurl.substring(pos);
  } else {
    return imageurl;
  }
}



// Two very different actions depending on if this is on twitter.com or twing.com
if (window.location.href.includes('bdsmlr.com/uploads/')) {
  
  if (document.head.textContent && 
      ( document.head.textContent.toLowerCase().includes('404 not found') ||
        document.head.textContent.toLowerCase().includes('403 forbidden') ) ) {
    var tmpstr = window.location.href;
    var pos = tmpstr.lastIndexOf(".");
    var pos2 = tmpstr.lastIndexOf("-og.");
    var pos3 = tmpstr.indexOf(".");

    if (pos == pos2+3) {
      // old mechanism - -og to be found only using server/blog name of the original poster
      // seems this didn't work -> now try to find -og on the cdn servers
      if (tmpstr.startsWith("https://bdsmlr.com")) {
        window.location.assign("https://cdn03.bdsmlr" + tmpstr.substring(pos3));
      }
      else if (!tmpstr.startsWith("https://cdn")) {
        window.location.assign("https://" + tmpstr.substring(pos3+1));
      }
      else if (tmpstr.startsWith("https://cdn03")) {
        window.location.assign("https://cdn02" + tmpstr.substring(pos3));
      }
      else {
        // seems there's no -og version to be found -> revert to non-"-og" version on cdn03.bdsmlr.com
        window.location.assign("https://cdn03" + tmpstr.substring(pos3,pos2) + tmpstr.substring(pos));
      }
    }
    else if (tmpstr.startsWith("https://cdn03")) {
      // look for non-"og" version on cdn02
      window.location.assign("https://cdn02" + tmpstr.substring(pos3));
    }
  }
  
}
// fix bug in bdsmlr redirection url 
else if (window.location.href.includes('bdsmlr.com//uploads/')) {
 
  var tmpstr = window.location.href;
  var pos = tmpstr.lastIndexOf("//");
  window.location.assign(tmpstr.substring(0,pos) + tmpstr.substring(pos+1));
  
}
else
{
  
  // create an observer instance and iterate through each individual new node
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      mutation.addedNodes.forEach(function(addedNode) {
        createImageLinks(mutation.target.ownerDocument, addedNode);
      });
    });    
  });

  // configuration of the observer
  // NOTE: subtree is false as the wanted nodes are direct children of <div class="newsfeed"> -> notable performance improvement
  // "theme1" is the class used by the feed root node for individual user's blog (xxxx.bdsmlr.com) -> seems unstable/temporary name -> might be changed by bdsmlr
  var config = { attributes: false, childList: true, characterData: false, subtree: false };
  // pass in the target node (<div> element contains all stream posts), as well as the observer options
  var postsmatch = document.evaluate(".//div[contains(@class,'newsfeed')] | .//div[contains(@class,'theme1')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  var postsnode = postsmatch.singleNodeValue;

  //process already loaded nodes (the initial posts before scrolling down for the first time)
  createImageLinks(document, postsnode);

  //start the observer for new nodes
  observer.observe(postsnode, config);


  // also observe the right sidebar blog stream on the dashboard
  // pass in the target node, as well as the observer options
  var sidepostsmatch = document.evaluate(".//div[@id='rightposts']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  var sidepostsnode = sidepostsmatch.singleNodeValue;
  // sidebar does only exist on dashboard
  if (sidepostsnode) {
    //start the observer for overlays
    observer.observe(sidepostsnode, config);
  }

}