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
5 changes: 3 additions & 2 deletions src/GleapAdminManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { loadIcon } from './UI';
import GleapAdminHelper from './GleapAdminHelper';
import Gleap, { GleapModalManager, GleapProductTours, GleapBannerManager } from './Gleap';
import { isGleapAdminOrigin, resolveGleapAdminOrigin } from './GleapAdminOrigins';
import { querySelectorSafe } from './GleapSafeSelector';

export default class GleapAdminManager {
libraryInstance = null;
Expand Down Expand Up @@ -158,7 +159,7 @@ export default class GleapAdminManager {

// Find the element and highlight it.
if (data?.data?.selector) {
const element = document.querySelector(data?.data?.selector);
const element = querySelectorSafe(data?.data?.selector);
if (element) {
element.classList.add('gleap-admin-highlight');
}
Expand Down Expand Up @@ -202,7 +203,7 @@ export default class GleapAdminManager {

if (data.name === 'click') {
try {
document.querySelector(data.data.selector).click();
querySelectorSafe(data.data.selector)?.click();
} catch (e) {
console.log(e);
}
Expand Down
5 changes: 3 additions & 2 deletions src/GleapCopilotTours.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GleapConfigManager, GleapTranslationManager } from './Gleap';
import { applyGleapCSPNonce } from './GleapHelper';
import { typeIntoElement } from './GleapInputFiller';
import { querySelectorSafe } from './GleapSafeSelector';
import { calculateContrast, loadIcon } from './UI';

const localStorageKey = 'gleap-tour-data';
Expand Down Expand Up @@ -90,7 +91,7 @@ function waitForElement(selector, timeout = 5000) {
let attempts = 0;
return new Promise((resolve, reject) => {
const interval = setInterval(() => {
const element = document.querySelector(selector);
const element = querySelectorSafe(selector);
if (element) {
clearInterval(interval);
resolve(element);
Expand Down Expand Up @@ -204,7 +205,7 @@ export default class GleapCopilotTours {
const steps = self.productTourData.steps;
const currentStep = steps[self.currentActiveIndex];
if (currentStep && currentStep.selector && currentStep.selector !== '') {
self.updatePointerPosition(document.querySelector(currentStep.selector), currentStep);
self.updatePointerPosition(querySelectorSafe(currentStep.selector), currentStep);
}
}
});
Expand Down
68 changes: 68 additions & 0 deletions src/GleapSafeSelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// `data-pf="x"` typed into the selector field instead of `[data-pf="x"]`.
const BARE_ATTRIBUTE_SELECTOR = /^[A-Za-z_][\w-]*\s*[~|^$*]?=\s*(?:"[^"]*"|'[^']*'|[^\s"'\]]+)$/;

/**
* Selectors a stored tour step may hold, most literal first.
*
* Steps can carry hand typed text, so besides the stored string this tries the
* two mistakes seen in the wild: an attribute without its brackets and an id
* containing unescaped colons.
*/
const selectorVariants = (selector) => {
const trimmed = selector.trim();
const variants = [trimmed];
if (BARE_ATTRIBUTE_SELECTOR.test(trimmed)) {
variants.push(`[${trimmed}]`);
}
variants.push(trimmed.replace(/(#[^#\s]+)/g, (match) => match.replace(/:/g, '\\:')));
return variants;
};

// Tooltips test selectors on every DOM mutation; warn about each one once.
const warnedSelectors = {};

const runFirstValid = (selector, fn) => {
if (typeof selector !== 'string' || !selector.trim()) {
return null;
}

const variants = selectorVariants(selector);
for (let i = 0; i < variants.length; i++) {
try {
return { result: fn(variants[i]) };
} catch (e) {}
}

if (!warnedSelectors[selector]) {
warnedSelectors[selector] = true;
try {
console.warn('[Gleap] Invalid selector:', selector);
} catch (e) {}
}
return null;
};

/**
* `document.querySelector` for stored selectors. Never throws: an invalid
* selector resolves to `null`, like one that matches nothing.
*/
export const querySelectorSafe = (selector, root = document) => {
const found = runFirstValid(selector, (variant) => root.querySelector(variant));
return found ? found.result : null;
};

/**
* `document.querySelectorAll` for stored selectors. Never throws.
*/
export const querySelectorAllSafe = (selector, root = document) => {
const found = runFirstValid(selector, (variant) => root.querySelectorAll(variant));
return found ? found.result : [];
};

/**
* `element.matches` for stored selectors. Never throws.
*/
export const matchesSelectorSafe = (element, selector) => {
const found = runFirstValid(selector, (variant) => element.matches(variant));
return found ? found.result : false;
};
5 changes: 3 additions & 2 deletions src/GleapTooltipManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { arrow, autoUpdate, computePosition, flip, offset, shift } from '@floati
import { GleapSession } from './Gleap';
import { loadIcon } from './UI';
import { checkPageFilter, checkPageRules } from './GleapPageFilter';
import { matchesSelectorSafe, querySelectorAllSafe } from './GleapSafeSelector';

export default class GleapTooltipManager {
tooltips = [];
Expand Down Expand Up @@ -40,7 +41,7 @@ export default class GleapTooltipManager {
checkNodeTooltip(node) {
if (this.filteredTooltips.length > 0) {
this.filteredTooltips.forEach((tooltip) => {
if (tooltip.selector && node.matches(tooltip.selector)) {
if (tooltip.selector && matchesSelectorSafe(node, tooltip.selector)) {
this.linkTooltip(node, tooltip);
}
});
Expand Down Expand Up @@ -449,7 +450,7 @@ export default class GleapTooltipManager {
const tooltip = this.filteredTooltips[i];

try {
const elements = document.querySelectorAll(tooltip.selector);
const elements = querySelectorAllSafe(tooltip.selector);

for (let j = 0; j < elements.length; j++) {
const element = elements[j];
Expand Down
22 changes: 6 additions & 16 deletions src/GleapTours.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { querySelectorSafe } from './GleapSafeSelector';

const GleapTours = (function () {
'use strict';
let currentConfig = {};
Expand Down Expand Up @@ -234,15 +236,9 @@ const GleapTours = (function () {
const { element } = step;
let elemObj = element;
if (typeof elemObj === 'string') {
try {
elemObj = document.querySelector(element);
} catch (error) {
// This will escape colons within IDs but not affect pseudo-classes or other valid uses of colons
let refactoredElement = element.replace(/(#[^#\s]+)/g, function (match) {
return match.replace(/:/g, '\\:');
});
elemObj = document.querySelector(refactoredElement);
}
// Stored selectors can be hand typed and invalid; that has to end in
// onElementNotFound below instead of an exception inside the retry timer.
elemObj = querySelectorSafe(element);
}
if (element && !elemObj && attemptTime >= 0) {
setTimeout(() => {
Expand Down Expand Up @@ -277,13 +273,7 @@ const GleapTours = (function () {
// Re-query the element if the cached reference is no longer in the DOM.
if (!document.body.contains(activeHighlight) && activeStep.element && typeof activeStep.element === 'string') {
try {
var freshEl = document.querySelector(activeStep.element);
if (!freshEl) {
var escaped = activeStep.element.replace(/(#[^#\s]+)/g, function (match) {
return match.replace(/:/g, '\\:');
});
freshEl = document.querySelector(escaped);
}
var freshEl = querySelectorSafe(activeStep.element);
if (freshEl) {
activeHighlight = freshEl;
setState('__activeElement', freshEl);
Expand Down
Loading