🍦 Vanilla JavaScript

Projects for the Vanilla JS Academy

Day 21

Sanitizing the NYT API Data

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 ) );
	});
})();