Sanitizereasy_svg_sanitizer()

easy_svg_sanitizer()

Get a fresh SVG sanitizer configured exactly like this site cleans uploads, so your add-on keeps and removes the same markup as Easy SVG does.

Function · Easy SVG · Status: Stable

easy_svg_sanitizer() gives your plugin the same SVG cleaner Easy SVG uses for uploads, with this site's allow-lists and remote reference setting already applied. Use it whenever your code accepts SVG markup it did not write itself. It is the one function Easy SVG promises to keep for add-ons.

Usage

easy_svg_sanitizer(): \enshrined\svgSanitize\Sanitizer|null
$sanitizer = easy_svg_sanitizer();
$clean     = $sanitizer ? $sanitizer->sanitize( $markup ) : false;

When to use it

  • Your plugin lets users paste or upload SVG markup (a form field, a REST endpoint, a block attribute) and you store or print it.
  • You import SVGs from somewhere else, for example a remote API, before saving them.
  • You build a feature on top of Easy SVG and want to respect what the site owner allowed through esw_svg_allowed_tags, esw_svg_allowed_attributes and esw_svg_remove_remote_references.

Do not use it:

  • For files that go through the WordPress media uploader. Easy SVG already cleans those; cleaning twice changes nothing but costs time.
  • Instead of the classes esw_svg_tags and esw_svg_attributes, or a new \enshrined\svgSanitize\Sanitizer() of your own. Those are internals, and a sanitizer built any other way ignores the site's filters.
  • As the only check for untrusted input. See "What it does not do" below.

Parameters

This function takes no parameters.

Return value

returnenshrined\svgSanitize\Sanitizer|null

A new Sanitizer instance on every call, configured for this site. null when the class \enshrined\svgSanitize\Sanitizer does not exist, which happens when the plugin's vendor folder is missing.

The instance is yours. Changing it (for example removeXMLTag( true )) affects nothing else. The methods you will use most:

MethodResult
sanitize( $markup )Cleaned markup as a string. '' when the input is empty in the PHP sense ('' and '0'). false when the XML cannot be parsed. Throws LogicException when the XML parses but does not have exactly one svg root, for example an HTML page.
getXmlIssues()What the last sanitize() call removed, as array( array( 'message' => ..., 'line' => ... ) ).
removeXMLTag( true )Leave out the ?xml declaration, which you want for inline output.
minify( true )Collapse whitespace in the result.
removeRemoteReferences( true )Make your copy stricter than the site.

Examples

Clean markup from a form field

/**
 * Clean user supplied SVG markup, or return '' when it is not usable.
 *
 * @param string $markup Raw markup.
 * @return string
 */
function myplugin_clean_svg( $markup ) {
	if ( ! function_exists( 'easy_svg_sanitizer' ) ) {
		return '';
	}

	$sanitizer = easy_svg_sanitizer();
	if ( null === $sanitizer ) {
		return '';
	}

	$sanitizer->removeXMLTag( true );

	try {
		$clean = $sanitizer->sanitize( (string) $markup );
	} catch ( \Throwable $e ) {
		return '';
	}

	if ( ! is_string( $clean ) || '' === trim( $clean ) ) {
		return '';
	}

	return $clean;
}

Treat false, '' and an exception the same way: the input was not a usable SVG.

Import an SVG from a remote API with your own limits

/**
 * Fetch an SVG, clean it and store it as a file in uploads.
 *
 * @param string $url Remote SVG address.
 * @return string|WP_Error Public URL of the stored file.
 */
function myplugin_import_remote_svg( $url ) {
	$max_bytes = 512 * KB_IN_BYTES;

	$response = wp_safe_remote_get( $url, array( 'timeout' => 10, 'limit_response_size' => $max_bytes ) );
	if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
		return new WP_Error( 'myplugin_fetch', __( 'The SVG could not be downloaded.', 'myplugin' ) );
	}

	$markup = wp_remote_retrieve_body( $response );
	if ( strlen( $markup ) >= $max_bytes || substr_count( strtolower( $markup ), '<use' ) > 200 ) {
		return new WP_Error( 'myplugin_too_big', __( 'The SVG is too large or too complex.', 'myplugin' ) );
	}

	$sanitizer = function_exists( 'easy_svg_sanitizer' ) ? easy_svg_sanitizer() : null;
	if ( null === $sanitizer ) {
		return new WP_Error( 'myplugin_no_sanitizer', __( 'Easy SVG is not active.', 'myplugin' ) );
	}

	try {
		$clean = $sanitizer->sanitize( $markup );
	} catch ( \Throwable $e ) {
		$clean = false;
	}
	if ( ! is_string( $clean ) || '' === trim( $clean ) ) {
		return new WP_Error( 'myplugin_not_svg', __( 'This is not a usable SVG.', 'myplugin' ) );
	}

	$upload = wp_upload_bits( 'myplugin-' . md5( $url ) . '.svg', null, $clean );
	if ( ! empty( $upload['error'] ) ) {
		return new WP_Error( 'myplugin_write', $upload['error'] );
	}

	return $upload['url'];
}

