Skip to content

Commit

Permalink
Merge pull request #46 from buttercup/feat/input_filtering
Browse files Browse the repository at this point in the history
Input filtering and improved value setting
  • Loading branch information
perry-mitchell authored Apr 6, 2024
2 parents cb2c44e + 1fa60c7 commit b9cddd2
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 29 deletions.
13 changes: 7 additions & 6 deletions source/LocustInputEvent.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
export type InputEventTrigger = "keypress" | "fill";

export class LocustInputEvent extends InputEvent {
private _data: string;
private _source: InputEventTrigger;

constructor(
source: InputEventTrigger,
type: string,
data: string,
eventInitDict?: EventInit
eventInitDict?: InputEventInit
) {
super(type, eventInitDict);
this._source = source;
this._data = data;
}

get data(): string {
return this._data;
/**
* React 15 compat
* @see https://chuckconway.com/changing-a-react-input-value-from-vanilla-javascript/
*/
get simulated(): boolean {
return true;
}

get source(): InputEventTrigger {
Expand Down
2 changes: 1 addition & 1 deletion source/LoginTarget.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import isVisible from "is-visible";
import EventEmitter from "eventemitter3";
import { getSharedObserver as getUnloadObserver } from "./UnloadObserver.js";
import { LoginTargetFeature } from "./types.js";
import { LocustInputEvent } from "./LocustInputEvent.js";
import { typeIntoInput } from "./typing.js";
import { LoginTargetFeature } from "./types.js";

interface ChangeListener {
input: HTMLElement;
Expand Down
34 changes: 21 additions & 13 deletions source/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SUBMIT_BUTTON_QUERIES,
USERNAME_QUERIES
} from "./inputPatterns.js";
import { ElementValidatorCallback, LoginTargetFeature } from "./types.js";

export interface FetchedForm {
form: HTMLFormElement | HTMLDivElement;
Expand Down Expand Up @@ -71,11 +72,14 @@ function fetchForms(queryEl: Document | HTMLElement = document): Array<HTMLFormE
return Array.prototype.slice.call(queryEl.querySelectorAll(FORM_QUERIES.join(",")));
}

export function fetchFormsWithInputs(queryEl: Document | HTMLElement = document): Array<FetchedForm> {
export function fetchFormsWithInputs(
validator: ElementValidatorCallback,
queryEl: Document | HTMLElement = document
): Array<FetchedForm> {
return fetchForms(queryEl).reduce((output: Array<FetchedForm>, formEl: HTMLFormElement | HTMLDivElement) => {
let usernameFields = fetchUsernameInputs(formEl);
const passwordFields = fetchPasswordInputs(formEl);
const otpFields = fetchOTPInputs(formEl);
let usernameFields = fetchUsernameInputs(validator, formEl);
const passwordFields = fetchPasswordInputs(validator, formEl);
const otpFields = fetchOTPInputs(validator, formEl);
if (otpFields.length > 0 && passwordFields.length === 0) {
// No password fields, so filter out any OTP fields from the potential username fields
usernameFields = usernameFields.filter(field => otpFields.includes(field) === false);
Expand All @@ -85,7 +89,7 @@ export function fetchFormsWithInputs(queryEl: Document | HTMLElement = document)
usernameFields,
passwordFields,
otpFields,
submitButtons: fetchSubmitButtons(formEl)
submitButtons: fetchSubmitButtons(validator, formEl)
};
if (form.usernameFields.length <= 0 && otpFields.length <= 0 && passwordFields.length <= 0) {
return output;
Expand All @@ -94,27 +98,31 @@ export function fetchFormsWithInputs(queryEl: Document | HTMLElement = document)
}, []);
}

function fetchOTPInputs(queryEl: Document | HTMLElement = document): Array<HTMLInputElement> {
function fetchOTPInputs(validator: ElementValidatorCallback, queryEl: Document | HTMLElement = document): Array<HTMLInputElement> {
const megaQuery = OTP_QUERIES.join(", ");
const inputs = Array.prototype.slice.call(queryEl.querySelectorAll(megaQuery)).filter((el: Element) => isInput(el)) as Array<HTMLInputElement>;
let inputs = Array.prototype.slice.call(queryEl.querySelectorAll(megaQuery)).filter((el: Element) => isInput(el)) as Array<HTMLInputElement>;
inputs = inputs.filter(input => validator(LoginTargetFeature.OTP, input));
return sortFormElements(inputs, "otp");
}

function fetchPasswordInputs(queryEl: Document | HTMLElement = document): Array<HTMLInputElement> {
function fetchPasswordInputs(validator: ElementValidatorCallback, queryEl: Document | HTMLElement = document): Array<HTMLInputElement> {
const megaQuery = PASSWORD_QUERIES.join(", ");
const inputs = Array.prototype.slice.call(queryEl.querySelectorAll(megaQuery)).filter((el: Element) => isInput(el)) as Array<HTMLInputElement>;
let inputs = Array.prototype.slice.call(queryEl.querySelectorAll(megaQuery)).filter((el: Element) => isInput(el)) as Array<HTMLInputElement>;
inputs = inputs.filter(input => validator(LoginTargetFeature.Password, input));
return sortFormElements(inputs, "password");
}

function fetchSubmitButtons(queryEl: Document | HTMLElement = document) {
function fetchSubmitButtons(validator: ElementValidatorCallback, queryEl: Document | HTMLElement = document) {
const megaQuery = SUBMIT_BUTTON_QUERIES.join(", ");
const inputs = Array.prototype.slice.call(queryEl.querySelectorAll(megaQuery));
let inputs = Array.prototype.slice.call(queryEl.querySelectorAll(megaQuery)) as Array<HTMLElement>;
inputs = inputs.filter(input => validator(LoginTargetFeature.Submit, input));
return sortFormElements(inputs, "submit");
}

function fetchUsernameInputs(queryEl: Document | HTMLElement = document): Array<HTMLInputElement> {
function fetchUsernameInputs(validator: ElementValidatorCallback, queryEl: Document | HTMLElement = document): Array<HTMLInputElement> {
const megaQuery = USERNAME_QUERIES.join(", ");
const inputs = Array.prototype.slice.call(queryEl.querySelectorAll(megaQuery)).filter((el: Element) => isInput(el)) as Array<HTMLInputElement>;
let inputs = Array.prototype.slice.call(queryEl.querySelectorAll(megaQuery)).filter((el: Element) => isInput(el)) as Array<HTMLInputElement>;
inputs = inputs.filter(input => validator(LoginTargetFeature.Username, input));
return sortFormElements(inputs, "username");
}

Expand Down
18 changes: 14 additions & 4 deletions source/loginTargets.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { fetchFormsWithInputs } from "./inputs.js";
import { LoginTarget } from "./LoginTarget.js";
import { revealShySubmitButtons } from "./prepare.js";
import { ElementValidatorCallback } from "./types.js";

const DEFAULT_VALIDATOR: ElementValidatorCallback = () => true;

/**
* Get the best login target on the current page
* @param queryEl The element to query within
* @returns A login target or null of none found
* @see getLoginTargets
*/
export function getLoginTarget(queryEl: Document | HTMLElement = document): LoginTarget | null {
export function getLoginTarget(
queryEl: Document | HTMLElement = document,
validator: ElementValidatorCallback = DEFAULT_VALIDATOR
): LoginTarget | null {
revealShySubmitButtons(queryEl);
const targets = getLoginTargets(queryEl);
const targets = getLoginTargets(queryEl, validator);
let bestScore = -9999,
bestTarget = null;
targets.forEach((target) => {
Expand All @@ -31,11 +37,15 @@ export function getLoginTarget(queryEl: Document | HTMLElement = document): Logi
* @param queryEl The element to query within
* @returns An array of login targets
*/
export function getLoginTargets(queryEl: Document | HTMLElement = document): Array<LoginTarget> {
export function getLoginTargets(
queryEl: Document | HTMLElement = document,
validator: ElementValidatorCallback = DEFAULT_VALIDATOR
): Array<LoginTarget> {
revealShySubmitButtons(queryEl);
return fetchFormsWithInputs(queryEl).map((info) => {
return fetchFormsWithInputs(validator, queryEl).map((info) => {
const { form, otpFields, usernameFields, passwordFields, submitButtons } = info;
const target = new LoginTarget();
// Set inputs to target - this attaches listeners
target.usernameField = usernameFields[0];
target.passwordField = passwordFields[0];
target.otpField = otpFields[0];
Expand Down
2 changes: 2 additions & 0 deletions source/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type ElementValidatorCallback = (feature: LoginTargetFeature, element: HTMLElement) => boolean;

export enum LoginTargetFeature {
Form = "form",
OTP = "otp",
Expand Down
16 changes: 14 additions & 2 deletions source/typing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,22 @@ export async function typeIntoInput(input: HTMLInputElement, value: string): Pro
while (characters.length > 0) {
const char = characters.shift();
newValue = `${newValue}${char}`;
// Set using attribute
input.setAttribute("value", newValue);
// Set using native methods
const proto = Object.getPrototypeOf(input);
const protoSetter = Object.getOwnPropertyDescriptor(proto, "value").set;
nativeInputValueSetter.call(input, newValue);
if (protoSetter && nativeInputValueSetter !== protoSetter) {
protoSetter.call(input, newValue);
}
// Try react set (React 16)
const tracker = (input as any)._valueTracker;
if (tracker) {
tracker.setValue(newValue);
}
// Input event data takes the single new character
const inputEvent = new LocustInputEvent("fill", "input", char, { bubbles: true });
const inputEvent = new LocustInputEvent("fill", "input", { bubbles: true, data: char });
input.dispatchEvent(inputEvent);
// Wait
const waitTime = Math.floor(Math.random() * (MAX_TYPE_WAIT - MIN_TYPE_WAIT)) + MIN_TYPE_WAIT;
Expand All @@ -39,6 +51,6 @@ export async function typeIntoInput(input: HTMLInputElement, value: string): Pro
const blurEvent = new FocusEvent("blur", { bubbles: true });
input.dispatchEvent(blurEvent);
// The change event gets all of the new data
const changeEvent = new LocustInputEvent("fill", "change", value, { bubbles: true });
const changeEvent = new LocustInputEvent("fill", "change", { bubbles: true, data: value });
input.dispatchEvent(changeEvent);
}
6 changes: 3 additions & 3 deletions test/inputs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("inputs", function () {
});

it("fetches forms by name", function () {
fetchFormsWithInputs(this.queryEl);
fetchFormsWithInputs(() => true, this.queryEl);
expect(this.queryEl.querySelectorAll.calledWithExactly(FORM_QUERIES.join(","))).to.be
.true;
expect(this.queryEl.querySelectorAll.calledOnce).to.be.true;
Expand All @@ -28,7 +28,7 @@ describe("inputs", function () {
tagName: "form"
};
this.forms.push(fakeForm);
fetchFormsWithInputs(this.queryEl);
fetchFormsWithInputs(() => true, this.queryEl);
expect(fakeForm.querySelectorAll.callCount).to.be.at.least(3);
});

Expand All @@ -44,7 +44,7 @@ describe("inputs", function () {
tagName: "form"
};
this.forms.push(fakeForm);
const forms = fetchFormsWithInputs(this.queryEl);
const forms = fetchFormsWithInputs(() => true, this.queryEl);
expect(forms).to.have.lengthOf(0);
});
});
Expand Down

0 comments on commit b9cddd2

Please sign in to comment.