Skip to content
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "snyk-api-and-web-record-sequence",
"version": "1.1.0",
"version": "1.2.0",
"description": "Snyk API & Web Record login/sequence",
"license": "MIT",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src/manifest-firefox.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Snyk API & Web Sequence Recorder",
"version": "1.1.0",
"version": "1.2.0",
"browser_specific_settings": {
"gecko": {
"id": "sequence-recorder@probely.com",
Expand Down
116 changes: 111 additions & 5 deletions src/pages/Content/modules/collectEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,59 @@ const lastNodes = {
};

let stoMouseover = false;
let pendingInput = null;
let flushedInput = null;
const keystrokeBuffers = new WeakMap();

function getEffectiveValue(element) {
const domValue = element.value;
if (domValue && domValue.trim()) {
return domValue;
}
return keystrokeBuffers.get(element) || domValue;
}

function isBufferedValue(element) {
const domValue = element.value;
return !(domValue && domValue.trim()) && keystrokeBuffers.has(element);
}

function isCanvasSurface(element) {
if (element.nodeName.toLowerCase() === 'canvas') return true;
if (element.shadowRoot) {
try {
if (element.shadowRoot.querySelector('canvas')) return true;
} catch (ex) { /* ignore */ }
}
for (const child of element.children || []) {
if (child.shadowRoot) {
try {
if (child.shadowRoot.querySelector('canvas')) return true;
} catch (ex) { /* ignore */ }
}
}
return false;
}

function getStableInputSelector(element, doc) {
const dynamicPatterns = ['focus', 'highlight', 'editable', 'caret'];
const classes = Array.from(element.classList || []).filter((cls) => {
const lower = cls.toLowerCase();
return !dynamicPatterns.some((p) => lower.includes(p)) && !/^[0-9]/.test(cls);
});
for (const cls of classes) {
const sel = '.' + cls;
try {
const matches = doc.querySelectorAll(sel);
if (matches.length === 1 && matches[0] === element) {
return sel;
}
} catch (ex) {
// ignore
}
}
return null;
}

export function interceptEvents(event, doc, ifrSelector, callback) {
let hasKeyReturn = false;
Expand Down Expand Up @@ -110,6 +163,26 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
if (type === 'click') {
lastNodes.click = tgt;

if (pendingInput && pendingInput.element !== tgt && callback) {
const useBuffer = isBufferedValue(pendingInput.element);
const fillEvent = {
...pendingInput.oEventBase,
type: useBuffer ? 'bfill_value' : 'fill_value',
value: getEffectiveValue(pendingInput.element),
frame: pendingInput.frame,
};
if (useBuffer) {
const stableSel = getStableInputSelector(pendingInput.element, doc);
if (stableSel) {
fillEvent.css = stableSel;
}
}
callback({ messageType: 'events', event: { ...fillEvent } });
keystrokeBuffers.delete(pendingInput.element);
flushedInput = pendingInput.element;
pendingInput = null;
}

if (
lastNodes.return === lastNodes.change &&
tgt !== lastNodes.return &&
Expand All @@ -134,7 +207,7 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
}
}
let typeStr = 'click';
if (nodeName === 'canvas') {
if (isCanvasSurface(tgt)) {
const rect = tgt.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
Expand Down Expand Up @@ -261,6 +334,16 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
lastNodes.keydown = tgt;
if (['input', 'textarea'].indexOf(nodeName) > -1) {
oNodes[tgt] = tgt.value;
pendingInput = { element: tgt, oEventBase: { ...oEventBase }, frame: ifrSelector };
if (event.key && event.key.length === 1) {
const buf = keystrokeBuffers.get(tgt) || '';
keystrokeBuffers.set(tgt, buf + event.key);
} else if (event.key === 'Backspace') {
const buf = keystrokeBuffers.get(tgt) || '';
if (buf.length > 0) {
keystrokeBuffers.set(tgt, buf.slice(0, -1));
}
}
}
if (
nodeName === 'input' &&
Expand All @@ -271,15 +354,30 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
hasKeyReturn = true;
// lastSelectorWithReturn = selector;
lastNodes.return = tgt;
const useBuffer = isBufferedValue(tgt);
oEventToSend = {
...oEventBase,
type: 'fill_value',
value: tgt.value,
type: useBuffer ? 'bfill_value' : 'fill_value',
value: getEffectiveValue(tgt),
frame: ifrSelector,
};
if (useBuffer) {
const stableSel = getStableInputSelector(tgt, doc);
if (stableSel) {
oEventToSend.css = stableSel;
}
}
keystrokeBuffers.delete(tgt);
}
} else if (type === 'blur') {
lastNodes.blur = tgt;
if (tgt === flushedInput) {
flushedInput = null;
return;
}
if (pendingInput && pendingInput.element === tgt) {
pendingInput = null;
}
if (['input', 'textarea'].indexOf(nodeName) > -1) {
oNodes[tgt] = tgt.value;
if (tgt === lastNodes.return) {
Expand All @@ -292,12 +390,20 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
) {
return;
}
const useBuffer = isBufferedValue(tgt);
oEventToSend = {
...oEventBase,
type: 'fill_value',
value: tgt.value,
type: useBuffer ? 'bfill_value' : 'fill_value',
value: getEffectiveValue(tgt),
frame: ifrSelector,
};
if (useBuffer) {
const stableSel = getStableInputSelector(tgt, doc);
if (stableSel) {
oEventToSend.css = stableSel;
}
}
keystrokeBuffers.delete(tgt);
}
} else if (type === 'change') {
lastNodes.change = tgt;
Expand Down
52 changes: 26 additions & 26 deletions src/pages/Popup/Popup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,30 @@ import logo from '../../assets/img/logo_probely.svg';
import help from '../../assets/img/help.svg';
import './Popup.css';

const helpURL = 'https://help.probely.com/en/articles/5402869-how-to-record-a-sequence-with-probely-s-sequence-recorder-plugin';
const helpURL = 'https://docs.snyk.io/scan-fix-and-prevent/scan-with-snyk/snyk-api-web/configure-targets/configure-web-targets/use-sequence-recorder';

const Popup = (props) => {
// 🔴
// console.log('PROPS :: ', props);
const [isRecording, setIsRecording] = useState(false);
const [startURL, setStartURL] = useState('');
const [recordingData, setRecordingData] = useState([]);
const [copyStatus, setCopyStatus] = useState({status: false, error: false, msg: 'Successfully copied to clipboard'});
const [copyStatus, setCopyStatus] = useState({ status: false, error: false, msg: 'Successfully copied to clipboard' });

useEffect(() => {
if (chrome && chrome.storage) {
chrome.storage.sync.get(['isRecording'], (data) => {
const recording = data.isRecording;
if(recording) {
if (recording) {
setIsRecording(true);
(chrome.action || chrome.browserAction).setBadgeText({
text: '🔴',
}, () => {});
}, () => { });
} else {
setIsRecording(false);
(chrome.action || chrome.browserAction).setBadgeText({
text: '',
}, () => {});
}, () => { });
}
});
chrome.runtime.onMessage.addListener((data, sender, sendResponse) => {
Expand All @@ -51,8 +51,8 @@ const Popup = (props) => {
if (chrome) {
(chrome.action || chrome.browserAction).setBadgeText({
text: '🔴',
}, () => {});
chrome.storage.sync.set({isRecording: true}, () => {
}, () => { });
chrome.storage.sync.set({ isRecording: true }, () => {
chrome.runtime.sendMessage({
messageType: 'start',
event: {
Expand All @@ -63,7 +63,7 @@ const Popup = (props) => {
url: startURL,
},
});
chrome.tabs.create({active: true, url: startURL}, (aa) => {
chrome.tabs.create({ active: true, url: startURL }, (aa) => {
});
});
}
Expand All @@ -74,15 +74,15 @@ const Popup = (props) => {
if (chrome) {
(chrome.action || chrome.browserAction).setBadgeText({
text: '',
}, () => {});
chrome.storage.sync.set({isRecording: false}, () => {
}, () => { });
chrome.storage.sync.set({ isRecording: false }, () => {
askForRecordingData();

chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (tabs && tabs.length) {
const curTab = tabs[0];
chrome.tabs.remove(curTab.id);
chrome.tabs.create({active: true, url: './review.html'}, (aa) => {
chrome.tabs.create({ active: true, url: './review.html' }, (aa) => {
});
}
});
Expand Down Expand Up @@ -126,7 +126,7 @@ const Popup = (props) => {
msg: 'Successfully copied to clipboard'
});
setTimeout(() => {
setCopyStatus({status: false, error: false, msg: ''});
setCopyStatus({ status: false, error: false, msg: '' });
}, 3000);
} else {
setCopyStatus({
Expand All @@ -135,7 +135,7 @@ const Popup = (props) => {
msg: 'Error on copy to clipboard'
});
setTimeout(() => {
setCopyStatus({status: false, error: false, msg: ''});
setCopyStatus({ status: false, error: false, msg: '' });
}, 5000);
}
}
Expand All @@ -144,7 +144,7 @@ const Popup = (props) => {
function onClickDownload() {
var blob = new Blob([JSON.stringify(recordingData, null, 2)], {
type: "text/plain;charset=utf-8"
});
});
var a = document.createElement('a');
a.download = 'snyk-api-and-web-recording.json';
a.rel = 'noopener';
Expand All @@ -169,7 +169,7 @@ const Popup = (props) => {

function onClickHelpLink(ev) {
ev.preventDefault();
chrome.tabs.create({active: true, url: helpURL}, (aa) => {
chrome.tabs.create({ active: true, url: helpURL }, (aa) => {
});
}

Expand All @@ -181,8 +181,8 @@ const Popup = (props) => {
</header>
<div className="App-container">
<p>
Use this plugin to record a sequence of steps to be followed by Snyk API & Web during a scan.{' '}
When you finish recording, upload the script to your target settings.
Use this plugin to record a sequence of steps to be followed by Snyk API & Web during a scan.{' '}
When you finish recording, upload the script to your target settings.
</p>
<p className="help-container">
<a
Expand All @@ -202,7 +202,7 @@ const Popup = (props) => {
<div className="input-url-container">
{isRecording ?
null
:
:
<>
<label className="start_url_label" htmlFor="start_url">Type the start URL to be recorded</label>
<input
Expand All @@ -221,14 +221,14 @@ const Popup = (props) => {
}
</div>
<div className="buttons-container">
{isRecording ?
{isRecording ?
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<p><a
href="#"
className="App-button"
onClick={(ev) => { onClickStartStopRecording(ev, false); }}
>Stop recording</a></p>
:
:
<button
type="submit"
className="App-button"
Expand Down Expand Up @@ -260,15 +260,15 @@ const Popup = (props) => {
>Clear recording data</button>
</div>
</>
: null}
: null}
<div className="copy-status-container">
{copyStatus.status ?
<div className={copyStatus.error ? 'copy-status error' : 'copy-status success'}>{copyStatus.msg}</div>
: null}
<div className={copyStatus.error ? 'copy-status error' : 'copy-status success'}>{copyStatus.msg}</div>
: null}
</div>
{recordingData.length ?
{recordingData.length ?
<textarea id="input-copy-to-clipboard" defaultValue={JSON.stringify(recordingData, null, 2)}></textarea>
: null}
: null}
</div>
);
};
Expand Down
Loading
Loading