It’s #FrontendFriday – Xing Premium Feature per Lesezeichen

Es ist wieder soweit: heute ist #FrontendFriday! :)

Im heutigen Blogpost geht es um die Umsetzung von berechtigungsabhängigen Features am Beispiel von Xing.

Sehr viele Kollegen/innen sind – so wie ich auch – bei Xing angemeldet. Die meisten meiner Xing Kontakte haben nur einen kostenlosen Xing Basis Account, wie ich selbst auch.
Das wohl reizvollste Feature einer Premium-Mitgliedschaft ist vermutlich „Besucher deines Profils sehen„.

Was ich auch sehr gut nachvollziehen kann, denn auch ich finde dieses Feature durchaus sehr interessant, schon allein deshalb, weil es stets auf der Profilseite – mit unkenntlich gemachten Profilbesuchern – angeteasert wird:

Da die Profilfotos meiner Profilbesucher ja bereits im Frontend zu sehen sind (wenn auch verschwommen), habe ich mich gefragt:
Gibt es evtl. eine Möglichkeit, dieses Feature auch nur mit einer Basis-Mitgliedschaft zu nutzen?

Die technischen Hürden

Mit Hilfe der Chrome-Devtools habe ich mich also auf die Suche nach einer Möglichkeit gemacht, die verschwommenen Bilder wieder scharf zu bekommen.
Das geht natürlich nur, wenn diese erst im Frontend unkenntlich gemacht werden und nicht schon verschwommen vom Backend geliefert werden.
Nach kurzer Zeit bin ich tatsächlich fündig geworden: im folgenden Quellcode ist zusehen, dass der svgBlurFilter auf die Bilder im Frontend angewandt wird.

Wird dieses HTML-Attribut geändert oder entfernt, sind die Profilbilder normal zu sehen.
Die nächste Hürde: den Profilnamen herausfinden. Da die Profilbilder eindeutig den Benutzern zugeordnet werden müssen, könnte es evtl. sein, dass die UserId im Dateinamen enthalten ist, welche zwangsläufig dann auch zum Namen des Benutzers führen muss.
Wenn man sich den Dateinamen genau ansieht, ist folgendes Muster zu erkennen:
[ein komischer Hashwert].[eine Zahlenfolge],[eine Ziffer].[breite]x[höhe].jpg

Um zu prüfen, welcher der Werte die UserId ist und das Mapping UserId → Name zu erhalten, habe ich etwas gegoogelt und bin auf folgenden Kommentar gestoßen:
https://blog.emeidi.com/2007/06/19/xing-und-die-profilbilder/comment-page-1/#comment-6689

Dem Kommentar ist zu entnehmen, dass der Endpoint https://www.xing.com/events/widgets/organized/ als Exploit nutzbar ist, um den zugehörigen Namen zu erhalten.
Das Ergebnis ist, dass die Zahlenfolge nach dem „komischen Hashwert“ die UserId ist. Hier prüfbar mit meiner UserId: https://www.xing.com/events/widgets/organized/16231807
Auf der Seite „Events“ kann man dann den Namen aus dem DOM extrahieren.

Update: Xing hat leider den Endpoint deaktiviert, so dass man nicht mehr den Namen aus der UserId erhalten kann.

Im Grunde hatte ich nun alles, was man für das Premium-Feature „Besucher deines Profils sehen“ benötigt: die Profilbilder und die Namen.
Diesen manuellen Prozess musste ich nun nur noch in JavaScript-Code gießen, was im Detail – mit ein paar weiteren Optimierungen – wie folgt aussieht:

/*
* Brought to you by Webfrontend-AG @ // :)
* Author: mwingler@doubleSlash.de
* Notice: there are two "versions" of the XING-Frontend - this code handles both of them
*/

// Calculate correct image size for "full view"
var newPicSize = (document.getElementById('profile-xingid-container').clientWidth - 40) / 5 - 5;

// Remove Premium ad banner
var premiumBanner = document.querySelectorAll('[class*="src-index-upsellContent-"]');
if (premiumBanner && premiumBanner[0] && premiumBanner[0].parentElement) {
premiumBanner[0].parentElement.remove();
}

// Get all blurred images, resize and remove filter
var allImages = document.querySelectorAll('.foundation-imageonview-changed');
if (allImages.length < 1) { allImages = []; document.querySelectorAll('[class*="malt-social-proof-SocialProof-userItem-"]').forEach((el) => {
allImages.push(el.firstChild.firstChild);
});
}

