diff --git a/event.js b/event.js
index eed4182..ce975a7 100644
--- a/event.js
+++ b/event.js
@@ -74,15 +74,197 @@ var shortnames = '';
var lastnameprefix = ['o', 'da', 'de', 'di', 'al', 'ul', 'el'];
var junk = false;
var headline = false;
+var genusers = false;
+var nickname = false;
var count = 0;
var run = false;
var maxrequests = 0;
var tabid = 0;
+var users = [];
+var totalResultCount = 0;
+var workerStatus = 0;
+
+// Worker functions integrated into service worker
+function userParse(jd) {
+ console.log(jd.elements.length);
+ for (var e = 0; e < jd.elements.length; e++) {
+ if (jd.elements[e].hasOwnProperty('items')) {
+ for (var x = 0; x < jd.elements[e].items.length; x++) {
+ var name = '';
+ var headline = '';
+ var subline = '';
+ var handle = '';
+
+ if (!jd.elements[e].items[x].itemUnion.hasOwnProperty('entityResult')) {
+ continue;
+ }
+
+ if (!jd.elements[e].items[x].itemUnion.entityResult.hasOwnProperty('title')) {
+ continue;
+ }
+
+ if (jd.elements[e].items[x].itemUnion.entityResult.title.hasOwnProperty('text')) {
+ name = jd.elements[e].items[x].itemUnion.entityResult.title.text;
+ }
+
+ if (name.includes('LinkedIn')) {
+ continue;
+ }
+
+ if (jd.elements[e].items[x].itemUnion.entityResult.hasOwnProperty('primarySubtitle') && jd.elements[e].items[x].itemUnion.entityResult.primarySubtitle.hasOwnProperty('text')) {
+ headline = jd.elements[e].items[x].itemUnion.entityResult.primarySubtitle.text;
+ }
+
+ if (jd.elements[e].items[x].itemUnion.entityResult.hasOwnProperty('secondarySubtitle') && jd.elements[e].items[x].itemUnion.entityResult.secondarySubtitle.hasOwnProperty('text')) {
+ subline = jd.elements[e].items[x].itemUnion.entityResult.secondarySubtitle.text;
+ }
+
+ var url = jd.elements[e].items[x].itemUnion.entityResult.navigationUrl.split('/');
+ handle = url[url.length - 1].split('?')[0];
+
+ users.push(new Array(name, headline, subline, handle));
+ }
+ }
+ }
+}
+
+async function processLinkedInData(url, csrftoken) {
+ users = [];
+ totalResultCount = 0;
+
+ var urlparts;
+ try {
+ urlparts = decodeURI(url).split('?')[1].split('&');
+ } catch (err) {
+ throw new Error('Unable to parse URL, check you are on a search results page');
+ }
+
+ var queryParameters = '';
+ var currentCompany = '';
+ var geoUrn = '';
+ var profileLanguage = '';
+ var serviceCategory = '';
+ var title = '';
+ var industry = '';
+
+ for (var i = 0; i < urlparts.length; i++) {
+ if (urlparts[i].includes('currentCompany')) {
+ currentCompany = urlparts[i].split('=')[1];
+ currentCompany = currentCompany.replace(/%2C/g, ',').replace('[', '').replace(']', '').replace(/"/g, '');
+ queryParameters += 'currentCompany:List(' + currentCompany + '),';
+ }
+
+ if (urlparts[i].includes('geoUrn')) {
+ geoUrn = urlparts[i].split('=')[1];
+ geoUrn = geoUrn.replace(/%2C/g, ',').replace('[', '').replace(']', '').replace(/"/g, '');
+ queryParameters += 'geoUrn:List(' + geoUrn + '),';
+ }
+
+ if (urlparts[i].includes('profileLanguage')) {
+ profileLanguage = urlparts[i].split('=')[1];
+ profileLanguage = profileLanguage.replace(/%2C/g, ',').replace('[', '').replace(']', '').replace(/"/g, '');
+ queryParameters += 'profileLanguage:List(' + profileLanguage + '),';
+ }
+
+ if (urlparts[i].includes('serviceCategory')) {
+ serviceCategory = urlparts[i].split('=')[1];
+ serviceCategory = serviceCategory.replace(/%2C/g, ',').replace('[', '').replace(']', '').replace(/"/g, '');
+ queryParameters += 'serviceCategory:List(' + serviceCategory + '),';
+ }
+
+ if (urlparts[i].includes('industry')) {
+ industry = urlparts[i].split('=')[1];
+ industry = industry.replace(/%2C/g, ',').replace('[', '').replace(']', '').replace(/"/g, '');
+ queryParameters += 'industry:List(' + industry + '),';
+ }
+
+ if (urlparts[i].includes('title')) {
+ title = urlparts[i].split('=')[1];
+ title = title.replace(/%2C/g, ',').replace('[', '').replace(']', '').replace(/"/g, '');
+ queryParameters += 'title:List(' + title + '),';
+ }
+ }
+
+ queryParameters += 'resultType:List(PEOPLE)';
+
+ var page = 0;
+ var timeout = 500;
+
+ while (page < totalResultCount || totalResultCount === 0) {
+ await new Promise(resolve => setTimeout(resolve, timeout));
+
+ var apiUrl =
+ 'https://www.linkedin.com/voyager/api/search/dash/clusters?decorationId=com.linkedin.voyager.dash.deco.search.SearchClusterCollection-92&origin=FACETED_SEARCH&q=all&query=(flagshipSearchIntent:SEARCH_SRP,queryParameters:(' +
+ queryParameters +
+ '),includeFiltersInResponse:false)&count=40&start=' + page * 40;
+
+ try {
+ var response = await fetch(apiUrl, {
+ headers: {
+ 'csrf-token': csrftoken,
+ 'x-restli-protocol-version': '2.0.0'
+ },
+ credentials: 'include'
+ });
+
+ if (response.status === 200) {
+ var responseText = await response.text();
+
+ if (responseText.includes("You've reached the monthly limit for profile searches.")) {
+ throw new Error('Search limit reached');
+ }
+
+ var jd = JSON.parse(responseText);
+
+ if (jd.metadata.totalResultCount == 0) {
+ throw new Error('No results found');
+ }
+
+ if (totalResultCount == 0) {
+ totalResultCount = Math.ceil(jd.metadata.totalResultCount / 40);
+ }
+ if (totalResultCount > 25) {
+ totalResultCount = 25;
+ }
+
+ userParse(jd);
+
+ // Update status
+ var message =
+ '
WeakestLink Dump Running
| Retrieved $$PAGE$$ of $$COUNT$$ page results |
Close this tab to stop dumping
';
+ message = message.replace('$$PAGE$$', page + 1);
+ message = message.replace('$$COUNT$$', totalResultCount);
+
+ chrome.scripting.executeScript({
+ target: { tabId: tabid },
+ func: function(htmlContent) {
+ document.body.innerHTML = htmlContent;
+ },
+ args: [message]
+ });
+
+ page++;
+ } else {
+ throw new Error('HTTP ' + response.status);
+ }
+ } catch (err) {
+ throw err;
+ }
+ }
+
+ return users;
+}
+
+// Handle messages from popup
+chrome.runtime.onMessage.addListener(function(request) {
+ if (request.action === 'dumpCurrentPage') {
+ dumpCurrentPage(request.url, request.tabid, request.junk, request.genusers, request.headline, request.nickname);
+ }
+});
function completed(data, finished, count, filename, tabid) {
- var blob = new Blob([data], {
- type: 'text/csv;charset=utf-8',
- });
+ // Convert data to base64 data URL for MV3 compatibility
+ var dataUrl = 'data:text/csv;charset=utf-8,' + encodeURIComponent(data);
var downloadid = 0;
@@ -95,7 +277,6 @@ function completed(data, finished, count, filename, tabid) {
var downloadpath = results[0]['filename'];
var message = '';
- var code = '';
var url = '';
message =
@@ -104,9 +285,12 @@ function completed(data, finished, count, filename, tabid) {
message = message.replace('$$COUNT$$', count);
message = message.replace('$$FILENAME$$', downloadpath);
message = message.replace('$$URL$$', url);
- code = 'document.body.innerHTML = "' + message + '";';
- chrome.tabs.executeScript(tabid, {
- code: code,
+ chrome.scripting.executeScript({
+ target: { tabId: tabid },
+ func: function(htmlContent) {
+ document.body.innerHTML = htmlContent;
+ },
+ args: [message]
});
//chrome.runtime.reload();
});
@@ -114,7 +298,7 @@ function completed(data, finished, count, filename, tabid) {
chrome.downloads.download(
{
- url: URL.createObjectURL(blob),
+ url: dataUrl,
filename: filename,
},
function (id) {
@@ -292,7 +476,7 @@ function dumpCurrentPage(url, intabid, junkoption, genusersoption, headlinoption
nickname = nicknameoption;
tabid = intabid;
- chrome.tabs.onRemoved.addListener(function (tabid, removed) {
+ chrome.tabs.onRemoved.addListener(function () {
chrome.runtime.reload();
});
@@ -323,8 +507,9 @@ function dumpCurrentPage(url, intabid, junkoption, genusersoption, headlinoption
function (details) {
if (details.tabId == tabid && finished === '') {
sleep(250);
- chrome.tabs.executeScript(tabid, {
- file: 'content.js',
+ chrome.scripting.executeScript({
+ target: { tabId: tabid },
+ files: ['content.js']
});
}
},
@@ -338,46 +523,11 @@ function dumpCurrentPage(url, intabid, junkoption, genusersoption, headlinoption
var today = new Date();
filename = 'WeakestLinkDump-' + today.toISOString().replace(/:/g, '-') + '.csv';
- var worker = new Worker('eventWorker.js');
-
- worker.addEventListener(
- 'message',
- function (e) {
- e = e.data;
-
- if (e.type == 'persondata') {
- for (i = 0; i < e.users.length; i++) {
- addPerson(e.users[i]);
- }
-
- finished = 'Completed';
- completed(userdata.concat(shortnames), finished, count, filename, tabid);
- } else if (e.type == 'status'){
- message =
- '
WeakestLink Dump Running
| Retrieved $$PAGE$$ of $$COUNT$$ page results |
Close this tab to stop dumping
';
- message = message.replace('$$PAGE$$', e.page);
- message = message.replace('$$COUNT$$', e.totalResultCount);
-
- code = 'document.body.innerHTML = "' + message + '";';
- chrome.tabs.executeScript(e.tabid, {
- code: code,
- });
- }
- else {
- finished = "Data Collection Error: " + e.message;
- completed(userdata.concat(shortnames), finished, count, filename, tabid);
- }
- },
- false
- );
-
// Listener that recieves messages from injected content.js
chrome.runtime.onMessage.addListener(function (message) {
try {
if (run) return;
- var cookie = '';
-
// No more results return data to popup
if (message.body.includes('Your search returned no results. Try removing filters or rephrasing your search') && finished === '') {
finished = 'Completed';
@@ -397,11 +547,22 @@ function dumpCurrentPage(url, intabid, junkoption, genusersoption, headlinoption
return;
}
- chrome.cookies.get({ url: 'https://www.linkedin.com', name: 'JSESSIONID' }, function (cookie) {
- worker.postMessage({
- csrftoken: cookie.value.replace(/['"]+/g, ''),
- url: message.url,
- });
+ chrome.cookies.get({ url: 'https://www.linkedin.com', name: 'JSESSIONID' }, async function (cookie) {
+ try {
+ var csrftoken = cookie.value.replace(/['"]+/g, '');
+ var users = await processLinkedInData(message.url, csrftoken);
+
+ // Process all users
+ for (var i = 0; i < users.length; i++) {
+ addPerson(users[i]);
+ }
+
+ finished = 'Completed';
+ completed(userdata.concat(shortnames), finished, count, filename, tabid);
+ } catch (error) {
+ finished = 'Data Collection Error: ' + error.message;
+ completed(userdata.concat(shortnames), finished, count, filename, tabid);
+ }
});
run = true;
diff --git a/manifest.json b/manifest.json
index 7efa2f6..4b0450e 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,13 +1,12 @@
{
- "manifest_version": 2,
+ "manifest_version": 3,
"name": "WeakestLink",
"description": "Extracts users from the currently viewed LinkedIn company page",
- "version": "2.6",
- "content_security_policy": "font-src data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; object-src 'self';",
+ "version": "2.7",
"background": {
- "scripts": ["event.js"]
+ "service_worker": "event.js"
},
- "browser_action": {
+ "action": {
"default_popup": "popup.html",
"default_icon": {
"16": "images/logo16.png",
@@ -17,11 +16,21 @@
}
},
"permissions": [
- "http://www.linkedin.com/*",
- "https://www.linkedin.com/*",
"downloads",
"webNavigation",
"storage",
- "cookies"
+ "cookies",
+ "scripting"
+ ],
+ "host_permissions": [
+ "http://www.linkedin.com/*",
+ "https://www.linkedin.com/*",
+ "https://raw.githubusercontent.com/*"
+ ],
+ "web_accessible_resources": [
+ {
+ "resources": ["eventWorker.js"],
+ "matches": [""]
+ }
]
}
diff --git a/popup.html b/popup.html
index b256cae..01f3a6d 100644
--- a/popup.html
+++ b/popup.html
@@ -16,7 +16,7 @@
- WeakestLink 2.6
+ WeakestLink 2.7
Relase Notes
Use of this extension is against LinkedIn TOS and your account may be restricted.
diff --git a/popup.js b/popup.js
index 3125543..dfbdd1c 100644
--- a/popup.js
+++ b/popup.js
@@ -46,8 +46,14 @@ window.addEventListener('load', function(evt) {
var url = tab.url.replace("COMPANY_PAGE_CANNED_SEARCH", "FACTED_SEARCH")
tabid = tab.id;
- chrome.runtime.getBackgroundPage(function(eventPage) {
- eventPage.dumpCurrentPage(url, tabid, junk, genusers, headline, nickname)
+ chrome.runtime.sendMessage({
+ action: 'dumpCurrentPage',
+ url: url,
+ tabid: tabid,
+ junk: junk,
+ genusers: genusers,
+ headline: headline,
+ nickname: nickname
});
window.close();
});