fix(extension): fix extension setup (#1012)

* fix(extension): use rm instead of trash

* fix(extension): use the appropriate peer-deps for web-ext and eslint-plugin-mozilla

* refactor(extension): moved .gitignore rules into Extension

* fix(extension): fix prettier setup
This commit is contained in:
Stefan Zabka 2022-10-14 12:51:59 +02:00 коммит произвёл GitHub
Родитель d0508248f3
Коммит abf10d745a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 1452 добавлений и 1824 удалений

6
.gitignore поставляемый
Просмотреть файл

@ -90,10 +90,4 @@ docs/apidoc/
# npm packages
node_modules
# built extension artifacts
Extension/dist
Extension/openwpm.xpi
Extension/bundled/content.js
Extension/bundled/feature.js
datadir

8
Extension/.gitignore поставляемый
Просмотреть файл

@ -8,4 +8,10 @@ coverage
.nyc_output
*.log
yarn.lock
yarn.lock
# built extension artifacts
dist
openwpm.xpi
bundled/content.js
bundled/feature.js

Просмотреть файл

@ -1,6 +1,17 @@
node_modules
build
test
src/**.js
.idea/*
coverage
.nyc_output
*.log
yarn.lock
# built extension artifacts
dist
openwpm.xpi
bundled/content.js
bundled/feature.js
bundled/privileged/sockets/bufferpack.js
dist
build
node_modules

Просмотреть файл

@ -0,0 +1 @@
{}

3052
Extension/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -17,25 +17,24 @@
"@types/firefox-webext-browser": "^94.0.1",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0",
"ajv": "^8.11.0",
"ajv": "^6.9.1",
"body-parser": "^1.20.0",
"download": "^8.0.0",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-fetch-options": "0.0.5",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-html": "^6.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.3.6",
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-mozilla": "^2.12.5",
"eslint-plugin-no-unsanitized": "^4.0.1",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-prettier": "^3.0.0",
"eslint-plugin-unicorn": "^43.0.0",
"express": "^4.18.1",
"prettier": "^2.7.1",
"prettier": "^1.19.1",
"safe-compare": "^1.1.4",
"trash-cli": "^5.0.0",
"ts-loader": "^9.4.1",
"typedoc": "^0.23.15",
"typescript": "^4.8.3",
@ -61,7 +60,7 @@
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:web-ext-lint",
"lint:eslint": "eslint .",
"lint:web-ext-lint": "web-ext lint",
"lint:prettier": "prettier . --list-different",
"lint:prettier": "prettier 'src/**/*.ts' --list-different",
"info": "npm-scripts-info",
"build": "npm run clean && npm run build:main && npm run build:module && npm run build:webpack && npm run build:webext",
"build:main": "tsc -p tsconfig.json",
@ -69,12 +68,12 @@
"build:webpack": "webpack",
"build:webext": "web-ext build",
"fix": "npm run fix:prettier && npm run fix:eslint",
"fix:prettier": "prettier . --write",
"fix:prettier": "prettier 'src/**/*.ts' --write",
"fix:eslint": "eslint --fix .",
"test": "npm run build && npm run test:lint",
"test:lint": "eslint . && prettier . --list-different || exit 0",
"test:lint": "eslint . && npm run lint:prettier || exit 0",
"watch": "npm run clean && npm run build",
"clean": "trash build test",
"clean": "rm -rf build test",
"prepare": "npm run build && npm run test",
"start": "web-ext run --no-reload"
},

Просмотреть файл

