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