Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Record enter key presses in input fields #102

Merged
merged 1 commit into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this add-on will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased
### Added
- Support for Enter key in input fields.

### Changed
- Branding to ZAP by Checkmarx.

Expand Down
114 changes: 112 additions & 2 deletions source/ContentScript/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ZestStatement,
ZestStatementElementClick,
ZestStatementElementSendKeys,
ZestStatementElementSubmit,
ZestStatementLaunchBrowser,
ZestStatementSwitchToFrame,
} from '../types/zestScript/ZestStatement';
Expand All @@ -44,14 +45,36 @@ class Recorder {

isNotificationRaised = false;

async sendZestScriptToZAP(zestStatement: ZestStatement): Promise<number> {
// Enter keydown events often occur before the input change events, so we reorder them if needed
cachedSubmit?: ZestStatementElementSubmit;

// We can get duplicate events for the enter key, this allows us to dedup them
cachedTimeStamp = -1;

async sendZestScriptToZAP(
zestStatement: ZestStatement,
sendCache = true
): Promise<number> {
if (sendCache) {
this.handleCachedSubmit();
}
// console.log('Sending statement', zestStatement);
this.notify(zestStatement);
return Browser.runtime.sendMessage({
type: ZEST_SCRIPT,
data: zestStatement.toJSON(),
});
}

handleCachedSubmit(): void {
if (this.cachedSubmit) {
// console.log('Sending cached submit', this.cachedSubmit);
this.sendZestScriptToZAP(this.cachedSubmit, false);
delete this.cachedSubmit;
this.cachedTimeStamp = -1;
}
}

handleFrameSwitches(level: number, frameIndex: number): void {
if (this.curLevel === level && this.curFrame === frameIndex) {
return;
Expand Down Expand Up @@ -118,12 +141,44 @@ class Recorder {
this.handleFrameSwitches(level, frame);
console.log(event, 'change', (event.target as HTMLInputElement).value);
const elementLocator = getPath(event.target as HTMLElement, element);
// Send the keys before a cached submit statement on the same element
if (
this.cachedSubmit &&
this.cachedSubmit.elementLocator.element !== elementLocator.element
) {
// The cached submit was not on the same element, so send it
this.handleCachedSubmit();
}
this.sendZestScriptToZAP(
new ZestStatementElementSendKeys(
elementLocator,
(event.target as HTMLInputElement).value
)
),
false
);
// Now send the cached submit, if there still is one
this.handleCachedSubmit();
}

handleKeypress(
params: {level: number; frame: number; element: Document},
event: KeyboardEvent
): void {
if (!this.shouldRecord(event.target as HTMLElement)) return;
const {element} = params;
if (event.key === 'Enter') {
if (this.cachedSubmit && this.cachedTimeStamp === event.timeStamp) {
// console.log('Ignoring dup Enter event', this.cachedSubmit);
return;
}
this.handleCachedSubmit();
const elementLocator = getPath(event.target as HTMLElement, element);
// console.log('Enter key pressed', elementLocator, event.timeStamp);
// Cache the statement as it often occurs before the change event occurs
this.cachedSubmit = new ZestStatementElementSubmit(elementLocator);
this.cachedTimeStamp = event.timeStamp;
// console.log('Caching submit', this.cachedSubmit);
}
}

handleResize(): void {
Expand All @@ -140,11 +195,30 @@ class Recorder {
console.log('Window Resize : ', width, height);
}

addListenerToInputField(
elements: Set<HTMLElement>,
inputField: HTMLElement,
level: number,
frame: number,
element: Document
): void {
if (!elements.has(inputField)) {
elements.add(inputField);
inputField.addEventListener(
'keydown',
this.handleKeypress.bind(this, {level, frame, element})
);
}
}

addListenersToDocument(
element: Document,
level: number,
frame: number
): void {
// A list of all of the text elements that we have added event listeners to
const textElements = new Set<HTMLElement>();

element.addEventListener(
'click',
this.handleClick.bind(this, {level, frame, element})
Expand Down Expand Up @@ -173,6 +247,38 @@ class Recorder {
i += 1;
}
});

// Add listeners to all of the text fields
element.querySelectorAll('input').forEach((input) => {
this.addListenerToInputField(textElements, input, level, frame, element);
});
// Observer callback function to handle DOM mutations to detect added text fields
const domMutated: MutationCallback = (mutationsList: MutationRecord[]) => {
mutationsList.forEach((mutation) => {
if (mutation.type === 'childList') {
// Look for added input elements
if (mutation.target instanceof Element) {
const inputs = mutation.target.getElementsByTagName('input');
for (let j = 0; j < inputs.length; j += 1) {
this.addListenerToInputField(
textElements,
inputs[j],
level,
frame,
element
);
}
}
}
});
};

const observer = new MutationObserver(domMutated);
observer.observe(document, {
attributes: false,
childList: true,
subtree: true,
});
}

shouldRecord(element: HTMLElement): boolean {
Expand Down Expand Up @@ -221,6 +327,7 @@ class Recorder {

stopRecordingUserInteractions(): void {
console.log('Stopping Recording User Interactions ...');
this.handleCachedSubmit();
Browser.storage.sync.set({zaprecordingactive: false});
this.active = false;
const floatingDiv = document.getElementById('ZapfloatingDiv');
Expand Down Expand Up @@ -367,6 +474,9 @@ class Recorder {
} else if (stmt instanceof ZestStatementElementSendKeys) {
notifyMessage.title = 'Send Keys';
notifyMessage.message = `${stmt.elementLocator.element}: ${stmt.keys}`;
} else if (stmt instanceof ZestStatementElementSubmit) {
notifyMessage.title = 'Submit';
notifyMessage.message = `${stmt.elementLocator.element}`;
} else if (stmt instanceof ZestStatementLaunchBrowser) {
notifyMessage.title = 'Launch Browser';
notifyMessage.message = stmt.browserType;
Expand Down
45 changes: 45 additions & 0 deletions source/types/zestScript/ZestStatement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import {
ZEST_CLIENT_ELEMENT_CLICK,
ZEST_CLIENT_ELEMENT_MOUSE_OVER,
ZEST_CLIENT_ELEMENT_SEND_KEYS,
ZEST_CLIENT_ELEMENT_SUBMIT,
ZEST_CLIENT_LAUNCH,
ZEST_CLIENT_SWITCH_TO_FRAME,
ZEST_CLIENT_WINDOW_CLOSE,
ZEST_COMMENT,
} from '../../utils/constants';

class ElementLocator {
Expand Down Expand Up @@ -91,6 +93,24 @@ class ZestStatementLaunchBrowser extends ZestStatement {
}
}

class ZestComment extends ZestStatement {
comment: string;

constructor(comment: string) {
super(ZEST_COMMENT);
this.comment = comment;
}

toJSON(): string {
return JSON.stringify({
index: this.index,
enabled: true,
elementType: this.elementType,
comment: this.comment,
});
}
}

abstract class ZestStatementElement extends ZestStatement {
elementLocator: ElementLocator;

Expand Down Expand Up @@ -147,6 +167,29 @@ class ZestStatementElementSendKeys extends ZestStatementElement {
}
}

class ZestStatementElementSubmit extends ZestStatementElement {
keys: string;

constructor(
elementLocator: ElementLocator,
windowHandle = DEFAULT_WINDOW_HANDLE
) {
super(ZEST_CLIENT_ELEMENT_SUBMIT, elementLocator);
this.windowHandle = windowHandle;
}

toJSON(): string {
return JSON.stringify({
value: this.keys,
windowHandle: this.windowHandle,
...this.elementLocator.toJSON(),
index: this.index,
enabled: true,
elementType: this.elementType,
});
}
}

class ZestStatementElementClear extends ZestStatementElement {
constructor(
elementLocator: ElementLocator,
Expand Down Expand Up @@ -234,12 +277,14 @@ class ZestStatementElementMouseOver extends ZestStatementElement {

export {
ElementLocator,
ZestComment,
ZestStatement,
ZestStatementLaunchBrowser,
ZestStatementElementMouseOver,
ZestStatementElementClick,
ZestStatementSwitchToFrame,
ZestStatementElementSendKeys,
ZestStatementElementSubmit,
ZestStatementElementClear,
ZestStatementWindowClose,
};
2 changes: 2 additions & 0 deletions source/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
export const ZEST_CLIENT_SWITCH_TO_FRAME = 'ZestClientSwitchToFrame';
export const ZEST_CLIENT_ELEMENT_CLICK = 'ZestClientElementClick';
export const ZEST_CLIENT_ELEMENT_SEND_KEYS = 'ZestClientElementSendKeys';
export const ZEST_CLIENT_ELEMENT_SUBMIT = 'ZestClientElementSubmit';
export const ZEST_CLIENT_LAUNCH = 'ZestClientLaunch';
export const ZEST_CLIENT_ELEMENT_CLEAR = 'ZestClientElementClear';
export const ZEST_CLIENT_WINDOW_CLOSE = 'ZestClientWindowClose';
export const ZEST_CLIENT_ELEMENT_MOUSE_OVER = 'ZestClientMouseOverElement';
export const ZEST_COMMENT = 'ZestComment';
export const DEFAULT_WINDOW_HANDLE = 'windowHandle1';

export const ZAP_STOP_RECORDING = 'zapStopRecording';
Expand Down
22 changes: 22 additions & 0 deletions test/ContentScript/integrationTests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,28 @@ function integrationTests(
expect(JSON.stringify(Array.from(actualData))).toBe(expectedData);
});

test('Should record return key', async () => {
// Given / When
server = getFakeZapServer(actualData, _JSONPORT);
const context = await driver.getContext(_JSONPORT, true);
await driver.setEnable(false);
const page = await context.newPage();
await page.goto(
`http://localhost:${_HTTPPORT}/webpages/interactions.html`
);
await page.type('#input-1', 'testinput');
await page.keyboard.press('Enter');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await page.close();
// Then
const expectedData =
'["{\\"action\\":{\\"action\\":\\"reportZestStatement\\"},\\"body\\":{\\"statementJson\\":\\"{\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"input-1\\",\\"index\\":1,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementClear\\"}\\",\\"apikey\\":\\"not set\\"}}",' +
'"{\\"action\\":{\\"action\\":\\"reportZestStatement\\"},\\"body\\":{\\"statementJson\\":\\"{\\"value\\":\\"testinput\\",\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"input-1\\",\\"index\\":2,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementSendKeys\\"}\\",\\"apikey\\":\\"not set\\"}}",' +
'"{\\"action\\":{\\"action\\":\\"reportZestStatement\\"},\\"body\\":{\\"statementJson\\":\\"{\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"input-1\\",\\"index\\":3,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementSubmit\\"}\\",\\"apikey\\":\\"not set\\"}}"]';
expect(JSON.stringify(Array.from(actualData))).toBe(expectedData);
});

test('Should stop recording', async () => {
// Given / When
server = getFakeZapServer(actualData, _JSONPORT);
Expand Down