You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

284 lines
9.1 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
/** 逆向工程辅助Google Places Autocomplete 探测headed 调试用) */
const DEFAULT_VERDICT: ReconVerdict = {
googleMapsLoaded: false,
triggerAvailable: false,
autocompleteAccessPath: "not-found",
pacTargetInputSelector: "",
reactFiberKey: "not-found",
widgetComponentName: "unknown",
widgetStateStructure: {},
cspRestrictions: "none",
placeChangedTriggerOk: false,
verdict: "PLAN_B",
};
/** 读取 headed 逆向产物;缺失时默认 PLAN_B自绘联想路径 */
export function loadReconVerdict(): ReconVerdict {
const path = join(process.cwd(), ".rpa", "recon", "verdict.json");
if (!existsSync(path)) {
return DEFAULT_VERDICT;
}
try {
return { ...DEFAULT_VERDICT, ...JSON.parse(readFileSync(path, "utf8")) };
} catch {
return DEFAULT_VERDICT;
}
}
export const AUTOCOMPLETE_MONKEY_PATCH_INIT = `(() => {
if (window.__rpaAutocompletePatched) return;
window.__rpaAutocompletePatched = true;
window.__rpaAutocompleteInstances = window.__rpaAutocompleteInstances || [];
window.__rpaPlaceChangedHandlers = window.__rpaPlaceChangedHandlers || [];
const waitForGoogle = () => {
if (typeof google === "undefined" || !google.maps || !google.maps.places || !google.maps.places.Autocomplete) {
setTimeout(waitForGoogle, 200);
return;
}
const orig = google.maps.places.Autocomplete;
google.maps.places.Autocomplete = function (...args) {
const inputEl = args[0] || null;
const instance = new orig(...args);
window.__rpaAutocompleteInstances.push(instance);
if (inputEl) {
instance.__rpaInputEl = inputEl;
}
if (typeof instance.addListener === "function") {
const origAddListener = instance.addListener.bind(instance);
instance.addListener = function(eventName, handler) {
if (eventName === "place_changed" && typeof handler === "function") {
window.__rpaPlaceChangedHandlers.push({
instance: instance,
handler: handler,
inputEl: inputEl,
});
}
return origAddListener(eventName, handler);
};
}
return instance;
};
google.maps.places.Autocomplete.prototype = orig.prototype;
};
waitForGoogle();
})();`;
export type ReconVerdict = {
googleMapsLoaded: boolean;
triggerAvailable: boolean;
autocompleteAccessPath: string;
pacTargetInputSelector: string;
reactFiberKey: string;
widgetComponentName: string;
widgetStateStructure: Record<string, unknown>;
cspRestrictions: string;
placeChangedTriggerOk: boolean;
verdict: "PLAN_A" | "PLAN_B";
};
export const RUN_RECON_PROBE = `() => {
try {
const result = {
googleMapsLoaded: typeof google !== "undefined" && typeof google.maps !== "undefined",
triggerAvailable: typeof google?.maps?.event?.trigger === "function",
placesLoaded: typeof google?.maps?.places?.Autocomplete === "function",
globalAutocompleteVars: [],
inputProbes: [],
pacTargetInputSelector: "",
autocompleteAccessPath: "not-found",
autocompleteInstanceFound: false,
reactFiberKey: "not-found",
widgetComponentName: "unknown",
widgetStateStructure: {},
cspRestrictions: "none",
placeChangedTriggerOk: false,
};
if (result.googleMapsLoaded) {
try {
result.globalAutocompleteVars = Object.keys(window).filter((k) => {
try {
return window[k] instanceof google.maps.places.Autocomplete;
} catch {
return false;
}
});
} catch {
result.globalAutocompleteVars = [];
}
}
const widget = document.querySelector("#react-quoter-landing-plugin");
if (widget) {
const fiberKey = Object.keys(widget).find(
(k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"),
);
if (fiberKey) {
result.reactFiberKey = fiberKey;
let fiber = widget[fiberKey];
const components = [];
while (fiber) {
if (fiber.stateNode && typeof fiber.stateNode.setState === "function") {
const state = fiber.stateNode.state || {};
const stateKeys = Object.keys(state);
if (stateKeys.some((k) => /pickup|delivery|address/i.test(k))) {
result.widgetComponentName =
fiber.type?.name || fiber.type?.displayName || "unknown";
result.widgetStateStructure = Object.fromEntries(
stateKeys
.filter((k) => /pickup|delivery|address/i.test(k))
.map((k) => [k, typeof state[k]]),
);
}
components.push({
name: fiber.type?.name || fiber.type?.displayName || "unknown",
stateKeys,
});
}
fiber = fiber.return;
}
}
}
const inputs = widget
? Array.from(widget.querySelectorAll('input[type="text"]'))
: Array.from(document.querySelectorAll('input[type="text"]'));
for (let i = 0; i < inputs.length; i += 1) {
const input = inputs[i];
const gm = Object.keys(input).filter((k) => k.startsWith("gm_"));
const pac = input.classList.contains("pac-target-input");
result.inputProbes.push({
index: i,
gmKeys: gm.length,
pacTarget: pac,
placeholder: input.placeholder || "",
});
if (pac && !result.pacTargetInputSelector) {
result.pacTargetInputSelector = ".pac-target-input";
}
}
if (!result.pacTargetInputSelector && inputs.length > 0) {
result.pacTargetInputSelector = "#react-quoter-landing-plugin input";
}
const resolveInstance = () => {
const instances = window.__rpaAutocompleteInstances || [];
if (instances.length > 0) {
result.autocompleteAccessPath = "window.__rpaAutocompleteInstances";
return instances[instances.length - 1];
}
if (result.globalAutocompleteVars.length > 0) {
result.autocompleteAccessPath = "window." + result.globalAutocompleteVars[0];
return window[result.globalAutocompleteVars[0]];
}
const pacInput =
document.querySelector(".pac-target-input") ||
document.querySelector("#react-quoter-landing-plugin input");
if (pacInput) {
for (const key of Object.keys(pacInput)) {
if (!key.startsWith("gm_")) continue;
const val = pacInput[key];
if (val && typeof val.getPlace === "function") {
result.autocompleteAccessPath = "input[gm_*].getPlace";
return val;
}
}
}
return null;
};
const instance = resolveInstance();
result.autocompleteInstanceFound = Boolean(instance);
const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
result.cspRestrictions = meta?.content ?? "none";
if (
instance &&
result.triggerAvailable &&
typeof instance.getPlace === "function"
) {
try {
const originalGetPlace = instance.getPlace.bind(instance);
instance.getPlace = () => ({
place_id: "ChIJReconProbePlaceId0000000",
formatted_address: "1234 Recon Probe St, Los Angeles, CA",
name: "1234 Recon Probe St",
geometry: { location: { lat: () => 34.05, lng: () => -118.25 } },
});
google.maps.event.trigger(instance, "place_changed");
result.placeChangedTriggerOk = true;
setTimeout(() => {
instance.getPlace = originalGetPlace;
}, 50);
} catch {
result.placeChangedTriggerOk = false;
}
}
return result;
} catch (error) {
return {
googleMapsLoaded: false,
triggerAvailable: false,
placesLoaded: false,
globalAutocompleteVars: [],
inputProbes: [],
pacTargetInputSelector: "",
autocompleteAccessPath: "not-found",
autocompleteInstanceFound: false,
reactFiberKey: "not-found",
widgetComponentName: "unknown",
widgetStateStructure: {},
cspRestrictions: "none",
placeChangedTriggerOk: false,
probeError: String(error),
};
}
}`;
export function buildVerdict(
probe: {
googleMapsLoaded: boolean;
triggerAvailable: boolean;
placesLoaded: boolean;
autocompleteAccessPath: string;
pacTargetInputSelector: string;
reactFiberKey: string;
widgetComponentName: string;
widgetStateStructure: Record<string, unknown>;
cspRestrictions: string;
autocompleteInstanceFound: boolean;
placeChangedTriggerOk: boolean;
},
): ReconVerdict {
const r2Pass =
probe.googleMapsLoaded && probe.triggerAvailable && probe.placesLoaded;
const r3Pass = probe.autocompleteInstanceFound;
const r4Pass = probe.placeChangedTriggerOk;
let verdict: "PLAN_A" | "PLAN_B" = "PLAN_A";
if (!r2Pass || !r3Pass || !r4Pass) {
verdict = "PLAN_B";
}
return {
googleMapsLoaded: probe.googleMapsLoaded,
triggerAvailable: probe.triggerAvailable,
autocompleteAccessPath: probe.autocompleteAccessPath,
pacTargetInputSelector: probe.pacTargetInputSelector,
reactFiberKey: probe.reactFiberKey,
widgetComponentName: probe.widgetComponentName,
widgetStateStructure: probe.widgetStateStructure,
cspRestrictions: probe.cspRestrictions,
placeChangedTriggerOk: probe.placeChangedTriggerOk,
verdict,
};
}