Goal
Modify the previous script to protect against cross-site scripting attacks.
Project
Notes
In addition to sanitizing API data, this revision also improves on rendering performance in the previous script. Rather than concatenating innerHTML, which triggers a re-rendering each time, a new child node is appended.View Source
let error = (() => { let elError = document.querySelector( "#error-message" ); let methods = {}; /** * @param {string} message */ methods.display = message => { elError.innerText = message; elError.classList.remove( "hidden" ); } methods.dismiss = () => elError.classList.add( "hidden" ); return methods; })(); /*! * Sanitize and encode all HTML in a user-submitted string * (c) 2018 Chris Ferdinandi, MIT License, https://gomakethings.com * @param {String} str The user-submitted string * @return {String} str The sanitized string */ let sanitizeHTML = str => { let temp = document.createElement( "div" ); temp.textContent = str; return temp.innerHTML; }; (() => { /* Variables and UI elements */ let elApiKey = document.querySelector( "#api-key" ); elApiKey.value = localStorage.getItem( "nyt-api-key" ); let elFetchStories = document.querySelector( "#fetch-stories" ); let elStories = document.querySelector( "#stories" ); let elStoryItems = document.querySelector( "#story-items" ); /** * @param {Object[]} */ let fetchStories = category => { error.dismiss(); let apiKey = elApiKey.value; if( !apiKey ) { error.display( "Please provide an API key before proceeding." ); return; } let apiURL = `https://api.nytimes.com/svc/topstories/v2/${category}.json?api-key=${apiKey}`; return fetch( apiURL ) .then( response => response.json() ) .then( json => { if( json.fault ) { if( json.fault.detail.errorcode === "oauth.v2.InvalidApiKey" ) { throw new Error( "Invalid API key" ); } throw new Error( json.fault.faultstring ); } elStories.classList.remove( "hidden" ); return json.results; }); }; /** * @param {Object[]} */ let renderStories = (category, stories) => { let html = `<section> <h3 class="font-family:sans font-size:0 font-weight:extrabold line-height:none text-transform:uppercase">${category}</h3> <div class="stack margin-top:1 story-items">`; stories.slice( 0, 5 ).forEach( story => { let thumbnail = story.multimedia.find( image => image.format === "thumbLarge" ); let thumbnailURL = sanitizeHTML( thumbnail.url ); let storyURL = sanitizeHTML( story.url ); let storyTitle = sanitizeHTML( story.title ); let storyAbstract = sanitizeHTML( story.abstract ); html += ` <a class="story columns" href="${storyURL}"> <img src="${thumbnailURL}" loading="lazy"> <article> <p><strong>${storyTitle}</strong></p> <p>${storyAbstract}</p> </article> </a>`; }); html += ` </div> </section>`; let elSection = document.createElement( "section" ); elSection.innerHTML = html; elStories.appendChild( elSection ); }; /* Event listeners */ elApiKey.addEventListener( "input", e => { localStorage.setItem( "nyt-api-key", e.target.value ); }); elFetchStories.addEventListener( "click", () => { elStories.innerHTML = ""; let allStories = []; fetchStories( "arts" ) .then( stories => { renderStories( "Arts", stories ); return fetchStories( "science" ); }) .then( stories => { renderStories( "Science", stories ); return fetchStories( "us" ); }) .then( stories => { renderStories( "U.S.", stories ); return fetchStories( "world" ); }) .then( stories => { renderStories( "World", stories ); }) .catch( err => error.display( err.message ) ); }); })();
Projects
- Toggle Password Visibility
- Toggle Multiple Password Fields
- Toggle Passwords in Multiple Forms
- Character Count
- Character and Word Count
- Announcing the Count
- Random Ron
- Random Ron Without Duplicates
- New York Times Top Stories
- New York Times Multiple Categories
- Sanitizing the NYT API Data
- Monster Shuffle