Commit e3d0760f authored by Laurent Wouters's avatar Laurent Wouters
Browse files

Transform into a web extension

parent 81e4f466868f
......@@ -4,7 +4,7 @@
"version": "0.2",
"description": "A browser for the web of data.",
"icons": {
"48": "icons/browser_action.png"
"48": "icons/app.png"
},
"permissions": [
"tabs",
......@@ -13,7 +13,7 @@
"*://*/*"
],
"browser_action": {
"default_icon": "icons/browser_action_disabled.png",
"default_icon": "icons/app_disabled.png",
"default_title": "Linked Data Browser"
},
"content_scripts": [
......@@ -22,16 +22,16 @@
"<all_urls>"
],
"js": [
"src/content/main.js"
"content/main.js"
],
"css": [
"src/content/main.css"
"content/main.css"
]
}
],
"background": {
"scripts": [
"src/background/main.js"
"background/main.js"
]
}
}
\ No newline at end of file
......@@ -8,7 +8,7 @@
"clean": "bsb -clean-world",
"test": "echo \"Error: no test specified\" && exit 1",
"serve": "bsb -clean-world && bsb -make-world && webpack && webpack-dev-server",
"webpack": "webpack -w",
"webpack": "webpack",
"webpack:production": "NODE_ENV=production webpack"
},
"repository": {
......@@ -21,6 +21,7 @@
"author": "LOGILAB <contact@logilab.fr>",
"license": "LGPL-3.0",
"dependencies": {
"@types/chrome": "0.0.69",
"@types/leaflet": "^1.2.7",
"@types/react": "^16.4.6",
"@types/react-dom": "^16.0.6",
......@@ -39,11 +40,11 @@
"reason-react": ">=0.4.0"
},
"devDependencies": {
"awesome-typescript-loader": "^5.2.0",
"bs-platform": "^3.1.5",
"copy-webpack-plugin": "^4.5.2",
"html-webpack-plugin": "^3.0.0",
"source-map-loader": "^0.2.3",
"ts-loader": "^4.4.2",
"typescript": "^2.9.2",
"web-ext": "^2.7.0",
"webpack": "^4.0.1",
......
/*******************************************************************************
* Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
*
* This file is part of CubicWeb.
*
* CubicWeb is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 2.1 of the License, or (at your option)
* any later version.
*
* CubicWeb is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
declare module "register" {
export function registerNavigations(
onBeforeNavigate: any,
onCompleted: any
): void;
}
var NO_DATA = {
badge: "",
syntax: null,
target: null
};
var allTabs = {};
var MIME = {
"text/n3": "N3",
"application/n-triples": "N3",
"application/n-quads": "N4",
"text/turtle": "TTL",
"application/rdf+xml": "RDF",
"application/ld+json": "JSON",
"application/trig": "TRIG"
}
function getTabData(id) {
if (!allTabs.hasOwnProperty(id)) {
allTabs[id] = {
isActive: false,
linkedData: NO_DATA
};
}
return allTabs[id];
}
function updateTab(tabId) {
updateIcon(tabId);
chrome.tabs.sendMessage(tabId, getTabData(tabId));
}
function updateIcon(tabId) {
data = getTabData(tabId);
chrome.browserAction.setIcon({
path: data.isActive ? {
48: "icons/browser_action.png"
} : {
48: "icons/browser_action_disabled.png"
},
tabId: tabId
});
chrome.browserAction.setBadgeBackgroundColor({
color: [217, 0, 0, 255],
tabId: tabId
});
chrome.browserAction.setBadgeText({
text: data.linkedData.badge,
tabId: tabId
});
}
function toggleActive(tab) {
data = getTabData(tab.id);
data.isActive = !data.isActive;
updateTab(tab.id);
}
chrome.browserAction.onClicked.addListener(toggleActive);
function onTabActivated(tabId) {
updateIcon(tabId);
}
// listen to tab URL changes
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { onTabActivated(tabId); });
// listen to tab switching
chrome.tabs.onActivated.addListener(function (activeInfo) { onTabActivated(activeInfo.tabId); });
function fetchAt(uri) {
return new Promise(function (resolve, reject) {
reject("No data!");
});
}
function fetchContent(tabId) {
return new Promise(function (resolve, reject) {
reject("No data!");
});
}
function parseLinkTags(content) {
var tags = [];
var regexp = RegExp("([a-zA-Z_0-9]+)\\s*=\\s*('[^']*'|\"[^\"]*\")", "g");
var match;
while ((match = regexp.exec(content)) !== null) {
tags.push({
name: match[1],
value: match[2].substring(1, match[2].length - 1)
});
}
return tags;
}
function parseLinks(content) {
var links = [];
var regexp = RegExp("<([^>]*)>(?:\\s*;\\s*([a-zA-Z_0-9]+)\\s*=\\s*('[^']*'|\"[^\"]*\"))*", "g");
var match;
while ((match = regexp.exec(content)) !== null) {
var link = { url: match[1] };
var tags = parseLinkTags(match[0]);
for (var i = 0; i != tags.length; i++) {
link[tags[i].name] = tags[i].value;
}
links.push(link);
}
return links;
}
function getHeader(headers, name) {
for (var i = 0; i != headers.length; i++) {
if (headers[i].name == name) {
return headers[i].value;
}
}
return null;
}
function doDetectDataOnLinks(links) {
for (var j = 0; j != links.length; j++) {
if (links[j].rel == "alternate" && MIME.hasOwnProperty(links[j].type)) {
return {
badge: MIME[links[j].type],
syntax: links[j].type,
target: links[j]
};
}
}
return NO_DATA;
}
function detectDataOnLinks(headers) {
linksContent = getHeader(headers, "Link");
if (linksContent == null)
return NO_DATA;
var links = parseLinks(linksContent);
return doDetectDataOnLinks(links);
}
function detectDataOnContent(tabId, headers) {
var contentType = getHeader(headers, "Content-Type");
if (contentType == null)
return NO_DATA;
contentType = contentType.split(";")[0];
if (MIME.hasOwnProperty(contentType)) {
return {
badge: MIME[contentType],
syntax: contentType,
target: "content"
};
}
return NO_DATA;
}
function onHeadersReceived(details) {
if (details.tabId == -1)
return;
data = getTabData(details.tabId);
if (data.linkedData != NO_DATA)
// already detected something
return;
data.linkedData = detectDataOnContent(details.tabId, details.responseHeaders);
if (data.linkedData == NO_DATA)
data.linkedData = detectDataOnLinks(details.responseHeaders);
updateIcon(details.tabId);
}
function onBeforeNavigate(details) {
if (details.tabId == -1 || details.frameId != 0)
return;
data = getTabData(details.tabId);
data.linkedData = NO_DATA;
updateIcon(details.tabId);
}
function onCompleted(details) {
if (details.tabId == -1 || details.frameId != 0)
return;
updateTab(details.tabId);
}
// listen to received headers
chrome.webRequest.onHeadersReceived.addListener(onHeadersReceived, { urls: ["<all_urls>"] }, ["responseHeaders"]);
// listen to navigation
chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate, { urls: ["<all_urls>"] });
chrome.webNavigation.onCompleted.addListener(onCompleted, { urls: ["<all_urls>"] });
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
data = getTabData(sender.tab.id);
if (request.reqType == "links") {
if (data.linkedData != NO_DATA)
return;
data.linkedData = doDetectDataOnLinks(request.links);
updateIcon(sender.tab.id);
}
});
\ No newline at end of file
/*******************************************************************************
* Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
*
* This file is part of CubicWeb.
*
* CubicWeb is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 2.1 of the License, or (at your option)
* any later version.
*
* CubicWeb is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
import { NO_DATA, MIME, LinkedData, Tag, Link } from "../common/api";
/// <reference path="./fallback.d.ts"/>
let F = require("./register");
import "chrome";
/**
* Data about a tab
*/
class TabData {
/**
* Whether the user activated the extension for this tab
*/
isActive: boolean;
/**
* The data associated to this tab
*/
linkedData: LinkedData;
}
/**
* The data about the tabs
*/
let allTabs: { [index: number]: TabData } = {};
/**
* Gets the data about a tab
* @param id The identifier of a tab
*/
function getTabData(id: number): TabData {
if (!allTabs.hasOwnProperty(id)) {
allTabs[id] = {
isActive: false,
linkedData: NO_DATA
};
}
return allTabs[id];
}
/**
* Updates a tab
* @param id The identifier of a tab
*/
function updateTab(tabId: number): void {
updateIcon(tabId);
chrome.tabs.sendMessage(tabId, getTabData(tabId));
}
/**
* Updates the icon of a tab
* @param id The identifier of a tab
*/
function updateIcon(tabId: number): void {
let data = getTabData(tabId);
chrome.browserAction.setIcon({
path: data.isActive
? {
48: "icons/app.png"
}
: {
48: "icons/app_disabled.png"
},
tabId: tabId
});
chrome.browserAction.setBadgeBackgroundColor({
color: [217, 0, 0, 255],
tabId: tabId
});
chrome.browserAction.setBadgeText({
text: data.linkedData.badge,
tabId: tabId
});
}
/**
* Toggle the activation state of a tab
* @param tab The activated tab
*/
function toggleActive(tab: chrome.tabs.Tab): void {
let data = getTabData(tab.id);
data.isActive = !data.isActive;
updateTab(tab.id);
}
/**
* Listen for the activation/de-activation actions
*/
chrome.browserAction.onClicked.addListener(toggleActive);
/**
* When a tab has been activated
* @param tabId The identifier of a tab
*/
function onTabActivated(tabId: number): void {
updateIcon(tabId);
}
// listen to tab URL changes
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
onTabActivated(tabId);
});
// listen to tab switching
chrome.tabs.onActivated.addListener(function(activeInfo) {
onTabActivated(activeInfo.tabId);
});
/**
* Fetches data at an URI
* @param uri The uri to fetch at
*/
function fetchAt(uri: string): Promise<string> {
return new Promise((resolve, reject) => {
reject("No data!");
});
}
/**
* fetches the content of the current page in a tab
* @param tabId The identifier of a tab
*/
function fetchContent(tabId: number): Promise<string> {
return new Promise(function(resolve, reject) {
reject("No data!");
});
}
/**
* Parses the tags for a link
* @param content The link description
*/
function parseLinkTags(content: string): Tag[] {
let tags: Tag[] = [];
let regexp = RegExp("([a-zA-Z_0-9]+)\\s*=\\s*('[^']*'|\"[^\"]*\")", "g");
let match;
while ((match = regexp.exec(content)) !== null) {
tags.push({
name: match[1],
value: match[2].substring(1, match[2].length - 1)
});
}
return tags;
}
/**
* Parses the content of a link header
* @param content The content to parse
*/
function parseLinks(content: string): Link[] {
let links: Link[] = [];
let regexp = RegExp(
"<([^>]*)>(?:\\s*;\\s*([a-zA-Z_0-9]+)\\s*=\\s*('[^']*'|\"[^\"]*\"))*",
"g"
);
let match;
while ((match = regexp.exec(content)) !== null) {
var link: Link = { url: match[1] };
var tags = parseLinkTags(match[0]);
for (var i = 0; i != tags.length; i++) {
link[tags[i].name] = tags[i].value;
}
links.push(link);
}
return links;
}
/**
* Get the value of a header
* @param headers The headers
* @param name The name of a header
*/
function getHeader(
headers: chrome.webRequest.HttpHeader[],
name: string
): string {
for (var i = 0; i != headers.length; i++) {
if (headers[i].name == name) {
return headers[i].value;
}
}
return null;
}
/**
* Detect data on links from headers
* @param links The links
*/
function doDetectDataOnLinks(links: Link[]): LinkedData {
for (var j = 0; j != links.length; j++) {
if (links[j].rel == "alternate" && MIME.hasOwnProperty(links[j].type)) {
return {
badge: MIME[links[j].type],
syntax: links[j].type,
target: links[j]
};
}
}
return NO_DATA;
}
/**
* Detects linked data in HTTP headers
* @param headers he headers
*/
function detectDataOnLinks(
headers: chrome.webRequest.HttpHeader[]
): LinkedData {
let linksContent = getHeader(headers, "Link");
if (linksContent == null) return NO_DATA;
var links = parseLinks(linksContent);
return doDetectDataOnLinks(links);
}
/**
* Detects linked data in page content
* @param tabId The tab identifier
* @param headers The HTTP headers
*/
function detectDataOnContent(
tabId: number,
headers: chrome.webRequest.HttpHeader[]
): LinkedData {
var contentType = getHeader(headers, "Content-Type");
if (contentType == null) return NO_DATA;
contentType = contentType.split(";")[0];
if (MIME.hasOwnProperty(contentType)) {
return {
badge: MIME[contentType],
syntax: contentType,
target: "content"
};
}
return NO_DATA;
}
/**
* When headers are received for a tab
* @param details The details
*/
function onHeadersReceived(
details: chrome.webRequest.WebResponseHeadersDetails
): void {
if (details.tabId == -1) return;
let data = getTabData(details.tabId);
if (data.linkedData != NO_DATA)
// already detected something
return;
data.linkedData = detectDataOnContent(details.tabId, details.responseHeaders);
if (data.linkedData == NO_DATA)
data.linkedData = detectDataOnLinks(details.responseHeaders);
updateIcon(details.tabId);
}
/**
* Event before a tab navigation occur
* @param details The event details
*/
function onBeforeNavigate(
details: chrome.webNavigation.WebNavigationFramedCallbackDetails
): void {
if (details.tabId == -1 || details.frameId != 0) return;
let data = getTabData(details.tabId);
data.linkedData = NO_DATA;
updateIcon(details.tabId);
}
/**
* Event after tab navigation occurred
* @param details the event details
*/
function onCompleted(
details: chrome.webNavigation.WebNavigationFramedCallbackDetails
): void {
if (details.tabId == -1 || details.frameId != 0) return;
updateTab(details.tabId);
}
// listen to received headers
chrome.webRequest.onHeadersReceived.addListener(
onHeadersReceived,
{ urls: ["<all_urls>"] },
["responseHeaders"]
);
// listen to navigation
F.registerNavigations(onBeforeNavigate, onCompleted);
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
let data = getTabData(sender.tab.id);
if (request.reqType == "links") {
if (data.linkedData != NO_DATA) return;
data.linkedData = doDetectDataOnLinks(request.links);
updateIcon(sender.tab.id);
}
});
/*******************************************************************************
* Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
*
* This file is part of CubicWeb.
*