@ -18,7 +18,7 @@ export class DnsInstrument {
public run(crawlID) {
const filter: RequestFilter = { urls: ["<all_urls>"], types: allTypes };
const requestStemsFromExtension = (details) => {
const requestStemsFromExtension = details => {
return (
details.originUrl &&
details.originUrl.indexOf("moz-extension://") > -1 &&
@ -58,7 +58,7 @@ export class DnsInstrument {
private handleResolvedDnsData(dnsRecordObj, dataReceiver) {
// Curring the data returned by API call.
return function (record) {
return function(record) {
// Get data from API call
dnsRecordObj.addresses = record.addresses.toString();
dnsRecordObj.canonical_name = record.canonicalName;

Просмотреть файл

@ -71,7 +71,7 @@ export class HttpInstrument {
public run(crawlID, saveContentOption: SaveContentOption) {
const filter: RequestFilter = { urls: ["<all_urls>"], types: allTypes };
const requestStemsFromExtension = (details) => {
const requestStemsFromExtension = details => {
return (
details.originUrl && details.originUrl.indexOf("moz-extension://") > -1
);
@ -106,7 +106,7 @@ export class HttpInstrument {
: ["requestBody"],
);
this.onBeforeSendHeadersListener = (details) => {
this.onBeforeSendHeadersListener = details => {
// Ignore requests made by extensions
if (requestStemsFromExtension(details)) {
return;
@ -125,7 +125,7 @@ export class HttpInstrument {
["requestHeaders"],
);
this.onBeforeRedirectListener = (details) => {
this.onBeforeRedirectListener = details => {
// Ignore requests made by extensions
if (requestStemsFromExtension(details)) {
return;
@ -138,7 +138,7 @@ export class HttpInstrument {
["responseHeaders"],
);
this.onCompletedListener = (details) => {
this.onCompletedListener = details => {
// Ignore requests made by extensions
if (requestStemsFromExtension(details)) {
return;
@ -271,7 +271,7 @@ export class HttpInstrument {
const headers = [];
let isOcsp = false;
if (details.requestHeaders) {
details.requestHeaders.map((requestHeader) => {
details.requestHeaders.map(requestHeader => {
const { name, value } = requestHeader;
const header_pair = [];
header_pair.push(escapeString(name));
@ -299,8 +299,7 @@ export class HttpInstrument {
"Pending request timed out waiting for data from both onBeforeRequest and onBeforeSendHeaders events",
);
} else {
const onBeforeRequestEventDetails =
await pendingRequest.onBeforeRequestEventDetails;
const onBeforeRequestEventDetails = await pendingRequest.onBeforeRequestEventDetails;
const requestBody = onBeforeRequestEventDetails.requestBody;
if (requestBody) {
@ -679,7 +678,7 @@ export class HttpInstrument {
const resultHeaders = [];
let location = "";
if (headers) {
headers.map((responseHeader) => {
headers.map(responseHeader => {
const { name, value } = responseHeader;
const header_pair = [];
header_pair.push(escapeString(name));

Просмотреть файл

@ -108,7 +108,7 @@ export class JavascriptInstrument {
}
this.crawlID = crawlID;
this.configured = true;
this.pendingRecords.map((update) => {
this.pendingRecords.map(update => {
update.browser_id = this.crawlID;
this.dataReceiver.saveRecord("javascript", update);
});

Просмотреть файл

@ -74,11 +74,10 @@ export class NavigationInstrument {
details.frameId,
);
const pendingNavigation = this.instantiatePendingNavigation(navigationId);
const navigation: Navigation =
await transformWebNavigationBaseEventDetailsToOpenWPMSchema(
crawlID,
details,
);
const navigation: Navigation = await transformWebNavigationBaseEventDetailsToOpenWPMSchema(
crawlID,
details,
);
navigation.parent_frame_id = details.parentFrameId;
navigation.before_navigate_event_ordinal = incrementedEventOrdinal();
navigation.before_navigate_time_stamp = new Date(
@ -97,11 +96,10 @@ export class NavigationInstrument {
details.tabId,
details.frameId,
);
const navigation: Navigation =
await transformWebNavigationBaseEventDetailsToOpenWPMSchema(
crawlID,
details,
);
const navigation: Navigation = await transformWebNavigationBaseEventDetailsToOpenWPMSchema(
crawlID,
details,
);
navigation.transition_qualifiers = escapeString(
JSON.stringify(details.transitionQualifiers),
);
@ -117,8 +115,7 @@ export class NavigationInstrument {
pendingNavigation.resolveOnCommittedEventNavigation(navigation);
const resolved = await pendingNavigation.resolvedWithinTimeout(1000);
if (resolved) {
const onBeforeNavigateEventNavigation =
await pendingNavigation.onBeforeNavigateEventNavigation;
const onBeforeNavigateEventNavigation = await pendingNavigation.onBeforeNavigateEventNavigation;
navigation.parent_frame_id =
onBeforeNavigateEventNavigation.parent_frame_id;
navigation.before_navigate_event_ordinal =

Просмотреть файл

@ -54,7 +54,7 @@ document.addEventListener(eventId, (e: CustomEvent) => {
// pass these on to the background page
const msgs = e.detail;
if (Array.isArray(msgs)) {
msgs.forEach((msg) => {
msgs.forEach(msg => {
emitMsg(msg.type, msg.content);
});
} else {

Просмотреть файл

@ -34,7 +34,7 @@ export class HttpPostParser {
if (requestBody.raw) {
return {
post_body_raw: JSON.stringify(
requestBody.raw.map((x) => [
requestBody.raw.map(x => [
x.file,
Uint8ToBase64(new Uint8Array(x.bytes)),
]),

Просмотреть файл

@ -58,7 +58,7 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
// Rough implementations of Object.getPropertyDescriptor and Object.getPropertyNames
// See http://wiki.ecmascript.org/doku.php?id=harmony:extended_object_api
Object.getPropertyDescriptor = function (subject, name) {
Object.getPropertyDescriptor = function(subject, name) {
if (subject === undefined) {
throw new Error("Can't get property descriptor for undefined");
}
@ -71,7 +71,7 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
return pd;
};
Object.getPropertyNames = function (subject) {
Object.getPropertyNames = function(subject) {
if (subject === undefined) {
throw new Error("Can't get property names for undefined");
}
@ -93,7 +93,7 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
let timestamp;
let result;
const later = function () {
const later = function() {
const last = Date.now() - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
@ -106,7 +106,7 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
}
};
return function () {
return function() {
context = this; // eslint-disable-line @typescript-eslint/no-this-alias
args = arguments;
timestamp = Date.now();
@ -174,7 +174,7 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
return object;
}
const seenObjects = [];
return JSON.stringify(object, function (key, value) {
return JSON.stringify(object, function(key, value) {
if (value === null) {
return "null";
}
@ -342,7 +342,7 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
}
// from http://stackoverflow.com/a/5202185
const rsplit = function (source: string, sep, maxsplit) {
const rsplit = function(source: string, sep, maxsplit) {
const split = source.split(sep);
return maxsplit
? [split.slice(0, -maxsplit).join(sep)].concat(split.slice(-maxsplit))
@ -350,7 +350,9 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
};
function getOriginatingScriptContext(getCallStack = false) {
const trace = getStackTrace().trim().split("\n");
const trace = getStackTrace()
.trim()
.split("\n");
// return a context object even if there is an error
const empty_context = {
scriptUrl: "",
@ -402,7 +404,12 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
scriptCol: columnNo,
funcName,
scriptLocEval,
callStack: getCallStack ? trace.slice(3).join("\n").trim() : "",
callStack: getCallStack
? trace
.slice(3)
.join("\n")
.trim()
: "",
};
return callContext;
} catch (e) {
@ -439,7 +446,7 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
func: any,
logSettings: LogSettings,
) {
return function () {
return function() {
const callContext = getOriginatingScriptContext(logSettings.logCallStack);
logCall(
objectName + "." + methodName,
@ -496,7 +503,7 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
get: () => {
return undefinedPropValue;
},
set: (value) => {
set: value => {
undefinedPropValue = value;
},
enumerable: false,
@ -511,8 +518,8 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
// accessor property
Object.defineProperty(object, propertyName, {
configurable: true,
get: (function () {
return function () {
get: (function() {
return function() {
let origProperty;
const callContext = getOriginatingScriptContext(
logSettings.logCallStack,
@ -591,8 +598,8 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
}
};
})(),
set: (function () {
return function (value) {
set: (function() {
return function(value) {
const callContext = getOriginatingScriptContext(
logSettings.logCallStack,
);
@ -730,17 +737,17 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
}
}
const sendFactory = function (eventId, $sendMessagesToLogger) {
const sendFactory = function(eventId, $sendMessagesToLogger) {
let messages = [];
// debounce sending queued messages
const send = debounce(function () {
const send = debounce(function() {
$sendMessagesToLogger(eventId, messages);
// clear the queue
messages = [];
}, 100);
return function (msgType, msg) {
return function(msgType, msg) {
// queue the message
messages.push({ type: msgType, content: msg });
send();
@ -755,7 +762,7 @@ export function getInstrumentJS(eventId: string, sendMessagesToLogger) {
// More details about how this function is invoked are in
// content/javascript-instrument-content-scope.ts
JSInstrumentRequests.forEach(function (item) {
JSInstrumentRequests.forEach(function(item) {
instrumentObject(
eval(item.object),
item.instrumentedName,

Просмотреть файл

@ -9,10 +9,10 @@ export class PendingNavigation {
public resolveOnBeforeNavigateEventNavigation: (details: Navigation) => void;
public resolveOnCommittedEventNavigation: (details: Navigation) => void;
constructor() {
this.onBeforeNavigateEventNavigation = new Promise((resolve) => {
this.onBeforeNavigateEventNavigation = new Promise(resolve => {
this.resolveOnBeforeNavigateEventNavigation = resolve;
});
this.onCommittedEventNavigation = new Promise((resolve) => {
this.onCommittedEventNavigation = new Promise(resolve => {
this.resolveOnCommittedEventNavigation = resolve;
});
}
@ -32,7 +32,7 @@ export class PendingNavigation {
public async resolvedWithinTimeout(ms) {
const resolved = await Promise.race([
this.resolved(),
new Promise((resolve) => setTimeout(resolve, ms)),
new Promise(resolve => setTimeout(resolve, ms)),
]);
return resolved;
}

Просмотреть файл

@ -7,8 +7,12 @@ import {
* Ties together the two separate events that together holds information about both request headers and body
*/
export class PendingRequest {
public readonly onBeforeRequestEventDetails: Promise<WebRequestOnBeforeRequestEventDetails>;
public readonly onBeforeSendHeadersEventDetails: Promise<WebRequestOnBeforeSendHeadersEventDetails>;
public readonly onBeforeRequestEventDetails: Promise<
WebRequestOnBeforeRequestEventDetails
>;
public readonly onBeforeSendHeadersEventDetails: Promise<
WebRequestOnBeforeSendHeadersEventDetails
>;
public resolveOnBeforeRequestEventDetails: (
details: WebRequestOnBeforeRequestEventDetails,
) => void;
@ -16,10 +20,10 @@ export class PendingRequest {
details: WebRequestOnBeforeSendHeadersEventDetails,
) => void;
constructor() {
this.onBeforeRequestEventDetails = new Promise((resolve) => {
this.onBeforeRequestEventDetails = new Promise(resolve => {
this.resolveOnBeforeRequestEventDetails = resolve;
});
this.onBeforeSendHeadersEventDetails = new Promise((resolve) => {
this.onBeforeSendHeadersEventDetails = new Promise(resolve => {
this.resolveOnBeforeSendHeadersEventDetails = resolve;
});
}
@ -39,7 +43,7 @@ export class PendingRequest {
public async resolvedWithinTimeout(ms) {
const resolved = await Promise.race([
this.resolved(),
new Promise((resolve) => setTimeout(resolve, ms)),
new Promise(resolve => setTimeout(resolve, ms)),
]);
return resolved;
}

Просмотреть файл

@ -8,8 +8,12 @@ import { ResponseBodyListener } from "./response-body-listener";
* Ties together the two separate events that together holds information about both response headers and body
*/
export class PendingResponse {
public readonly onBeforeRequestEventDetails: Promise<WebRequestOnBeforeRequestEventDetails>;
public readonly onCompletedEventDetails: Promise<WebRequestOnCompletedEventDetails>;
public readonly onBeforeRequestEventDetails: Promise<
WebRequestOnBeforeRequestEventDetails
>;
public readonly onCompletedEventDetails: Promise<
WebRequestOnCompletedEventDetails
>;
public responseBodyListener: ResponseBodyListener;
public resolveOnBeforeRequestEventDetails: (
details: WebRequestOnBeforeRequestEventDetails,
@ -18,10 +22,10 @@ export class PendingResponse {
details: WebRequestOnCompletedEventDetails,
) => void;
constructor() {
this.onBeforeRequestEventDetails = new Promise((resolve) => {
this.onBeforeRequestEventDetails = new Promise(resolve => {
this.resolveOnBeforeRequestEventDetails = resolve;
});
this.onCompletedEventDetails = new Promise((resolve) => {
this.onCompletedEventDetails = new Promise(resolve => {
this.resolveOnCompletedEventDetails = resolve;
});
}
@ -46,7 +50,7 @@ export class PendingResponse {
public async resolvedWithinTimeout(ms) {
const resolved = await Promise.race([
this.resolved(),
new Promise((resolve) => setTimeout(resolve, ms)),
new Promise(resolve => setTimeout(resolve, ms)),
]);
return resolved;
}

Просмотреть файл

@ -8,10 +8,10 @@ export class ResponseBodyListener {
private resolveContentHash: (contentHash: string) => void;
constructor(details: WebRequestOnBeforeRequestEventDetails) {
this.responseBody = new Promise((resolve) => {
this.responseBody = new Promise(resolve => {
this.resolveResponseBody = resolve;
});
this.contentHash = new Promise((resolve) => {
this.contentHash = new Promise(resolve => {
this.resolveContentHash = resolve;
});
@ -21,8 +21,8 @@ export class ResponseBodyListener {
) as any;
let responseBody = new Uint8Array();
filter.ondata = (event) => {
digestMessage(event.data).then((digest) => {
filter.ondata = event => {
digestMessage(event.data).then(digest => {
this.resolveContentHash(digest);
});
const incoming = new Uint8Array(event.data);
@ -33,7 +33,7 @@ export class ResponseBodyListener {
filter.write(event.data);
};
filter.onstop = (_event) => {
filter.onstop = _event => {
this.resolveResponseBody(responseBody);
filter.disconnect();
};

Просмотреть файл

@ -6,8 +6,6 @@
export async function digestMessage(msgUint8: Uint8Array) {
const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8); // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join(""); // convert bytes to hex string
const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); // convert bytes to hex string
return hashHex;
}

Просмотреть файл

@ -2,7 +2,7 @@ export function encode_utf8(s) {
return unescape(encodeURIComponent(s));
}
export const escapeString = function (str: any) {
export const escapeString = function(str: any) {
// Convert to string if necessary
if (typeof str !== "string") {
str = String(str);
@ -11,7 +11,7 @@ export const escapeString = function (str: any) {
return encode_utf8(str);
};
export const escapeUrl = function (
export const escapeUrl = function(
url: string,
stripDataUrlData: boolean = true,
) {
@ -29,7 +29,7 @@ export const escapeUrl = function (
// Base64 encoding, found on:
// https://stackoverflow.com/questions/12710001/how-to-convert-uint8-array-to-base64-encoded-string/25644409#25644409
export const Uint8ToBase64 = function (u8Arr: Uint8Array) {
export const Uint8ToBase64 = function(u8Arr: Uint8Array) {
const CHUNK_SIZE = 0x8000; // arbitrary number
let index = 0;
const length = u8Arr.length;
@ -43,6 +43,6 @@ export const Uint8ToBase64 = function (u8Arr: Uint8Array) {
return btoa(result);
};
export const boolToInt = function (bool: boolean) {
export const boolToInt = function(bool: boolean) {
return bool ? 1 : 0;
};

Просмотреть файл

@ -7,7 +7,7 @@ let storageController = null;
let logAggregator = null;
let listeningSocket = null;
const listeningSocketCallback = async (data) => {
const listeningSocketCallback = async data => {
// This works even if data is an int
const action = data.action;
let newVisitID = data.visit_id;
@ -42,7 +42,7 @@ const listeningSocketCallback = async (data) => {
visitID = newVisitID;
}
};
export const open = async function (
export const open = async function(
storageControllerAddress,
logAddress,
curr_crawlID,
@ -88,7 +88,7 @@ export const open = async function (
});
};
export const close = function () {
export const close = function() {
if (storageController != null) {
storageController.close();
}
@ -97,7 +97,7 @@ export const close = function () {
}
};
const makeLogJSON = function (lvl, msg) {
const makeLogJSON = function(lvl, msg) {
const log_json = {
name: "Extension-Logger",
level: lvl,
@ -111,7 +111,7 @@ const makeLogJSON = function (lvl, msg) {
return log_json;
};
export const logInfo = function (msg) {
export const logInfo = function(msg) {
// Always log to browser console
console.log(msg);
@ -124,7 +124,7 @@ export const logInfo = function (msg) {
logAggregator.send(JSON.stringify(["EXT", JSON.stringify(log_json)]));
};
export const logDebug = function (msg) {
export const logDebug = function(msg) {
// Always log to browser console
console.log(msg);
@ -137,7 +137,7 @@ export const logDebug = function (msg) {
logAggregator.send(JSON.stringify(["EXT", JSON.stringify(log_json)]));
};
export const logWarn = function (msg) {
export const logWarn = function(msg) {
// Always log to browser console
console.warn(msg);
@ -150,7 +150,7 @@ export const logWarn = function (msg) {
logAggregator.send(JSON.stringify(["EXT", JSON.stringify(log_json)]));
};
export const logError = function (msg) {
export const logError = function(msg) {
// Always log to browser console
console.error(msg);
@ -163,7 +163,7 @@ export const logError = function (msg) {
logAggregator.send(JSON.stringify(["EXT", JSON.stringify(log_json)]));
};
export const logCritical = function (msg) {
export const logCritical = function(msg) {
// Always log to browser console
console.error(msg);
@ -182,7 +182,7 @@ export const dataReceiver = {
},
};
export const saveRecord = function (instrument, record) {
export const saveRecord = function(instrument, record) {
record.visit_id = visitID;
if (!visitID && !debugging) {
@ -211,7 +211,7 @@ export const saveRecord = function (instrument, record) {
};
// Stub for now
export const saveContent = async function (content, contentHash) {
export const saveContent = async function(content, contentHash) {
// Send page content to the data aggregator
// deduplicated by contentHash in a levelDB database
if (debugging) {
@ -244,13 +244,13 @@ function Uint8ToBase64(u8Arr) {
return btoa(result);
}
export const escapeString = function (string) {
export const escapeString = function(string) {
// Convert to string if necessary
if (typeof string !== "string") string = "" + string;
return encode_utf8(string);
};
export const boolToInt = function (bool) {
export const boolToInt = function(bool) {
return bool ? 1 : 0;
};

Просмотреть файл

@ -10,7 +10,7 @@ eval "$(conda shell.bash hook)"
conda activate openwpm
pushd Extension
npm install --legacy-peer-deps
npm install
popd
echo "Success: Extension/openwpm.xpi has been built"