|
Server : Apache System : Linux s1230 5.15.0-139-generic #149~20.04.1 SMP Tue Jul 14 11:21:49 UTC 2026 x86_64 User : p141464 ( 418825) PHP Version : 7.4.33.12 Disable Function : NONE Directory : /html/relaunch-kmu/wp-content/plugins/wpforms-lite/includes/functions/ |
Upload File : |
<?php
/**
* Helper functions to clean and sanitize data, escape it and prepare the output.
*
* @since 1.8.0
*/
// phpcs:disable Generic.Commenting.DocComment.MissingShort
/** @noinspection PhpUndefinedNamespaceInspection */
/** @noinspection PhpUndefinedClassInspection */
// phpcs:enable Generic.Commenting.DocComment.MissingShort
use WPForms\Helpers\Templates;
use WPForms\Vendor\HTMLPurifier;
use WPForms\Vendor\HTMLPurifier_Config;
use WPForms\Helpers\File;
use WPForms\Vendor\enshrined\svgSanitize\Sanitizer;
/**
* Decode special characters, both alpha- (<) and numeric-based (').
* Sanitize recursively, preserve new lines.
* Handle all the possible mixed variations of < and `<` that can be processed into tags.
*
* @since 1.4.1
* @since 1.6.0 Sanitize recursively, preserve new lines.
*
* @param string $string Raw string to decode.
*
* @return string
*/
function wpforms_decode_string( $string ) {
if ( ! is_string( $string ) ) {
return $string;
}
/*
* Sanitization should be done first, so tags are stripped and < is converted to < etc.
* This iteration may do nothing when the string already comes with < and > only.
*/
$string = wpforms_sanitize_text_deeply( $string, true );
// Now we need to convert the string without tags: < back to < (same for quotes).
$string = wp_kses_decode_entities( html_entity_decode( $string, ENT_QUOTES ) );
// And now we need to sanitize AGAIN, to avoid unwanted tags that appeared after decoding.
return wpforms_sanitize_text_deeply( $string, true );
}
/**
* Sanitize key, primarily used for looking up options.
*
* @since 1.3.9
*
* @param string $key Key name.
*
* @return string
*/
function wpforms_sanitize_key( $key = '' ) {
return preg_replace( '/[^a-zA-Z0-9_\-\.\:\/]/', '', $key );
}
/**
* Sanitize hex color.
*
* @since 1.2.1
*
* @param string $color Color value.
*
* @return string
*/
function wpforms_sanitize_hex_color( $color ) {
if ( empty( $color ) ) {
return '';
}
// 3 or 6 hex digits, or the empty string.
if ( preg_match( '|^#([A-Fa-f0-9]{3}){1,2}$|', $color ) ) {
return $color;
}
return '';
}
/**
* Sanitize error message, primarily used during form frontend output.
*
* @since 1.3.7
* @since 1.7.6 Expand list of allowed HTML tags and attributes.
*
* @param string $error Error message.
*
* @return string
*/
function wpforms_sanitize_error( $error = '' ) {
$allow = [
'a' => [
'href' => [],
'title' => [],
'target' => [],
'rel' => [],
],
'br' => [],
'em' => [],
'strong' => [],
'del' => [],
'p' => [
'style' => [],
],
'blockquote' => [],
'ul' => [],
'ol' => [],
'li' => [],
'span' => [
'style' => [],
],
];
return wp_kses( $error, $allow );
}
/**
* Sanitize a string, that can be a multiline.
*
* @uses wpforms_sanitize_text_deeply()
*
* @since 1.4.1
*
* @param string $string String to deeply sanitize.
*
* @return string Sanitized string, or empty string if not a string provided.
*/
function wpforms_sanitize_textarea_field( $string ) {
return wpforms_sanitize_text_deeply( $string, true );
}
/**
* Deeply sanitize the string, preserve newlines if needed.
* Prevent maliciously prepared strings from containing HTML tags.
*
* @since 1.6.0
*
* @param string $string String to deeply sanitize.
* @param bool $keep_newlines Whether to keep newlines. Default: false.
*
* @return string Sanitized string, or empty string if not a string provided.
*/
function wpforms_sanitize_text_deeply( $string, $keep_newlines = false ) {
if ( is_object( $string ) || is_array( $string ) ) {
return '';
}
$string = (string) $string;
$keep_newlines = (bool) $keep_newlines;
$new_value = _sanitize_text_fields( $string, $keep_newlines );
if ( strlen( $new_value ) !== strlen( $string ) ) {
$new_value = wpforms_sanitize_text_deeply( $new_value, $keep_newlines );
}
return $new_value;
}
/**
* Sanitize an HTML string with a set of allowed HTML tags.
*
* @since 1.7.0
*
* @param string $value String to sanitize.
*
* @return string Sanitized string.
*/
function wpforms_sanitize_richtext_field( $value ) {
$count = 1;
$value = convert_invalid_entities( $value );
// Remove 'script' and 'style' tags recursively.
while ( $count ) {
$value = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $value, - 1, $count );
}
// Make sure we have allowed tags only.
$value = wp_kses( $value, wpforms_get_allowed_html_tags_for_richtext_field() );
// Make sure that all tags are balanced.
return force_balance_tags( $value );
}
/**
* Escaping for Rich Text field values.
*
* @since 1.7.0
* @since 1.9.1 Removed new lines after adding paragraphs and breaks tags.
*
* @param string $value Text to escape.
*
* @return string Escaped text.
*/
function wpforms_esc_richtext_field( $value ) {
$value = wpautop( wpforms_sanitize_richtext_field( $value ) );
return trim( str_replace( [ "\r\n", "\r", "\n" ], '', $value ) );
}
/**
* Retrieve allowed HTML tags for Rich Text field.
*
* @since 1.7.0
*
* @return array Array of allowed tags.
*/
function wpforms_get_allowed_html_tags_for_richtext_field() {
$allowed_tags = array_fill_keys(
[
'img',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'p',
'a',
'ul',
'ol',
'li',
'dl',
'dt',
'dd',
'hr',
'br',
'code',
'pre',
'strong',
'b',
'em',
'i',
'blockquote',
'cite',
'q',
'del',
'span',
'small',
'table',
'thead',
'tbody',
'tfoot',
'th',
'tr',
'td',
'abbr',
'address',
'sub',
'sup',
'ins',
'figure',
'figcaption',
'caption',
'div',
],
array_fill_keys(
[ 'align', 'class', 'id', 'style', 'src', 'rel', 'alt', 'href', 'target', 'width', 'height', 'title', 'cite', 'start', 'reversed', 'datetime', 'scope', 'colspan', 'rowspan' ],
[]
)
);
/**
* Allowed HTML tags for Rich Text field.
*
* @since 1.7.0
*
* @param array $allowed_tags Allowed HTML tags.
*/
$tags = (array) apply_filters( 'wpforms_get_allowed_html_tags_for_richtext_field', $allowed_tags );
// Force unset iframes, script and style no matter when we get back
// from apply_filters, as they are a huge security risk.
unset( $tags['iframe'], $tags['script'], $tags['style'] );
return $tags;
}
/**
* Sanitize an array, that consists of values as strings.
* After that - merge all array values into multiline string.
*
* @since 1.4.1
*
* @param array $array Data to sanitize.
*
* @return mixed If not an array is passed (or empty var) - return unmodified var.
* Otherwise - a merged array into multiline string.
*/
function wpforms_sanitize_array_combine( $array ) {
if ( empty( $array ) || ! is_array( $array ) ) {
return $array;
}
return implode( "\n", array_map( 'sanitize_text_field', $array ) );
}
/**
* Format, sanitize, and return/echo HTML element ID, classes, attributes,
* and data attributes.
*
* @since 1.3.7
*
* @param string $id HTML id attribute value.
* @param array $class A list of classnames for the class attribute.
* @param array $datas Data attributes.
* @param array $atts Any additional HTML attributes and their values.
* @param bool $echo Whether to echo the output or just return it. Defaults to return.
*
* @return string|void
*/
function wpforms_html_attributes( $id = '', $class = [], $datas = [], $atts = [], $echo = false ) {
$id = trim( $id );
$parts = [];
if ( ! empty( $id ) ) {
$id = sanitize_html_class( $id );
if ( ! empty( $id ) ) {
$parts[] = 'id="' . $id . '"';
}
}
if ( ! empty( $class ) ) {
$class = wpforms_sanitize_classes( $class, true );
if ( ! empty( $class ) ) {
$parts[] = 'class="' . $class . '"';
}
}
if ( ! empty( $datas ) ) {
foreach ( $datas as $data => $val ) {
$parts[] = 'data-' . sanitize_html_class( $data ) . '="' . esc_attr( $val ) . '"';
}
}
if ( ! empty( $atts ) ) {
foreach ( $atts as $att => $val ) {
if ( '0' === (string) $val || ! empty( $val ) ) {
if ( $att[0] === '[' ) {
// Handle special case for bound attributes in AMP.
$escaped_att = '[' . sanitize_html_class( trim( $att, '[]' ) ) . ']';
} else {
$escaped_att = sanitize_html_class( $att );
}
$parts[] = $escaped_att . '="' . esc_attr( $val ) . '"';
}
}
}
$output = implode( ' ', $parts );
if ( $echo ) {
echo trim( $output ); // phpcs:ignore
} else {
return trim( $output );
}
}
/**
* Sanitize string of CSS classes.
*
* @since 1.2.1
*
* @param array|string $classes CSS classes.
* @param bool $convert True will convert strings to array and vice versa.
*
* @return string|array
*/
function wpforms_sanitize_classes( $classes, $convert = false ) {
$array = is_array( $classes );
$css = [];
if ( ! empty( $classes ) ) {
if ( ! $array ) {
$classes = explode( ' ', trim( $classes ) );
}
foreach ( array_unique( $classes ) as $class ) {
if ( ! empty( $class ) ) {
$css[] = sanitize_html_class( $class );
}
}
}
if ( $array ) {
return $convert ? implode( ' ', $css ) : $css;
}
return $convert ? $css : implode( ' ', $css );
}
/**
* Include a template - alias to \WPForms\Helpers\Template::get_html.
* Use 'require' if $args are passed or 'load_template' if not.
*
* @since 1.5.6
*
* @param string $template_name Template name.
* @param array $args Arguments.
* @param bool $extract Extract arguments.
*
* @throws RuntimeException If extract() tries to modify the scope.
*
* @return string Compiled HTML.
*/
function wpforms_render( $template_name, $args = [], $extract = false ) {
return Templates::get_html( $template_name, $args, $extract );
}
/**
* Alias for default readonly function.
*
* @since 1.6.9
*
* @param mixed $readonly One of the values to compare.
* @param mixed $current The other value to compare if not just true.
* @param bool $echo Whether to echo or just return the string.
*
* @return string HTML attribute or empty string.
*/
function wpforms_readonly( $readonly, $current = true, $echo = true ) {
if ( function_exists( 'wp_readonly' ) ) {
return wp_readonly( $readonly, $current, $echo );
}
return __checked_selected_helper( $readonly, $current, $echo, 'readonly' );
}
/**
* Get the required label text, with a filter.
*
* @since 1.4.4
*
* @return string
*/
function wpforms_get_required_label() {
return apply_filters( 'wpforms_required_label', esc_html__( 'This field is required.', 'wpforms-lite' ) );
}
/**
* Get the required field label HTML, with a filter.
*
* @since 1.4.8
*
* @return string
*/
function wpforms_get_field_required_label() {
$label_html = apply_filters_deprecated(
'wpforms_field_required_label',
[ ' <span class="wpforms-required-label">*</span>' ],
'1.4.8 of the WPForms plugin',
'wpforms_get_field_required_label'
);
return apply_filters( 'wpforms_get_field_required_label', $label_html );
}
/**
* Escape unselected choices for radio/checkbox fields.
*
* @since 1.8.3
*
* @param string $formatted_field HTML field.
*
* @return string
*/
function wpforms_esc_unselected_choices( $formatted_field ) {
$allowed_html = wp_kses_allowed_html( 'post' );
$allowed_html['input'] = [
'type' => [],
'disabled' => [],
'checked' => [],
'class' => [],
'value' => [],
];
$allowed_html['label'] = [];
return wp_kses( $formatted_field, $allowed_html );
}
/**
* Sanitize HTML produced for an entry field value before it is displayed.
*
* Entry surfaces pass the value through the `wpforms_html_field_value` filter, whose callbacks may
* return markup built from stored submission data. Sanitize on output, mirroring the entry
* single-View screen, so event-handler attributes and <script> cannot survive to the browser.
*
* Callers that render a field value known to be built as an iframe by the plugin itself pass
* `$is_iframe_allowed`, which widens the allowlist for that single call only.
* Use wpforms_is_entry_field_value_iframe_allowed() to decide, so the allowance is granted per
* field rather than per screen.
*
* @since 2.0.1
*
* @param string $value Field value HTML.
* @param bool $is_iframe_allowed Whether the iframe tag is allowed in the value.
*
* @return string
*/
function wpforms_esc_entry_field_value( $value, bool $is_iframe_allowed = false ): string {
$allowed_html = wp_kses_allowed_html( 'post' );
if ( ! $is_iframe_allowed ) {
return wp_kses( (string) $value, $allowed_html );
}
// The Rich Text and Geolocation field values render through an iframe that the `post` allowlist drops.
// The allowlist is extended locally instead of via add_filter()/remove_filter(): WordPress keys hook
// registrations by callback and priority, so a nested call inside a region that registered the same
// callback (like the entry single-View fields loop) would detach the outer registration.
$allowed_html = wpforms_get_allowed_html_tags_for_entry_field_value( $allowed_html, 'post' );
// Force kses to treat `data-src` as a URI attribute, so unsafe protocols are stripped from it.
// The filter is registered only around this single core call and removed right after it. The
// has_filter() check keeps that bracket re-entrant: a nested call (or an existing outside
// registration) must not have its registration removed when the inner bracket unwinds.
$is_registered = has_filter( 'wp_kses_uri_attributes', 'wpforms_add_data_src_uri_attribute' ) !== false;
if ( ! $is_registered ) {
add_filter( 'wp_kses_uri_attributes', 'wpforms_add_data_src_uri_attribute' );
}
$sanitized = wp_kses( (string) $value, $allowed_html );
if ( ! $is_registered ) {
remove_filter( 'wp_kses_uri_attributes', 'wpforms_add_data_src_uri_attribute' );
}
return $sanitized;
}
/**
* Add the `data-src` attribute to the list of attributes kses validates as a URI.
*
* Makes kses run wp_kses_bad_protocol() on `data-src`, stripping `javascript:` and `data:` URIs.
*
* @since 2.0.1
*
* @param array|mixed $uri_attributes List of attributes treated as URIs by kses.
*
* @return array
*/
function wpforms_add_data_src_uri_attribute( $uri_attributes ): array {
$uri_attributes = (array) $uri_attributes;
$uri_attributes[] = 'data-src';
return $uri_attributes;
}
/**
* Neutralize all HTML tags in a value by entity-encoding it.
*
* Neither half of this does the job alone. Wp_strip_all_tags() relies on strip_tags(), which leaves a
* tag intact when whitespace follows the opening bracket: `< iframe data-src="https://evil.test">`
* passes through untouched and can then be parsed back into a real tag by a later kses call. Escaping
* with esc_html() alone closes that hole, but it also stops removing genuine markup, so a submitted
* `<b>hi</b>` reaches the screen as visible `<b>hi</b>` rather than `hi`, including on the front-end
* Entry Preview.
*
* Running both in this order gives each half the case it handles well: strip_tags() deletes what really
* parses as a tag (and wp_strip_all_tags() drops `<script>`/`<style>` contents with it), then esc_html()
* makes the leftovers inert. A spaced-out fake tag therefore survives as text instead of becoming markup,
* while legitimate bare-angle-bracket content such as `5 < 10 and 20 > 15` still round-trips unchanged —
* unlike wp_kses() with an empty allowlist, which matches any `<...>` span against its tag grammar
* (accepting a bare digit run like `10` as a tag name) and silently destroys it to `5 15`.
*
* The (string) cast is required because several call sites may pass null or an array element.
* Callers that hand the result to another escaper must make sure it does not re-encode the entities
* produced here.
*
* @since 2.0.1
*
* @param mixed $value Value to neutralize.
*
* @return string
*/
function wpforms_neutralize_html_tags( $value ): string {
return esc_html( wp_strip_all_tags( (string) $value ) );
}
/**
* Determine whether an entry field value may legitimately contain an iframe.
*
* Only the Rich Text and Geolocation Map values are rendered as an iframe, and the plugin builds
* that markup itself from a URL it escapes. Every other field value is stored submission data, so
* granting the allowance per field instead of per screen keeps a stored iframe from surviving to the
* entry screens, where the field-value script copies `data-src` into `src`.
*
* @since 2.0.1
*
* @param array $field Entry field data.
*
* @return bool
*/
function wpforms_is_entry_field_value_iframe_allowed( array $field ): bool {
$types = [ 'richtext', 'map' ];
/**
* Filter the field types whose entry value may contain an iframe.
*
* Addons that render an entry field value through an iframe must register their field type here,
* otherwise the iframe is stripped on the entry View and Print screens.
*
* @since 2.0.1
*
* @param array $types Field types allowed to render an iframe.
* @param array $field Entry field data.
*/
$types = (array) apply_filters( 'wpforms_is_entry_field_value_iframe_allowed_types', $types, $field );
return in_array( $field['type'] ?? '', $types, true );
}
/**
* Allow additional tags for the entry field value sanitization.
*
* The `data-src` attribute is safe to allow only because the sink URL-validates it: the calling
* context registers wpforms_add_data_src_uri_attribute() so kses rejects unsafe protocols in it.
*
* @since 2.0.1
*
* @param array|mixed $tags List of allowed HTML.
* @param string $context Context name.
*
* @return array
*/
function wpforms_get_allowed_html_tags_for_entry_field_value( $tags, string $context ): array {
$tags = (array) $tags;
if ( $context !== 'post' ) {
return $tags;
}
$tags['iframe'] = [
'data-src' => [],
'class' => [],
];
return $tags;
}
/**
* Decode HTML entities in a string.
* Do it cycle to decode all possible entities, including cases like `&lt;`.
*
* @since 1.9.2.3
*
* @param string $html HTML.
* @param int $flags Flags.
* @param string|null $encoding Encoding.
*
* @return string
* @noinspection PhpMissingParamTypeInspection
*/
function wpforms_html_entity_decode_deep( string $html, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, $encoding = null ): string {
do {
$previous_html = $html;
$html = html_entity_decode( $html, $flags, $encoding );
} while ( $html !== $previous_html );
return $html;
}
/**
* Sanitize form data.
*
* @since 1.9.3
*
* @param array $data Form data.
*
* @return array
*/
function wpforms_sanitize_form_data( array $data ): array {
foreach ( $data['fields'] as & $field ) {
$field = wpforms_sanitize_field( $field );
}
unset( $field );
return $data;
}
/**
* Sanitize form field.
*
* @since 1.9.3
*
* @param array $field Field.
*
* @return array
*/
function wpforms_sanitize_field( array $field ): array {
$raw_field_options = [
'html' => [ 'code' ],
];
$field_type = $field['type'] ?? '';
$raw_options = $raw_field_options[ $field_type ] ?? [];
/**
* Filter raw options for a field type.
* Allows modifying options that should not be sanitized.
*
* @since 1.9.3
*
* @param array $raw_options Raw options.
* @param string $field_type Field type.
*/
$raw_options = (array) apply_filters( 'wpforms_raw_options', $raw_options, $field_type );
$purifier = wpforms_get_html_purifier();
$decode_and_purify = static function ( $item ) use ( $purifier ) {
return $purifier->purify( wpforms_html_entity_decode_deep( $item ) );
};
foreach ( $field as $option => & $value ) {
if ( in_array( $option, $raw_options, true ) ) {
continue;
}
$value = wp_unslash( $value );
if ( is_array( $value ) ) {
array_walk_recursive( $value, $decode_and_purify );
} else {
$value = $decode_and_purify( $value );
}
$value = wp_slash( $value );
}
unset( $value );
return $field;
}
/**
* Get allowed HTML purifier object.
*
* @since 1.9.3
*
* @return HTMLPurifier
*/
function wpforms_get_html_purifier(): HTMLPurifier {
static $purifier;
if ( $purifier ) {
return $purifier;
}
require_once WPFORMS_PLUGIN_DIR . '/vendor_prefixed/ezyang/htmlpurifier/library/HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
$cache_dir = trailingslashit( File::get_cache_dir() ) . 'htmlpurifier';
// Make sure the cache directory exists.
File::mkdir( $cache_dir );
$config->set( 'Cache.SerializerPath', $cache_dir );
$config->set( 'Attr.AllowedRel', 'noopener,noreferrer,external,follow,nofollow,ugc,sponsored,tag' );
$config->set( 'Attr.AllowedFrameTargets', [ '_blank', '_self', '_parent', '_top' ] );
$config->set( 'HTML.TargetNoopener', false );
$config->set( 'HTML.TargetNoreferrer', false );
$purifier = new HTMLPurifier( $config );
return $purifier;
}
/**
* Sanitize an SVG file in place, stripping scripts, event handlers and other
* potentially malicious markup.
*
* Non-SVG files are left untouched. Sites that explicitly enable SVG support
* would otherwise store SVG uploads unsanitized, allowing stored XSS. To stay
* safe by default, this function fails closed: when the SVG sanitizer is
* unavailable or sanitization fails, it returns false so the caller can reject
* the file rather than store it unsanitized.
*
* Gzipped SVG variants (e.g. .svgz) cannot be parsed as plain XML, so they fail
* sanitization and are rejected rather than stored unsanitized.
*
* @since 1.10.1.1
*
* @param string $file_path Absolute path to the file to sanitize.
* @param string $file_name Optional. Original file name used to determine the extension when
* the path itself has none (e.g. a PHP upload temp file). Default ''.
*
* @return bool True if the file is not an SVG or was sanitized successfully, false on failure.
*/
function wpforms_sanitize_svg_file( string $file_path, string $file_name = '' ): bool {
if ( ! is_file( $file_path ) ) {
return false;
}
// Derive the extension from the original name when provided, otherwise from the path.
$name_for_ext = $file_name !== '' ? $file_name : $file_path;
$extension = strtolower( pathinfo( $name_for_ext, PATHINFO_EXTENSION ) );
// Bail early for anything that is not an SVG. Nothing to sanitize.
if ( ! in_array( $extension, [ 'svg', 'svgz' ], true ) ) {
return true;
}
$contents = file_get_contents( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions
if ( empty( $contents ) ) {
return false;
}
$sanitized = wpforms_sanitize_svg_markup( $contents );
if ( $sanitized === false ) {
return false;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions
return file_put_contents( $file_path, $sanitized ) !== false;
}
/**
* Sanitize an SVG markup string, stripping scripts, event handlers and other
* potentially malicious markup.
*
* Fails closed: returns false when the sanitizer library is unavailable or when
* the markup cannot be sanitized (e.g. it is not valid SVG XML).
*
* @since 1.10.1.1
*
* @param string $markup SVG markup to sanitize.
*
* @return string|false Sanitized markup, or false on failure.
*/
function wpforms_sanitize_svg_markup( string $markup ) {
// Fail closed if the sanitizer library is not available.
if ( ! class_exists( Sanitizer::class ) ) {
return false;
}
$sanitizer = new Sanitizer();
// Block references to remote resources to prevent SSRF and data exfiltration.
$sanitizer->removeRemoteReferences( true );
$sanitized = $sanitizer->sanitize( $markup );
if ( $sanitized === false ) {
return false;
}
// The sanitizer library only strips remote references wrapped in quotes, e.g. url( 'http://...' ).
// Bare references such as fill="url(http://...)" slip through, so strip remote url() targets here.
// Local references like url(#gradient) and data: URIs have no // and are intentionally left intact.
$sanitized = (string) preg_replace(
'~url\(\s*[\'"]?\s*(?:(?:https?|ftp|file):)?//[^)]*\)~i',
'url()',
$sanitized
);
// The library's removeRemoteReferences() only matches url()-wrapped values, and its href allow-list
// explicitly permits bare http(s) URLs. So elements like <image>, <pattern> and <marker> keep a remote
// href/xlink:href and silently load the resource (tracking pixel risk) when the file is opened. Strip
// any href/xlink:href whose value is a remote reference (scheme://... or protocol-relative //...).
// Local fragment references (#id), relative paths (/path) and data: URIs have no // and are kept intact.
return (string) preg_replace(
'~\s(?:xlink:)?href\s*=\s*([\'"])\s*(?:(?:https?|ftp|file):)?//.*?\1~is',
'',
$sanitized
);
}