Skip to content

Commit

Permalink
Merge pull request #102 from psiinon/record/enter
Browse files Browse the repository at this point in the history
Record enter key presses in input fields
  • Loading branch information
kingthorin authored Nov 28, 2024
2 parents cba7e06 + ab2ddb8 commit 0abdd66
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 2 deletions.
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

0 comments on commit 0abdd66

Please sign in to comment.