Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- Below is a **drop‑in replacement** for the `openDetail()` routine (and a quick refresher on how to reuse the same escaping helper that we already introduced for the card rendering).
- Everything that is inserted into the DOM – titles, text blocks, lists, etc. – is first run through the same `escapeHTML()` routine so that any `<script>` or other harmful markup that might sneak into the JSON file can’t be executed.
- ```javascript
- /* --------------------------------------------------------------------
- Utility – escape HTML (kept in the global scope so that all helpers
- can reuse it). This is the same function we used in `renderCard()`.
- -------------------------------------------------------------------- */
- function escapeHTML(str) {
- return String(str)
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
- }
- /* --------------------------------------------------------------------
- Rendering the cards (kept the same – just a reminder that it uses
- the helper). It is unchanged except for the `escapeHTML()` calls.
- -------------------------------------------------------------------- */
- function renderCard(book) {
- const title = escapeHTML(book.title);
- const card = `
- <div class="card" data-id="${escapeHTML(book.id)}">
- <h3>${title}</h3>
- <div class="card-body">
- <h4>${escapeHTML(book.title)} – ${escapeHTML(book.author)}</h4>
- <p class="card-text">${escapeHTML(book.blurb)}</p>
- </div>
- </div>`;
- return card;
- }
- ```
- ---
- ### 1. The new `openDetail()` implementation
- ```javascript
- /* --------------------------------------------------------------------
- Global variable that will hold the currently shown detail pane.
- -------------------------------------------------------------------- */
- let activeDetailView = null;
- /* --------------------------------------------------------------------
- 1⃣ Build the content **with escaping** and return it as a
- `template` element. Using a <template> gives us a cheap
- sandbox where we can create children with *textContent* – no
- chance of injecting raw HTML.
- -------------------------------------------------------------------- */
- function openDetail(id) {
- const result = playbooks.find(b => b.id === id);
- if (!result) return;
- // ---------- 1. Build the steps list --------------------------------
- const stepsContainer = document.createElement('ul');
- stepsContainer.className = 'detail-steps';
- result.steps.forEach(step => {
- const li = document.createElement('li');
- li.textContent = step; // <– safe: textContent escapes everything
- stepsContainer.appendChild(li);
- });
- // ---------- 2. Build the whole detail pane ------------------------
- const detailContainer = document.createElement('div');
- detailContainer.className = 'detail-view';
- detailContainer.setAttribute('data-id', result.id);
- // 2a. A small “Close” button that simply removes the pane
- const closeBtn = document.createElement('button');
- closeBtn.className = 'detail-close';
- closeBtn.textContent = '×'; // × looks nicer than “Close”
- closeBtn.onclick = closeDetail; // will be defined below
- detailContainer.appendChild(closeBtn);
- // 2b. Add the heading (escaped!)
- const heading = document.createElement('h2');
- heading.textContent = result.title;
- detailContainer.appendChild(heading);
- // 2c. Add the blurb (escaped!)
- const blurb = document.createElement('p');
- blurb.textContent = result.blurb;
- detailContainer.appendChild(blurb);
- // 2d. Append the steps list
- detailContainer.appendChild(stepsContainer);
- // 2e. (Optional) Add a little bit of styling if you want the pane
- // to sit on top of the page
- detailContainer.style.position = 'fixed';
- detailContainer.style.top = '20px';
- detailContainer.style.left = '50%';
- detailContainer.style.transform = 'translateX(-50%)';
- detailContainer.style.background = '#fff';
- detailContainer.style.padding = '1rem 2rem';
- detailContainer.style.boxShadow = '0 4px 12px rgba(0,0,0,.15)';
- detailContainer.style.zIndex = 9999;
- // ---------- 3⃣ Add the pane to the document ----------------------
- document.body.appendChild(detailContainer);
- activeDetailView = detailContainer; // keep a handle so we can close it
- // ---------- 4⃣ Highlight the corresponding card ----------------
- const highlightedCard = document.querySelector(`.card[data-id="${id}"]`);
- if (highlightedCard) highlightedCard.classList.add('highlight');
- }
- /* --------------------------------------------------------------------
- Closes the detail pane – removes the highlight on the card
- and removes the detail element from the DOM.
- -------------------------------------------------------------------- */
- function closeDetail() {
- if (activeDetailView) {
- const id = activeDetailView.getAttribute('data-id');
- const card = document.querySelector(`.card[data-id="${id}"]`);
- if (card) card.classList.remove('highlight');
- activeDetailView.remove();
- activeDetailView = null;
- }
- }
- /* --------------------------------------------------------------------
- Wire the “Search” results (or any other trigger) to the new routine.
- -------------------------------------------------------------------- */
- searchInput.addEventListener('input', () => {
- const query = searchInput.value.trim();
- displaySearchResults(query);
- });
- /* --------------------------------------------------------------------
- When a user clicks a search result, open its detail pane.
- -------------------------------------------------------------------- */
- document.addEventListener('click', event => {
- if (event.target.closest('.search-result')) {
- const target = event.target.closest('.search-result');
- const id = target.dataset.id;
- openDetail(id);
- }
- });
- ```
- ---
- ### 2. How it protects against XSS
- 1. **All strings from the JSON are inserted as `textContent`**
- – This property automatically escapes any `<`, `>`, `"`, `&`, etc.
- 2. **No `innerHTML` is used on untrusted data**
- The only `innerHTML` that appears is in `renderCard()` where we *already*
- applied `escapeHTML()` to every piece of text.
- 3. **The detail pane is a separate element**
- We never inject raw HTML from the server into the DOM; we build
- the detail pane entirely from DOM nodes that are created by
- JavaScript, so the browser itself prevents any stray tags from
- being interpreted.
- 4. **Event delegation keeps the click‑handler safe**
- We test the target with `closest()` which stops at the nearest
- ancestor that matches. Even if a malicious string were to slip
- into the search results, it would be treated as *plain text*.
- ---
- ### 3. Optional tweaks
- * If you wish to keep the **old `onclick` behaviour** from the
- search results, you could add `onclick="openDetail('${id}')"`,
- but the approach above is safer because it avoids string‑based
- event binding entirely.
- * Add a CSS rule for `.highlight` if you want a visible visual cue:
- ```css
- .card.highlight {
- border: 2px solid #f0ad4e; /* e.g. a gold border */
- box-shadow: 0 0 8px rgba(240, 173, 78, .6);
- }
- ```
- * If you want the pane to scroll to the card instead of using a
- “fixed” position, you can use `scrollIntoView()`:
- ```js
- highlightedCard.scrollIntoView({behavior:'smooth', block:'center'});
- ```
- ---
- ### 4. Recap of what changed
- * **`openDetail()`** – now creates a safe, **textContent‑only**
- pane and attaches a close button.
- * **`closeDetail()`** – removes the pane and the card highlight.
- * **`renderCard()`** – uses the same escape helper.
- * **Search click** – wired to call `openDetail(id)`.
- These changes make sure that any user‑supplied data that appears in
- the detail view can no longer be interpreted as executable
- JavaScript or markup, keeping your page safe from XSS.
Advertisement
Add Comment
Please, Sign In to add comment