generateFunctionMocks = (proto, itemMainProp, dataArray) => ({
item: utils.createProxy(proto.item, {
apply(target, ctx, args) {
if (!args.length) {
throw new TypeError(
`Failed to execute 'item' on '${proto[Symbol.toStringTag]}': 1 argument required, but only 0 present.`
);
}
// Special behavior alert:
// - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup
// - If anything else than an integer (including as string) is provided it will return the first entry
const isInteger = args[0] && Number.isInteger(Number(args[0])); // Cast potential string to number first, then check for integer
// Note: Vanilla never returns `undefined`
return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null;
}
}),
/** Returns the MimeType object with the specified name. */
namedItem: utils.createProxy(proto.namedItem, {
apply(target, ctx, args) {
if (!args.length) {
throw new TypeError(
`Failed to execute 'namedItem' on '${proto[Symbol.toStringTag]}': 1 argument required, but only 0 present.`
);
}
return dataArray.find((mt) => mt[itemMainProp] === args[0]) || null; // Not `undefined`!
}
}),
/** Does nothing and shall return nothing */
refresh: proto.refresh
? utils.createProxy(proto.refresh, {
apply(target, ctx, args) {
return undefined;
}
})
: undefined
});
function generateMagicArray(
dataArray = [],
proto = MimeTypeArray.prototype,
itemProto = MimeType.prototype,
itemMainProp = "type"
) {
// Quick helper to set props with the same descriptors vanilla is using
const defineProp = (obj, prop, value) =>
Object.defineProperty(obj, prop, {
value,
writable: false,
enumerable: false, // Important for mimeTypes & plugins: `JSON.stringify(navigator.mimeTypes)`
configurable: false
});
// Loop over our fake data and construct items
const makeItem = (data) => {
const item = {};
for (const prop of Object.keys(data)) {
if (prop.startsWith("__")) {
continue;
}
defineProp(item, prop, data[prop]);
}
// navigator.plugins[i].length should always be 1
if (itemProto === Plugin.prototype) {
defineProp(item, "length", 1);
}
// We need to spoof a specific `MimeType` or `Plugin` object
return Object.create(itemProto, Object.getOwnPropertyDescriptors(item));
};
const magicArray = [];
// Loop through our fake data and use that to create convincing entities
dataArray.forEach((data) => {
magicArray.push(makeItem(data));
});
// Add direct property access based on types (e.g. `obj['application/pdf']`) afterwards
magicArray.forEach((entry) => {
defineProp(magicArray, entry[itemMainProp], entry);
});
// This is the best way to fake the type to make sure this is false: `Array.isArray(navigator.mimeTypes)`
const magicArrayObj = Object.create(proto, {
...Object.getOwnPropertyDescriptors(magicArray),
// There's one ugly quirk we unfortunately need to take care of:
// The `MimeTypeArray` prototype has an enumerable `length` property,
// but headful Chrome will still skip it when running `Object.getOwnPropertyNames(navigator.mimeTypes)`.
// To strip it we need to make it first `configurable` and can then overlay a Proxy with an `ownKeys` trap.
length: {
value: magicArray.length,
writable: false,
enumerable: false,
configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length`
}
});
// Generate our functional function mocks :-)
const functionMocks = generateFunctionMocks(proto, itemMainProp, magicArray);
// Override custom object with proxy
return new Proxy(magicArrayObj, {
get(target, key = "") {
// Redirect function calls to our custom proxied versions mocking the vanilla behavior
if (key === "item") {
return functionMocks.item;
}
if (key === "namedItem") {
return functionMocks.namedItem;
}
if (proto === PluginArray.prototype && key === "refresh") {
return functionMocks.refresh;
}
// Everything else can pass through as normal
return utils.cache.Reflect.get(...arguments);
},
ownKeys(target) {
// There are a couple of quirks where the original property demonstrates "magical" behavior that makes no sense
// This can be witnessed when calling `Object.getOwnPropertyNames(navigator.mimeTypes)` and the absense of `length`
// My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly
// For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing
// Specific issues when not patching this: `length` property is available, direct `types` props (e.g. `obj['application/pdf']`) are missing
const keys = [];
const typeProps = magicArray.map((mt) => mt[itemMainProp]);
typeProps.forEach((_, i) => keys.push(`${i}`));
typeProps.forEach((propName) => keys.push(propName));
return keys;
}
});
}