// Output: an img tag, escaped like any other URL.
$src = myplugin_import_remote_svg( 'https://icons.example.com/logo.svg' );
if ( ! is_wp_error( $src ) ) {
	printf( '<img src="%s" alt="%s" />', esc_url( $src ), esc_attr__( 'Logo', 'myplugin' ) );
}

The size and use limits are yours on purpose: easy_svg_sanitizer() does not apply the site's upload limits (see "What it does not do"). Writing through wp_upload_bits() also passes Easy SVG's own check on that path, which accepts the file because it is already clean. wp_upload_bits() only accepts the .svg name when SVG is an allowed upload type on the site (the Allow SVG uploads in the media library switch) or the current user has unfiltered_upload.

Version check and logging what was removed

/**
 * Clean markup and log what the sanitizer took out.
 *
 * @param string $markup Raw markup.
 * @return string Clean markup or ''.
 */
function myplugin_clean_and_report( $markup ) {
	// EASY_SVG_API and easy_svg_sanitizer() arrived together in Easy SVG 5.0.0.
	if ( ! defined( 'EASY_SVG_API' ) || EASY_SVG_API < 1 || ! function_exists( 'easy_svg_sanitizer' ) ) {
		return '';
	}

	try {
		$sanitizer = easy_svg_sanitizer();
		$clean     = $sanitizer ? $sanitizer->sanitize( $markup ) : false;
	} catch ( \Throwable $e ) {
		return '';
	}

	if ( $sanitizer && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
		foreach ( $sanitizer->getXmlIssues() as $issue ) {
			error_log( sprintf( 'myplugin SVG: %s (line %d)', $issue['message'], $issue['line'] ) );
		}
	}

	return is_string( $clean ) ? trim( $clean ) : '';
}

The try wraps the easy_svg_sanitizer() call too: if a site's allow-list filter returns something other than an array, building the sanitizer throws a TypeError.

Use this function, not the classes esw_svg_tags or esw_svg_attributes and not a sanitizer you construct yourself. One built any other way does not see the site's filters: it removes what the site keeps and, worse, keeps what the site removes. Never print markup you did not run through sanitize(), and never print the raw input when sanitize() returned false or threw.

How it works

  1. Easy SVG creates a global $sanitizer when the plugin loads, but only when the class exists and the name is still free. It is a template: site snippets from version 4.1 still configure it (for example removeXMLTag( true )), and those settings carry over to every copy.
  2. The function clones that global when it holds a real Sanitizer, otherwise it starts fresh. Something else stored under that name (another plugin's object) is ignored, not called.
  3. The allow-lists and the remote reference setting are always set on the copy, so they win over anything set on the global.
  4. One instance per call: two callers never share state, and your changes to your copy never leak.

Easy SVG itself uses this function on every path: media uploads and sideloads (esw_svg_file_checker()), the wp_upload_bits check (easy_svg_markup_is_clean()), the check on every new attachment, and the icon library (adding, replacing and easy_svg_update_icon_markup()).

What it does not do

These steps run in Easy SVG's own upload paths, around the sanitizer, not inside it. If you handle untrusted input, do the equivalent yourself:

  • Size cap. Uploads are refused above the Maximum SVG size (MB) setting (filter easy_svg_max_bytes).
  • Complexity cap. Files with more than 10 PHP processing instructions or 1000 use elements are refused before parsing (filters easy_svg_max_php_tags, easy_svg_max_use_tags), because those shapes can keep the parser busy for minutes.
  • Empty result. A result that is empty after trim() is refused.
  • Animation check. If a site added animate or set to the allow-list, uploads also remove animation elements that target href or on* attributes.
  • Icon hardening. Icons additionally lose script, style, foreignObject, every animation element, every on* attribute and every href not starting with #.

Pitfalls

  • Call it late enough. The function is defined when Easy SVG's main file loads. In your own plugin file's top level it may not exist yet; call it inside a hook (init or later) or check function_exists() there.
  • null is a real answer. A broken install without the vendor folder returns null. Never call ->sanitize() on the return value unchecked.
  • Three ways to fail. false (not parseable), '' (empty input) and a LogicException (not exactly one svg root) all mean "not usable". Catch \Throwable, not only Exception.
  • Output is pretty-printed and starts with an XML declaration. Call removeXMLTag( true ) before inlining it into HTML.
  • An outdated library can win. If another plugin loaded an older enshrined/svg-sanitize first, you get that class. Easy SVG then shows administrators a warning on the Dashboard, Plugins and Media screens. easy_svg_sanitizer() still returns an instance of that older class.
  • It cleans, it does not validate design. A file that loses an element because it is not allowed still comes back as a valid string. Use getXmlIssues() if you need to tell the user what was removed.