allImages.forEach((img) => {
img.setAttribute('width', newPicSize);
img.setAttribute('height', newPicSize);
img.setAttribute('filter', '');

var currSrc = img.getAttribute('xlink:href');
var isNewFrontend = false;
if (currSrc) {
img.setAttribute('xlink:href', currSrc.split('128x128.jpg').join('256x256.jpg'));
isNewFrontend = true;
} else {
currSrc = img.getAttribute('src');
img.setAttribute('src', currSrc.split('128x128.jpg').join('256x256.jpg'));
}

// Extract UserId
var userIdArray = currSrc.toString().split(',')[0].split('.');
var userId = userIdArray[userIdArray.length - 1];

/*
* Use the public /events endpoint as exploit to get the username and insert into DOM
* See: https://blog.emeidi.com/2007/06/19/xing-und-die-profilbilder/comment-page-1/#comment-6689
*/
var exploitUrl = 'https://www.xing.com/events/widgets/organized/' + userId;
var xhr = new XMLHttpRequest;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
var theName = xhr.response.split('')[0];
var nameStyle = 'width: ' + newPicSize + 'px;background-color:#fff;z-index:999;cursor:pointer;';
var nameSearchUrl = 'https://www.xing.com/search/members?keywords=' + theName;
var adjacentHTML = '' + theName + '';

if (isNewFrontend) {
img.parentNode.parentNode.insertAdjacentHTML('afterend', adjacentHTML);
} else {
img.parentNode.insertAdjacentHTML('afterend', adjacentHTML);
}

img.parentElement.setAttribute('href', nameSearchUrl);
img.parentElement.setAttribute('target', '_blank');
}
}

xhr.open('GET', exploitUrl, true);
xhr.send('');

// Set position and size (both XING versions)
img.parentElement.parentElement.parentElement.parentElement.style = 'padding-bottom: 25px;';
img.setAttribute('width', newPicSize);
img.setAttribute('height', newPicSize);
img.setAttribute('sizes', newPicSize + 'px');

if (img.parentElement && img.parentElement.parentElement) {
img.parentElement.style = 'width:' + newPicSize + 'px;height:' + newPicSize + 'px;';
img.style = 'width:' + newPicSize + 'px;height:' + newPicSize + 'px;';
img.parentElement.parentElement.setAttribute('width', newPicSize);
img.parentElement.parentElement.setAttribute('height', newPicSize);
var parentSvg = img.parentElement.parentElement;
}

if (parentSvg) {
parentSvg.parentElement.style = 'width:' + newPicSize + 'px;height:' + newPicSize + 'px;margin-right:5px;';
}
});

Das Ergebnis

Um den Code nutzen zu können, müsst ihr lediglich ein neues Lesezeichen anlegen und meinen minifizierten JS-Code unter folgendem Link in das Feld URL/Adresse einfügen und speichern (Chrome / Firefox):
https://pastebin.com/EmcEdtr4

Im Chrome sieht das ganze dann z.B. so aus:

Wenn ihr nun euer eigenes Profil in Xing aufruft (nicht die Startseite), müsst ihr nur noch auf das eben neu angelegte Lesezeichen klicken – et voilà:

Hinweis: alle Protagonisten haben einer Veröffentlichung zugestimmt. Danke! :)

Fazit

Xing hat das berechtigungsabhängige Feature „Besucher deines Profils sehen“ im Frontend gelöst. Dadurch war es mir nur mit Basis-Mitgliedschaft möglich, dieses über JavaScript „nachzubauen“.

Bei der Frage, ob bestimmte Bestandteile von Features (z.B. Bild verschwommen machen) im Frontend oder Backend umgesetzt werden sollen, steht meist der Entwicklungsaufwand (Kosten) gegenüber dem Nutzen.
Gerade bei visuellen Funktionen, wie der Manipulation der Profilbilder ist eine Umsetzung im Frontend meist weniger aufwendig und somit günstiger, als eine Umsetzung im Backend.

Damit die Profilbilder auch keine UserId und somit keine Namen enthalten, wäre vermutlich eine größere Anpassung im Backend notwendig gewesen. Auch hier waren vermutlich die Entwicklungskosten gegenüber dem Nutzen (keine Möglichkeit via JavaScript UserId zu entlocken und Namen zu erhalten) schlichtweg höher.

Da das Feature nicht sicherheitskritisch ist, ist die Wahl der Umsetzung im Frontend für mich völlig nachvollziehbar.
XING hat aus meiner Sicht also nichts falsch gemacht, sondern bei der technischen Implementierung des Features einfach nur auf ein optimales Kosten-Nutzen-Verhältnis geachtet.

 

Zurück zur Übersicht

Ein Kommentar zur “It’s #FrontendFriday – Xing Premium Feature per Lesezeichen

  1. Update: Xing hat wohl leider den Endpoint deaktiviert, so dass man über …/organized/ nicht mehr den Namen extrahieren kann.

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

*Pflichtfelder

*