our
$VERSION
=
'1.58'
;
sub
_BUFFER_SIZE {
return
65_536 }
sub
_MSIE_VERSION {
return
11 }
my
$content_name
=
'content.js'
;
my
%_function_bodies
= (
'Navigator.bluetooth'
=>
q[return { getAvailability: function () { return new Promise((resolve, reject) => resolve(false))} };]
,
'Navigator.canShare'
=>
q[return true]
,
'Navigator.clearAppBadge'
=>
q[return new Promise((resolve, reject) => resolve(undefined))]
,
'Navigator.connection'
=>
q[let downlink = (new Array(7.55, 1.6))[Math.floor(Math.random() * 2)]
; let rtt = (new Array(50, 100))[Math.floor(Math.random() * 2)]; let obj = { onchange: null, effectiveType: decodeURIComponent(\\x27%274g%27\\x27), rtt: rtt, downlink: downlink, saveData: false };
return
new NetworkInformation(obj)],
'Navigator.deprecatedReplaceInURN'
=>
q[throw TypeError(decodeURIComponent(\\x27Failed to execute %27deprecatedReplaceInURN%27 on %27Navigator%27: Passed URL must be a valid URN URL.\\x27))]
,
'Navigator.deprecatedURNtoURL'
=>
q[throw TypeError(decodeURIComponent(\\x27Failed to execute %27deprecatedURNtoURL%27 on %27Navigator%27: Passed URL must be a valid URN URL.\\x27))]
,
'Navigator.deviceMemory'
=>
q[return (navigator.hardwareConcurrency < 4 ? 4 : 8)]
,
'Navigator.getBattery'
=>
q[return new Promise((resolve, reject) => resolve({ charging: true, chargingTime: 0, dischargingTime: Infinity, level: 1, onchargingchange: null }))]
,
'Navigator.getGamePads'
=>
q[return new Array( null, null, null, null )]
,
'Navigator.getInstalledRelatedApps'
=>
q[return new Promise((resolve,reject) => resolve([]
))],
'Navigator.getUserMedia'
=>
q[return getUserMedia]
,
'Navigator.gpu'
=>
q[return { wgslLanguageFeatures: { size: 0 } }]
,
'Navigator.hid'
=>
q[return { getDevices: function() { return new Promise((resolve,reject) => resolve([]
)) }, requestDevices: function() {
return
new Promise((resolve, reject) => resolve([])) } }],
'Navigator.ink'
=>
q[return {}]
,
'Navigator.keyboard'
=>
q[return { getLayoutMap: function() { }, lock: function() { }, unlock: function() { } }]
,
'Navigator.locks'
=>
q[return { query: function() { }, request: function() { } }]
,
'Navigator.login'
=>
q[return { setStatus: function() { return undefined } }]
,
'Navigator.webkitGetUserMedia'
=>
q[return getUserMedia]
,
'Navigator.xr'
=>
q[return new XRSystem({ondevicechange: null})]
,
);
sub
new {
my
(
$class
,
%agent_parameters
) =
@_
;
my
$zip
= Archive::Zip->new();
my
$manifest
=
$zip
->addString(
$class
->_manifest_contents(),
'manifest.json'
);
$manifest
->desiredCompressionMethod( Archive::Zip::COMPRESSION_DEFLATED() );
my
$content
=
$zip
->addString(
$class
->_content_contents(
%agent_parameters
),
$content_name
);
$content
->desiredCompressionMethod( Archive::Zip::COMPRESSION_DEFLATED() );
return
$zip
;
}
sub
_manifest_contents {
my
(
$class
) =
@_
;
return
<<"_JS_";
{
"description": "Firefox::Marionette Stealth extension",
"manifest_version": 2,
"name": "Firefox Marionette Stealth extension",
"version": "1.1",
"permissions": [
"activeTab"
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["$content_name"],
"match_about_blank": true,
"run_at": "document_start",
"all_frames": true
}
]
}
_JS_
}
sub
_content_contents {
my
(
$class
,
%agent_parameters
) =
@_
;
my
$user_agent_contents
=
$class
->user_agent_contents(
%agent_parameters
);
$user_agent_contents
=~ s/\s+/ /smxg;
$user_agent_contents
=~ s/\\n/\\\\n/smxg;
return
<<"_JS_";
{
let script = document.createElement('script');
let text = document.createTextNode('$user_agent_contents');
script.appendChild(text);
(document.head || document.documentElement).appendChild(script);
script.remove();
}
_JS_
}
my
$_function_definition_count
= 1;
sub
_get_browser_type_and_version {
my
(
$class
,
$user_agent_string
) =
@_
;
my
(
$browser_type
,
$browser_version
);
if
(
$user_agent_string
=~ /Chrome\/(\d+)/smx ) {
(
$browser_type
,
$browser_version
) = (
'chrome'
, $1 );
if
(
$user_agent_string
=~ /Edg(?:[eA]|iOS)?\/(\d+)/smx ) {
(
$browser_type
,
$browser_version
) = (
'edge'
, $1 );
}
elsif
(
$user_agent_string
=~ /(?:Opera|Presto|OPR)\/(\d+)/smx ) {
(
$browser_type
,
$browser_version
) = (
'opera'
, $1 );
}
}
elsif
(
$user_agent_string
=~ /Version\/(\d+)(?:[.]\d+)?[ ].
*Safari
\/\d+/smx )
{
(
$browser_type
,
$browser_version
) = (
'safari'
, $1 );
}
elsif
(
$user_agent_string
=~ /Trident/smx ) {
(
$browser_type
,
$browser_version
) = (
'ie'
, _MSIE_VERSION() );
}
my
$general_token_re
=
qr/Mozilla\/
5[.]0[ ]/smx;
my
$platform_etc_re
=
qr/[(][^)]+[)][ ]/
smx;
my
$gecko_trail_re
=
qr/Gecko\/
20100101[ ]/smx;
my
$firefox_version_re
=
qr/Firefox\/
(\d+)[.]0/smx;
if
(
$user_agent_string
=~
/^
$general_token_re
$platform_etc_re
$gecko_trail_re
$firefox_version_re
$/smx
)
{
(
$browser_type
,
$browser_version
) = (
'firefox'
, $1 );
}
return
(
$browser_type
,
$browser_version
);
}
sub
user_agent_contents {
my
(
$class
,
%parameters
) =
@_
;
my
(
$to_browser_type
,
$to_browser_version
);
if
(
defined
$parameters
{to} ) {
(
$to_browser_type
,
$to_browser_version
) =
$class
->_get_browser_type_and_version(
$parameters
{to} );
}
$_function_definition_count
= 1;
my
(
$definition_name
,
$function_definition
) =
$class
->_get_js_function_definition(
$to_browser_type
,
'webdriver'
,
'return false'
);
my
$native_code_body
=
$class
->_native_code_body(
$to_browser_type
);
my
$contents
=
<<"_JS_";
{
if (("console" in window) && ("log" in window.console)) {
console.log("Loading Firefox::Marionette::Extension::Stealth");
}
let navProto = Object.getPrototypeOf(window.navigator);
let winProto = Object.getPrototypeOf(window);
$function_definition
Object.defineProperty(navProto, "webdriver", {get: $definition_name, enumerable: false, configurable: true});
console.clear = function() { console.log("$class blocked an attempt at clearing the console...") };
console.clear.toString = function clear_def() { return "function clear() $native_code_body" };
let getUserMedia = navProto.mozGetUserMedia;
_JS_
my
(
$from_browser_type
,
$from_browser_version
);
if
(
defined
$to_browser_type
) {
(
$from_browser_type
,
$from_browser_version
) =
$class
->_get_browser_type_and_version(
$parameters
{from} );
if
( (
defined
$from_browser_type
)
&& (
$from_browser_type
ne
$to_browser_type
) )
{
$contents
.=
<<"_JS_";
window.eval.toString = function eval_def() { return "function eval() $native_code_body" };
Function.prototype.bind.toString = function bind_def() { return "function bind() $native_code_body" };
_JS_
}
if
( (
$to_browser_type
eq
'chrome'
)
|| (
$to_browser_type
eq
'edge'
)
|| (
$to_browser_type
eq
'opera'
) )
{
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Navigator.webkitPersistentStorage'
,
{
function_body
=>
q[return { queryUsageAndQuota: function() { return undefined }, requestQuota: function() { return undefined } }]
},
{}
);
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Navigator.webkitTemporaryStorage'
,
{
function_body
=>
q[return { queryUsageAndQuota: function() { return undefined }, requestQuota: function() { return undefined } }]
},
{}
);
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Window.webkitResolveLocalFileSystemURL'
,
{
function_body
=>
q[return undefined]
}, {}
);
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Window.webkitMediaStream'
,
{
function_body
=>
q[return undefined]
}, {} );
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Window.webkitSpeechGrammar'
,
{
function_body
=>
q[return undefined]
}, {} );
$contents
.=
<<'_JS_';
delete navProto.oscpu;
delete window.ApplePayError;
delete window.CSSPrimitiveValue;
delete window.Counter;
delete navigator.getStorageUpdates;
delete window.WebKitMediaKeys;
let chrome = {
csi: function () { },
getVariableName: function () { },
loadTimes: function () { },
metricsPrivate: {
MetricTypeType: {
HISTOGRAM_LINEAR: "histogram-linear",
HISTOGRAM_LOG: "histogram-log"
},
getFieldTrial: function () { },
getHistogram: function () { },
getVariationParams: function () { },
recordBoolean: function () { },
recordCount: function () { },
recordEnumerationValue: function () { },
recordLongTime: function () { },
recordMediumCount: function () { },
recordMediumTime: function () { },
recordPercentage: function () { },
recordSmallCount: function () { },
recordSparseValue: function () { },
recordSparseValueWithHashMetricName: function () { },
recordSparseValueWithPersistentHash: function () { },
recordTime: function () { },
recordUserAction: function () { },
recordValue: function () { }
},
send: function () { },
timeTicks: { nowInMicroseconds: function () { } },
webstore: function () { },
app: function () { },
runtime: { connect: function() { }, sendMessage: function() { } }
};
Object.defineProperty(winProto, "chrome", {value: chrome, writable: true, enumerable: true, configurable: true});
let canLoadAdAuctionFencedFrame = function() { return true };
Object.defineProperty(navProto, "canLoadAdAuctionFencedFrame", {value: canLoadAdAuctionFencedFrame, writable: true, enumerable: true, configurable: true});
let createAuctionNonce = function() { return crypto.randomUUID() };
Object.defineProperty(navProto, "createAuctionNonce", {value: createAuctionNonce, writable: true, enumerable: true, configurable: true});
Object.defineProperty(navProto, "deprecatedRunAdAuctionEnforcesKAnonymity", {value: false, writable: true, enumerable: true, configurable: true});
_JS_
if
(
$to_browser_type
eq
'edge'
) {
}
elsif
(
$to_browser_type
eq
'opera'
) {
my
(
$scrap_name
,
$scrap_definition
) =
$class
->_get_js_function_definition(
$to_browser_type
,
'scrap'
,
'return null'
);
$contents
.=
<<"_JS_";
$scrap_definition
Object.defineProperty(winProto, "g_opr", {value: {scrap: $scrap_name}, enumerable: true, configurable: true});
Object.defineProperty(winProto, "opr", {value: {}, enumerable: true, configurable: true});
_JS_
}
}
elsif
(
$to_browser_type
eq
'safari'
) {
$contents
.=
<<'_JS_';
delete navProto.oscpu;
delete navigator.webkitPersistentStorage;
delete navigator.webkitTemporaryStorage;
delete window.webkitResolveLocalFileSystemURL;
delete window.webkitMediaStream;
delete window.webkitSpeechGrammar;
_JS_
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Window.ApplePayError'
,
{
function_body
=>
q[return undefined]
}, {} );
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Window.CSSPrimitiveValue'
,
{
function_body
=>
q[return undefined]
}, {} );
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Window.Counter'
, {
function_body
=>
q[return undefined]
},
{} );
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Navigator.getStorageUpdates'
,
{
function_body
=>
q[return undefined]
}, {} );
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Window.WebKitMediaKeys'
,
{
function_body
=>
q[return undefined]
}, {} );
}
elsif
(
$to_browser_type
eq
'ie'
) {
$contents
.=
<<'_JS_';
let docProto = Object.getPrototypeOf(window.document);
delete navigator.webkitPersistentStorage;
delete navigator.webkitTemporaryStorage;
delete window.webkitResolveLocalFileSystemURL;
delete window.webkitMediaStream;
delete window.webkitSpeechGrammar;
delete window.ApplePayError;
delete window.CSSPrimitiveValue;
delete window.Counter;
delete navigator.getStorageUpdates;
delete window.WebKitMediaKeys;
Object.defineProperty(docProto, "documentMode", {value: true, writable: true, enumerable: true, configurable: true});
Object.defineProperty(navProto, "msDoNotTrack", {value: "0", writable: true, configurable: true});
Object.defineProperty(winProto, "msWriteProfilerMark", {value: {}, writable: true, configurable: true});
_JS_
}
if
(
$to_browser_type
eq
'firefox'
) {
$contents
.=
<<'_JS_';
delete navigator.webkitPersistentStorage;
delete navigator.webkitTemporaryStorage;
delete window.webkitResolveLocalFileSystemURL;
delete window.webkitMediaStream;
delete window.webkitSpeechGrammar;
delete window.ApplePayError;
delete window.CSSPrimitiveValue;
delete window.Counter;
delete navigator.getStorageUpdates;
delete window.WebKitMediaKeys;
if ("onmozfullscreenchange" in window) {
} else {
Object.defineProperty(window, "onmozfullscreenchange", {value: undefined, writable: true, configurable: true});
}
if ("mozInnerScreenX" in window) {
} else {
Object.defineProperty(window, "mozInnerScreenX", {value: 0, writable: true, configurable: true});
}
if ("CSSMozDocumentRule" in window) {
} else {
Object.defineProperty(window, "CSSMozDocumentRule", {value: undefined, writable: true, configurable: true});
}
if ("CanvasCaptureMediaStream" in window) {
} else {
Object.defineProperty(window, "CanvasCaptureMediaStream", {value: undefined, writable: true, configurable: true});
}
_JS_
}
else
{
$contents
.=
<<'_JS_';
delete navProto.buildID;
delete window.InstallTrigger;
delete navProto.mozGetUserMedia;
delete window.onmozfullscreenchange;
delete window.mozInnerScreenX;
delete window.CSSMozDocumentRule;
delete window.CanvasCaptureMediaStream;
_JS_
}
if
(
$from_browser_version
) {
$contents
.=
$class
->_browser_compat_data(
from_browser_type
=>
$from_browser_type
,
from_browser_version
=>
$from_browser_version
,
to_browser_type
=>
$to_browser_type
,
to_browser_version
=>
$to_browser_version
,
filters
=>
$parameters
{filters},
);
}
}
my
%navigator_agent_mappings
= (
to
=>
'userAgent'
,
app_version
=>
'appVersion'
,
platform
=>
'platform'
,
product
=>
'product'
,
product_sub
=>
'productSub'
,
vendor
=>
'vendor'
,
vendor_sub
=>
'vendorSub'
,
oscpu
=>
'oscpu'
,
);
foreach
my
$key
(
sort
{
$a
cmp
$b
}
keys
%navigator_agent_mappings
) {
if
(
defined
$parameters
{
$key
} ) {
my
$encoded
= URI::Escape::uri_escape(
$parameters
{
$key
} );
$contents
.=
$class
->_check_and_add_function(
$to_browser_type
,
'Navigator.'
.
$navigator_agent_mappings
{
$key
},
{
function_body
=>
qq[return decodeURIComponent(\\x27$encoded\\x27)]
,
override
=> 1
},
{}
);
}
elsif
(
$parameters
{to} ) {
$contents
.=
<<"_JS_";
delete navigator.$navigator_agent_mappings{$key};
_JS_
}
}
$contents
.=
<<'_JS_';
if (("console" in window) && ("log" in window.console)) {
console.log("Loaded Firefox::Marionette::Extension::Stealth");
}
}
_JS_
return
$contents
;
}
sub
_check_and_add_class {
my
(
$class
,
$property_name
,
$to_string_tag_allowed
) =
@_
;
my
$javascript_class
=
$property_name
;
my
$win_proto_class
=
$javascript_class
;
$win_proto_class
=~ s/^Window[.]/winProto./smx;
my
$contents
=
<<"_JS_";
if ("$javascript_class" in window) {
} else {
window.$javascript_class = class {
constructor(obj) {
for(let key of Object.keys(obj)) {
Object.defineProperty(this, key, {value: obj[key], enumerable: true, configurable: true});
}
}
_JS_
if
(
$to_string_tag_allowed
) {
$contents
.=
<<"_JS_";
get [Symbol.toStringTag]() {
return "$javascript_class";
}
_JS_
}
$contents
.=
<<"_JS_";
};
}
_JS_
return
$contents
;
}
sub
_native_code_body {
my
(
$class
,
$to_browser_type
) =
@_
;
my
$native_code_body
=
q[{]
.
q[\\]
.
'n\\x20\\x20\\x20\\x20[native code]'
.
q[\\]
.
q[n}]
;
if
( (
defined
$to_browser_type
) && (
$to_browser_type
eq
'chrome'
) ) {
$native_code_body
=
q[{ [native code]
}];
}
return
$native_code_body
;
}
sub
_get_js_function_definition {
my
(
$class
,
$to_browser_type
,
$name
,
$function_body
) =
@_
;
$_function_definition_count
+= 1;
my
$native_code_body
=
$class
->_native_code_body(
$to_browser_type
);
my
$actual_name
=
"fm_def_$_function_definition_count"
;
return
(
$actual_name
,
<<"_JS_");
let $actual_name = new Function("$function_body");
$actual_name.toString = function fm_def() { return "function ${name}() $native_code_body" };
_JS_
}
sub
_check_and_add_function {
my
(
$class
,
$to_browser_type
,
$property_name
,
$proposed_change_properties
,
$deleted_classes
)
=
@_
;
my
$javascript_class
=
$property_name
;
$javascript_class
=~ s/[.][\-_@[:alnum:]]+$//smx;
my
$function_name
=
$property_name
;
$function_name
=~ s/^.*[.]([\-_@[:alnum:]]+)$/$1/smx;
my
$parent_class
=
$javascript_class
;
if
(
$javascript_class
=~ /^(.*?)[.]/smx ) {
$parent_class
= ($1);
}
my
$contents
=
q[]
;
if
( !
$deleted_classes
->{
$javascript_class
} ) {
my
(
$definition_name
,
$function_definition
) =
$class
->_get_js_function_definition(
$to_browser_type
,
$function_name
,
$proposed_change_properties
->{function_body} );
$contents
.=
<<"_JS_";
$function_definition
_JS_
if
(
$proposed_change_properties
->{
override
} ) {
$contents
.=
<<"_JS_"; # So far, all overrides are Navigator overrides
Object.defineProperty(window.$javascript_class, "$function_name", {get: $definition_name, enumerable: true, configurable: true});
Object.defineProperty(navProto, "$function_name", {get: $definition_name, enumerable: true, configurable: true});
_JS_
}
elsif
(
$javascript_class
eq
'Window'
) {
$contents
.=
<<"_JS_";
if ("$function_name" in window) {
} else {
Object.defineProperty(window, "$function_name", {get: $definition_name, enumerable: true, configurable: true});
}
_JS_
}
else
{
$contents
.=
<<"_JS_";
if (winProto.$parent_class && winProto.$javascript_class) {
if ("$function_name" in winProto.$javascript_class) {
} else {
Object.defineProperty(winProto.$javascript_class.prototype, "$function_name", {get: $definition_name, enumerable: true, configurable: true});
}
} else if (window.$parent_class && window.$javascript_class) {
if (window.$javascript_class.prototype) {
if ("$function_name" in window.$javascript_class.prototype) {
} else {
Object.defineProperty(window.$javascript_class.prototype, "$function_name", {get: $definition_name, enumerable: true, configurable: true});
}
} else {
if ("$function_name" in window.$javascript_class) {
} else {
Object.defineProperty(window.$javascript_class, "$function_name", {get: $definition_name, enumerable: true, configurable: true});
}
}
_JS_
if
(
$javascript_class
eq
'Navigator'
) {
$contents
.=
<<"_JS_";
Object.defineProperty(navProto, "$function_name", {get: $definition_name, enumerable: true, configurable: true});
_JS_
}
elsif
(
$javascript_class
eq
'Document'
) {
$contents
.=
<<"_JS_";
Object.defineProperty(window.document, "$function_name", {get: $definition_name, enumerable: true, configurable: true});
_JS_
}
$contents
.=
<<"_JS_";
}
_JS_
}
}
return
$contents
;
}
sub
_check_and_delete_class {
my
(
$class
,
$property_name
) =
@_
;
my
$parent_class
=
$property_name
;
if
(
$property_name
=~ /^(.*?)[.]/smx ) {
(
$parent_class
) = ($1);
}
my
$contents
=
<<"_JS_";
if (winProto.$parent_class && winProto.$property_name) {
delete winProto.$property_name;
} else if (window.$parent_class && window.$property_name) {
delete window.$property_name;
}
_JS_
if
(
$property_name
eq
'SubtleCrypto'
) {
$contents
.=
<<'_JS_';
if ("crypto" in window) {
delete window["crypto"];
}
_JS_
}
else
{
my
$lc_property_name
=
lc
$property_name
;
$contents
.=
<<"_JS_";
if ("$lc_property_name" in window) {
delete window["$lc_property_name"];
}
_JS_
}
return
$contents
;
}
sub
_check_and_delete_function {
my
(
$class
,
$property_name
,
$deleted_classes
) =
@_
;
my
$contents
=
q[]
;
my
$javascript_class
=
$property_name
;
$javascript_class
=~ s/[.][\-_@[:alpha:]]+$//smx;
my
$function_name
=
$property_name
;
$function_name
=~ s/^.*[.]([\-_@[:alpha:]]+)$/$1/smx;
my
$parent_class
=
$javascript_class
;
if
(
$javascript_class
=~ /^(.*?)[.]/smx ) {
$parent_class
= ($1);
}
if
( !
$deleted_classes
->{
$javascript_class
} ) {
$contents
.=
<<"_JS_";
if (winProto.$parent_class && winProto.$javascript_class) {
delete winProto.$javascript_class\["$function_name"\];
} else if (window.$parent_class && window.$javascript_class) {
if (window.$javascript_class.prototype) {
delete window.$javascript_class.prototype\["$function_name"\];
}
delete window.$javascript_class\["$function_name"\];
}
_JS_
}
if
(
$javascript_class
eq
'Navigator'
) {
$contents
.=
<<"_JS_";
if ("$function_name" in navProto) {
delete navProto["$function_name"];
} else if ("$function_name" in navigator) {
delete navigator["$function_name"];
}
_JS_
}
elsif
(
$javascript_class
eq
'Document'
) {
$contents
.=
<<"_JS_";
if (("document" in winProto) && ("$function_name" in winProto.document)) {
delete winProto.document["$function_name"];
} else if (("document" in window) && ("$function_name" in window.document)) {
delete window.document["$function_name"];
}
_JS_
}
elsif
(
$javascript_class
eq
'Window'
) {
$contents
.=
<<"_JS_";
if ("$function_name" in winProto) {
delete winProto["$function_name"];
} else if ("$function_name" in window) {
delete window["$function_name"];
}
_JS_
}
return
$contents
;
}
sub
_read_bcd {
my
(
$class
) =
@_
;
my
%browser_properties
;
my
$bcd_path
= Firefox::Marionette::BCD_PATH();
if
( (
defined
$bcd_path
)
&& (
my
$bcd_handle
= FileHandle->new(
$bcd_path
, Fcntl::O_RDONLY() ) )
)
{
my
$bcd_contents
;
my
$result
;
while
(
$result
=
$bcd_handle
->
read
(
my
$buffer
, _BUFFER_SIZE() ) ) {
$bcd_contents
.=
$buffer
;
}
close
$bcd_handle
or Firefox::Marionette::Exception->throw(
"Failed to close '$bcd_path':$EXTENDED_OS_ERROR"
);
%browser_properties
= %{ JSON->new()->decode(
$bcd_contents
) };
}
elsif
(
$OS_ERROR
== POSIX::ENOENT() ) {
Carp::carp(
q[BCD file is not available. Please run 'build-bcd-for-firefox']
);
}
else
{
Firefox::Marionette::Exception->throw(
"Failed to open '$bcd_path' for reading:$EXTENDED_OS_ERROR"
);
}
return
%browser_properties
;
}
sub
_available_in {
my
(
$class
,
%properties
) =
@_
;
my
$available
;
my
$browser_type
=
$properties
{browser_type};
foreach
my
$proposed_change_properties
( @{
$properties
{changes} } ) {
if
(
$proposed_change_properties
->{add} ) {
if
(
$proposed_change_properties
->{add} <=
$properties
{browser_version} )
{
if
( !
$proposed_change_properties
->{pref_name} ) {
if
( !
defined
$proposed_change_properties
->{function_body} )
{
$proposed_change_properties
->{function_body} =
$_function_bodies
{
$properties
{property_name} }
||
'return null'
;
}
$available
=
$proposed_change_properties
;
}
}
}
elsif
(
defined
$proposed_change_properties
->{rm} ) {
if
(
$proposed_change_properties
->{rm} <=
$properties
{browser_version} )
{
$available
=
undef
;
}
}
}
return
$available
;
}
sub
_this_change_should_be_processed {
my
(
$class
,
$proposed_change
,
$property_name
,
$change_number
,
$filters
) =
@_
;
if
(
defined
$filters
) {
if
(
$property_name
!~ /
$filters
/smx ) {
return
0;
}
}
return
1;
}
sub
_browser_compat_data {
my
(
$class
,
%parameters
) =
@_
;
my
%browser_properties
=
$class
->_read_bcd();
my
$contents
=
q[]
;
my
%deleted_classes
;
my
$change_number
= 0;
VERSION:
foreach
my
$property_name
(
sort
{
$a
cmp
$b
}
keys
%browser_properties
) {
my
$property_object
=
$browser_properties
{
$property_name
};
my
$property_type
=
$property_object
->{type};
my
%from_properties
= (
browser_type
=>
$parameters
{from_browser_type},
browser_version
=>
$parameters
{from_browser_version},
property_type
=>
$property_type
,
property_name
=>
$property_name
,
changes
=>
$browser_properties
{
$property_name
}{browsers}
{
$parameters
{from_browser_type} },
);
my
%to_properties
= (
browser_type
=>
$parameters
{to_browser_type},
browser_version
=>
$parameters
{to_browser_version},
property_type
=>
$property_type
,
property_name
=>
$property_name
,
changes
=>
$browser_properties
{
$property_name
}{browsers}
{
$parameters
{to_browser_type} },
);
my
(
$delete_property
,
$add_property
,
$change_properties
);
if
(
my
$proposed_change_properties
=
$class
->_available_in(
%from_properties
) )
{
if
( !
$class
->_available_in(
%to_properties
) ) {
$delete_property
= 1;
}
}
else
{
if
(
my
$proposed_change_properties
=
$class
->_available_in(
%to_properties
) )
{
$add_property
= 1;
$change_properties
=
$proposed_change_properties
;
}
}
my
$to_string_tag_allowed
= 0;
if
(
defined
$browser_properties
{
'Symbol.toStringTag'
}{browsers}
{
$parameters
{to_browser_type} }[0]{add} )
{
if
(
$browser_properties
{
'Symbol.toStringTag'
}{browsers}
{
$parameters
{to_browser_type} }[0]{add} <
$parameters
{to_browser_version} )
{
$to_string_tag_allowed
= 1;
}
}
my
$change_details
= {
to_browser_type
=>
$parameters
{to_browser_type},
delete_property
=>
$delete_property
,
add_property
=>
$add_property
,
change_number
=>
$change_number
,
property_name
=>
$property_name
,
property_type
=>
$property_type
,
filters
=>
$parameters
{filters},
proposed_change_properties
=>
$change_properties
,
deleted_classes
=> \
%deleted_classes
,
to_string_tag_allowed
=>
$to_string_tag_allowed
,
};
$contents
.=
$class
->_process_change(
$change_details
);
}
return
$contents
;
}
sub
_process_change {
my
(
$class
,
$change_details
) =
@_
;
my
$contents
=
q[]
;
if
(
$change_details
->{delete_property} ) {
if
(
$change_details
->{property_type} eq
'class'
) {
my
$proposed_change
=
$class
->_check_and_delete_class(
$change_details
->{property_name} );
if
(
$class
->_this_change_should_be_processed(
$proposed_change
,
$change_details
->{property_name},
$change_details
->{change_number},
$change_details
->{filters},
)
)
{
$contents
.=
$proposed_change
;
$change_details
->{deleted_classes}
->{
$change_details
->{property_name} } = 1;
}
}
else
{
if
(
my
$proposed_change
=
$class
->_check_and_delete_function(
$change_details
->{property_name},
$change_details
->{deleted_classes}
)
)
{
if
(
$class
->_this_change_should_be_processed(
$proposed_change
,
$change_details
->{property_name},
$change_details
->{change_number},
$change_details
->{filters},
)
)
{
$contents
.=
$proposed_change
;
}
}
}
}
elsif
(
$change_details
->{add_property} ) {
if
(
$change_details
->{property_type} eq
'class'
) {
my
$proposed_change
=
$class
->_check_and_add_class(
$change_details
->{property_name},
$change_details
->{to_string_tag_allowed}
);
if
(
$class
->_this_change_should_be_processed(
$proposed_change
,
$change_details
->{property_name},
$change_details
->{change_number},
$change_details
->{filters},
)
)
{
$contents
.=
$proposed_change
;
}
}
else
{
if
(
my
$proposed_change
=
$class
->_check_and_add_function(
$change_details
->{to_browser_type},
$change_details
->{property_name},
$change_details
->{proposed_change_properties},
$change_details
->{deleted_classes}
)
)
{
if
(
$class
->_this_change_should_be_processed(
$proposed_change
,
$change_details
->{property_name},
$change_details
->{change_number},
$change_details
->{filters},
)
)
{
$contents
.=
$proposed_change
;
}
}
}
}
return
$contents
;
}
1;
Hide Show 84 lines of Pod