Skip to content

Remove icms_core_Textsanitizer stub #88

Description

@MekDrop

Instead having such stub that can't work outside @ImpressCMS, replace with something similar how you solved #84 issue with #85 PR:

class icms_core_Textsanitizer
{
    public static function getInstance(): self
    {
        return new self();
    }

    public function displayTarea(
        string $text,
        int $html,
        int $smiley,
        int $xcode,
        int $image,
        int $br
    ): string {
        return $text;
    }
}

Note: In @ImpressCMS displayTarea works like this:

	public function displayTarea($text, $html = 0, $smiley = 1, $xcode = 1, $image = 1, $br = 1) {
		// Before this can be deprecated, the events for displayTarea need to be added, first
		//icms_core_Debug::setDeprecated('icms_core_DataFilter::checkVar - type = text or html, $options1 = input or output', sprintf(_CORE_REMOVE_IN_VERSION, '1.4'));

		/* trigger all the events tied to the beforeDisplayTarea event */
		icms::$preload->triggerEvent('beforeDisplayTarea', array(&$text, $html, $smiley, $xcode, $image, $br));

		if ($html === 0){
			$text = icms_core_DataFilter::filterTextareaDisplay($text, $smiley, $xcode, $image, $br);
		} else {
			$text = icms_core_DataFilter::filterHTMLdisplay($text, $xcode, $br);
		}

		/* trigger all events tied to the afterDisplayTarea event */
		icms::$preload->triggerEvent('afterDisplayTarea', array(&$text, $html, $smiley, $xcode, $image, $br));
		return $text;
	}

and icms_core_DataFilter source:

<?php

// ------------------------------------------------------------------------ //
// XOOPS - PHP Content Management System //
// Copyright (c) 2000 XOOPS.org //
// <http://www.xoops.org/> //
// ------------------------------------------------------------------------ //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation; either version 2 of the License, or //
// (at your option) any later version. //
// //
// You may not change or alter any portion of this comment or credits //
// of supporting developers from this source code or any supporting //
// source code which is considered copyrighted (c) material of the //
// original comment or credit authors. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program; if not, write to the Free Software //
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //
// ------------------------------------------------------------------------ //
// Author: Kazumi Ono (AKA onokazu) //
// URL: http://www.myweb.ne.jp/, http://www.xoops.org/, http://jp.xoops.org/ //
// Project: The XOOPS Project //
// ------------------------------------------------------------------------- //
/**
 * Class to filter Data
 *
 * @category ICMS
 * @package Core
 * @subpackage Filters
 * @since 1.3
 * @author vaughan montgomery (vaughan@impresscms.org)
 * @author ImpressCMS Project
 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
 * @version SVN: $Id: DataFilter.php 12369 2013-11-24 00:04:20Z skenow $
 */
/**
 *
 * @category ICMS
 * @package Core
 * @subpackage Filters
 *
 */
class icms_core_DataFilter {

	/**
	 * Default characters to escape in addSlashes() for backwards compatibility with PHP's addslashes()
	 * 
	 * @var string Contains: ' (single quote), " (double quote), \ (backslash), and \0 (NUL byte)
	 */
	const DEFAULT_ESCAPE_CHARS = "'\"\\\0";

	/**
	 *
	 * @public	array
	 */
	public static $displaySmileys = array();

	/**
	 *
	 * @public	array
	 */
	public static $allSmileys = array();

	/**
	 */
	public function __construct() {}

	// -------- Public Functions --------

	/**
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param $text
	 * @param $msg
	 */
	static public function filterDebugInfo($text, $msg) {
		echo "<div style='padding: 5px; color: red; font-weight: bold'>$text</div>";
		echo "<div><pre>";
		print_r($msg);
		echo "</pre></div>";
	}

	/**
	 * Filters out invalid strings included in URL, if any
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 * @param array $matches
	 * @return string
	 */
	static public function _filterImgUrl($matches) {
		if (self::checkUrlString($matches[2])) {
			return $matches[0];
		} else {
			return '';
		}
	}

	/**
	 * Checks if invalid strings are included in URL
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 * @param string $text
	 * @return bool
	 */
	static public function checkUrlString($text) {
		// Check control code
		if (preg_match("/[\0-\31]/", $text)) {
			return false;
		}
		// check black pattern(deprecated)
		return !preg_match("/^(javascript|vbscript|about):/i", $text);
	}

	/**
	 * Convert linebreaks to <br /> tags
	 *
	 * @param string $text
	 * @return string
	 */
	static public function nl2Br($text) {
		return preg_replace("/(\015\012)|(\015)|(\012)/", "<br />", $text);
	}

	/**
	 * for displaying data in html textbox forms
	 *
	 * @param string $text
	 * @return string
	 */
	static public function htmlSpecialChars($text) {
		return preg_replace(array("/&amp;/i", "/&nbsp;/i"), array('&', '&amp;nbsp;'), @htmlspecialchars($text, ENT_QUOTES, _CHARSET));
	}

	/**
	 * Reverses {@link htmlSpecialChars()}
	 *
	 * @param string $text
	 * @return string
	 */
	static public function undoHtmlSpecialChars($text) {
		return htmlspecialchars_decode($text, ENT_QUOTES);
	}

	/**
	 *
	 * @param unknown_type $text
	 */
	static public function htmlEntities($text) {
		return preg_replace(array("/&amp;/i", "/&nbsp;/i"), array('&', '&amp;nbsp;'), @htmlentities($text, ENT_QUOTES, _CHARSET));
	}

	/**
	 * Add slashes to escape special characters in text
	 * 
	 * When called without the second parameter, escapes characters that would be escaped by PHP's addslashes():
	 * single quote ('), double quote ("), backslash (\), and NUL byte (\0)
	 * 
	 * @param string $text The text to apply the slashes to
	 * @param string|null $param Optional. Which characters to apply the escaping to. 
	 *                            If null (default), uses the same characters as addslashes() for backwards compatibility.
	 * @return string The text with special characters escaped
	 */
	public static function addSlashes(string $text, ?string $param = null) {
		// Default to escaping the same characters as addslashes() for backwards compatibility
		if ($param === null) {
			$param = self::DEFAULT_ESCAPE_CHARS;
		}
		return addcslashes($text, $param);
	}

	/**
	 * Note: magic_quotes_gpc and magic_quotes_runtime are deprecated as of PHP5.3.0
	 * (and that should be always on >= PHP 5.4!!!)
	 *
	 * @param string $text
	 * @return string
	 * @deprecated we shouldn't be using this as a 'filter'
	 */
	public static function stripSlashesGPC($text) {
		return $text;
	}

	/**
	 * Filters Multidimensional Array Recursively removing keys with empty values
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param array $array Array to be filtered
	 * @return array $array
	 */
	static public function cleanArray($arr) {
		$rtn = array();

		foreach ($arr as $key => $a) {
			if (!is_array($a) && (!empty($a) || $a === 0)) {
				$rtn[$key] = $a;
			} elseif (is_array($a)) {
				if (count($a) > 0) {
					$a = self::cleanArray($a);
					$rtn[$key] = $a;
					if (count($a) == 0) {
						unset($rtn[$key]);
					}
				}
			}
		}
		return $rtn;
	}

	/*
	 * Public Function checks Variables using specified filter type
	 *
	 * @TODO needs error trapping for debug if invalid types and options used!!
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param string $data Data to be checked
	 * @param string $type Type of Filter To use for Validation
	 * Valid Filter Types:
	 * 'url' = Checks & validates URL
	 * 'email' = Checks & validates Email Addresses
	 * 'ip' = Checks & validates IP Addresses
	 * 'str' = Checks & Sanitizes String Values
	 * 'int' = Validates Integer Values
	 * 'html' = Validates HTML
	 * 'text' = Validates plain textareas (Non HTML)
	 * 'special' htmlspecialchars filter options
	 *
	 * @param mixed $options1 Options to use with specified filter
	 * Valid Filter Options:
	 * URL:
	 * 'scheme' = URL must be an RFC compliant URL (like http://example)
	 * 'host' = URL must include host name (like http://www.example.com)
	 * 'path' = URL must have a path after the domain name (like www.example.com/example1/)
	 * 'query' = URL must have a query string (like "example.php?name=Vaughan&age=34")
	 * EMAIL:
	 * 'true' = Generate an email address that is protected from spammers
	 * 'false' = Generate an email address that is NOT protected from spammers
	 * IP:
	 * 'ipv4' = Requires the value to be a valid IPv4 IP (like 255.255.255.255)
	 * 'ipv6' = Requires the value to be a valid IPv6 IP (like 2001:0db8:85a3:08d3:1319:8a2e:0370:7334)
	 * 'rfc' = Requires the value to be a RFC specified private range IP (like 192.168.0.1)
	 * 'res' = Requires that the value is not within the reserved IP range. both IPV4 and IPV6 values
	 * STR:
	 * 'noencode' = Do NOT encode quotes
	 * 'striplow' = Strip characters with ASCII value below 32
	 * 'striphigh' = Strip characters with ASCII value above 127
	 * 'encodelow' = Encode characters with ASCII value below 32
	 * 'encodehigh' = Encode characters with ASCII value above 127
	 * 'encodeamp' = Encode the & character to &amp;
	 * SPECIAL:
	 * 'striplow' = Strip characters with ASCII value below 32
	 * 'striphigh' = Strip characters with ASCII value above 32
	 * 'encodehigh' = Encode characters with ASCII value above 32
	 * INT:
	 * minimum integer range value
	 * HTML:
	 * 'input' = Filters HTML for input to DB
	 * 'output' = Filters HTML for rendering output
	 * 'print' = Filters HTML for output to Printer
	 * 'edit' = used for edit content forms
	 * TEXT:
	 * 'input' = Filters plain text for input to DB
	 * 'output' = Filters plain text for rendering output
	 * 'print' = Filters plain text for output to printer
	 *
	 * @param mixed $options2 Options to use with specified filter options1
	 * URL:
	 * 'true' = URLEncode the URL (ie. http://www.example > http%3A%2F%2Fwww.example)
	 * 'false' = Do Not URLEncode the URL
	 * EMAIL:
	 * 'true' = Reject if email is banned (Uses: $icmsConfigUser['bad_emails'])
	 * 'false' = Do Not use Email Blacklist
	 * IP:
	 * NOT USED!
	 * INT:
	 * maximum integer range value
	 *
	 * @return mixed
	 */

	/**
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param $data
	 * @param $type
	 * @param $options1
	 * @param $options2
	 */
	static public function checkVar($data, $type, $options1 = '', $options2 = '') {
		$valid_types = array('url', 'email', 'ip', 'str', 'int', 'special', 'html', 'text');

		if (!in_array($type, $valid_types)) {
			return false;
		} else {
			switch ($type) {
				case 'url':
					$valid_options1 = array('scheme', 'path', 'host', 'query');
					$valid_options2 = array(0, 1);
					if (!isset($options1) || $options1 == '' || !in_array($options1, $valid_options1)) {
						$options1 = '';
					}
					if (!isset($options2) || $options2 == '' || !in_array($options2, $valid_options2)) {
						$options2 = 0;
					} else {
						$options2 = 1;
					}
					break;

				case 'email':
					$valid_options1 = array(0, 1);
					$valid_options2 = array(0, 1);
					if (!isset($options1) || $options1 == '' || !in_array($options1, $valid_options1)) {
						$options1 = 0;
					} else {
						$options1 = 1;
					}
					if (!isset($options2) || $options2 == '' || !in_array($options2, $valid_options2)) {
						$options2 = 0;
					} else {
						$options2 = 1;
					}
					break;

				case 'ip':
					$valid_options1 = array('ipv4', 'ipv6', 'rfc', 'res');
					$options2 = '';
					if (!isset($options1) || $options1 == '' || !in_array($options1, $valid_options1)) {
						$options1 = 'ipv4';
					}
					break;

				case 'str':
					$valid_options1 = array('noencode', 'striplow', 'striphigh', 'encodelow', 'encodehigh', 'encodeamp');
					$options2 = '';

					if (!isset($options1) || $options1 == '' || !in_array($options1, $valid_options1)) {
						$options1 = '';
					}
					break;

				case 'special':
					$valid_options1 = array('striplow', 'striphigh', 'encodehigh');
					$options2 = '';

					if (!isset($options1) || $options1 == '' || !in_array($options1, $valid_options1)) {
						$options1 = '';
					}
					break;

				case 'int':
					if (!is_int($options1) || !is_int($options2)) {
						$options1 = '';
						$options2 = '';
					} else {
						$options1 = (int) $options1;
						$options2 = (int) $options2;
					}
					break;

				case 'html':
					$valid_options1 = array('input', 'output', 'print', 'edit');
					$options2 = '';
					if (!isset($options1) || $options1 == '' || !in_array($options1, $valid_options1)) {
						$options1 = 'input';
					}
					break;

				case 'text':
					$valid_options1 = array('input', 'output', 'print');
					$options2 = '';
					if (!isset($options1) || $options1 == '' || !in_array($options1, $valid_options1)) {
						$options1 = 'input';
					}
					break;
			}
		}
		return self::priv_checkVar($data, $type, $options1, $options2);
	}

	/**
	 * Filter an array of variables, such as $_GET or $_POST, using a set of filters.
	 *
	 * Any items in the input array not found in the filter array will be filtered as
	 * a string.
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param array $input items to be filtered
	 * @param array $filters the keys of this array should match the keys in
	 *        the input array and the values should be valid types
	 *        for the checkVar method
	 * @param bool $strict when true (default), items not in the filter array will be discarded
	 *        when false, items not in the filter array will be filtered as strings and included
	 * @return array
	 */
	static public function checkVarArray(array $input, array $filters, $strict = true) {
		foreach (array_intersect_key($input, $filters) as $key => $value) {
			$options[0] = $options[1] = '';
			if (array_key_exists('options', $filters) && isset($filters[$key]['options'][0])) {
				$options[0] = $filters[$key]['options'][0];
			}
			if (array_key_exists('options', $filters) && isset($filters[$key]['options'][1])) {
				$options[1] = $filters[$key]['options'][1];
			}
			if (is_array($filters[$key])) {
				$filter = $filters[$key][0];
			} else {
				$filter = $filters[$key];
			}
			if (is_array($input[$key])) {
				// pass the filter type into the array and all members in it
				$inner_filter = array_fill_keys(array_keys($input[$key]), $filter);
				$output[$key] = self::checkVarArray($input[$key], $inner_filter, true);
			} else {
				$output[$key] = self::checkVar($input[$key], $filter, $options[0], $options[1]);
			}
		}

		if (!$strict) {
			foreach ($diff = array_diff_key($input, $filters) as $key => $value) {
				if (is_array($diff[$key])) {
					$output[$key] = self::checkVarArray($diff[$key], array($key => 'str'), false);
				} else {
					$output[$key] = self::checkVar($diff[$key], 'str');
				}
			}
		}
		return $output;
	}

	/**
	 * Filters textarea form data for INPUt to DB (text only!!)
	 * For HTML please use icms_core_HTMLFilter::filterHTMLinput()
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param string $text
	 * @return string
	 */
	static public function filterTextareaInput($text) {
		icms::$preload->triggerEvent('beforeFilterTextareaInput', array(&$text));

		$text = self::htmlSpecialChars(strip_tags($text));

		icms::$preload->triggerEvent('afterFilterTextareaInput', array(&$text));

		return $text;
	}

	/**
	 * Filters textarea for DISPLAY purposes (text only!!)
	 * For HTML please use icms_core_HTMLFilter::filterHTMLdisplay()
	 *
	 * @param string $text
	 * @param bool $smiley allow smileys?
	 * @param bool $icode allow icmscode?
	 * @param bool $image allow inline images?
	 * @param bool $br convert linebreaks?
	 * @return string
	 */
	static public function filterTextareaDisplay($text, $smiley = 1, $icode = 1, $image = 1, $br = 1) {
		if($text) {
			icms::$preload->triggerEvent('beforeFilterTextareaDisplay', array(&$text, $smiley, $icode, $image, $br));

		// neccessary for the time being until we rework the IPF & Data Object Types in 2.0

			$text = str_replace('<!-- input filtered -->', '', $text);
			$text = str_replace('<!-- filtered with htmlpurifier -->', '', $text);

			$text = self::htmlSpecialChars($text);
			$text = self::codePreConv($text, $icode);
			$text = self::makeClickable($text);
			if ($smiley != 0) {
				$text = self::smiley($text);
			}
			if ($icode != 0) {
				if ($image != 0) {
					$text = self::codeDecode($text);
				} else {
					$text = self::codeDecode($text, 0);
				}
			}
			if ($br !== 0) {
				$text = self::nl2Br($text);
			}
			$text = self::codeConv($text, $icode, $image);

			icms::$preload->triggerEvent('afterFilterTextareaDisplay', array(&$text, $smiley, $icode, $image, $br));
			return $text;
		}
	}

	/**
	 * Filters HTML form data for INPUT to DB
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param string $html
	 * @param bool $smiley allow smileys?
	 * @param bool $icode allow icmscode?
	 * @param bool $image allow inline images?
	 * @return string
	 */
	static public function filterHTMLinput($html, $smiley = 1, $icode = 1, $image = 1, $br = 0) {
		icms::$preload->triggerEvent('beforeFilterHTMLinput', array(&$html, 1, 1, 1, $br));

		$html = str_replace('<!-- input filtered -->', '', $html);

		$html = self::codePreConv($html, 1);
		$html = self::smiley($html);
		$html = self::codeDecode($html);
		$html = self::codeConv($html, 1, 1);

		$html = icms_core_HTMLFilter::filterHTML($html);

		$purified = strpos($html, '<!-- filtered with htmlpurifier -->');
		if ($purified === false && $br == 1) {
			$html = self::nl2Br($html);
		}

		$html .= '<!-- input filtered -->';

		icms::$preload->triggerEvent('afterFilterHTMLinput', array(&$html, 1, 1, 1, $br));
		return $html;
	}

	/**
	 * Filters HTML form data for Display Only
	 * we don't really require the icmscode stuff, but we need to for content already in the DB before
	 * we start filtering on INPUT instead of OUTPUT!!
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param string $html
	 * @param bool $icode allow icmscode?
	 * @return string
	 */
	static public function filterHTMLdisplay($html, $icode = 1, $br = 0) {
		global $icmsConfig;

		icms::$preload->triggerEvent('beforeFilterHTMLdisplay', array(&$html, 1, $br));

		$ifiltered = strpos($html, '<!-- input filtered -->');
		if ($ifiltered === false) {
			$html = self::codePreConv($html, 1);
			$html = self::smiley($html);
			$html = self::codeDecode($html);
			$html = self::codeConv($html, 1, 1);

			$html = icms_core_HTMLFilter::filterHTML($html);

			// $html .= '<!-- warning! output filtered only -->';

			$purified = strpos($html, '<!-- filtered with htmlpurifier -->');
			if ($purified === false || $br == 1) {
				$html = self::nl2Br($html);
			}
		}

		if ($icmsConfig['debug_mode'] !== 0) {
			$purified = strpos($html, '<!-- filtered with htmlpurifier -->');
			if ($ifiltered === false) {
				if ($purified === false) {
					icms::$logger->addFilter(self::icms_substr($html, 0, 400), 3);
				} else {
					$html = str_replace('<!-- filtered with htmlpurifier -->', '', $html);
					icms::$logger->addFilter(self::icms_substr($html, 0, 400), 4);
				}
			} else {
				$html = str_replace('<!-- input filtered -->', '', $html);
				if ($purified === false) {
					icms::$logger->addFilter(self::icms_substr($html, 0, 400), 1);
				} else {
					$html = str_replace('<!-- filtered with htmlpurifier -->', '', $html);
					icms::$logger->addFilter(self::icms_substr($html, 0, 400), 2);
				}
			}
		} else {
			$html = str_replace('<!-- input filtered -->', '', $html);
			$html = str_replace('<!-- filtered with htmlpurifier -->', '', $html);
		}

		$html = self::makeClickable($html);
		$html = self::censorString($html);

		icms::$preload->triggerEvent('afterFilterHTMLdisplay', array(&$html, 1, $br));
		return $html;
	}

	/**
	 * Replace icmsCodes with their equivalent HTML formatting
	 *
	 * @param string $text
	 * @param bool $allowimage Allow images in the text?
	 *        On false, uses links to images.
	 * @return string
	 */
	static public function codeDecode(&$text, $allowimage = 1) {
		$patterns = array();
		$replacements = array();
		$patterns[] = "/\[siteurl=(['\"]?)([^\"'<>]*)\\1](.*)\[\/siteurl\]/sU";
		$replacements[] = '<a href="' . ICMS_URL . '/\\2">\\3</a>';
		$patterns[] = "/\[url=(['\"]?)(http[s]?:\/\/[^\"'<>]*)\\1](.*)\[\/url\]/sU";
		$replacements[] = '<a href="\\2" rel="external">\\3</a>';
		$patterns[] = "/\[url=(['\"]?)(ftp?:\/\/[^\"'<>]*)\\1](.*)\[\/url\]/sU";
		$replacements[] = '<a href="\\2" rel="external">\\3</a>';
		$patterns[] = "/\[url=(['\"]?)([^\"'<>]*)\\1](.*)\[\/url\]/sU";
		$replacements[] = '<a href="http://\\2" rel="external">\\3</a>';
		$patterns[] = "/\[color=(['\"]?)([a-zA-Z0-9]*)\\1](.*)\[\/color\]/sU";
		$replacements[] = '<span style="color: #\\2;">\\3</span>';
		$patterns[] = "/\[size=(['\"]?)([a-z0-9-]*)\\1](.*)\[\/size\]/sU";
		$replacements[] = '<span style="font-size: \\2;">\\3</span>';
		$patterns[] = "/\[font=(['\"]?)([^;<>\*\(\)\"']*)\\1](.*)\[\/font\]/sU";
		$replacements[] = '<span style="font-family: \\2;">\\3</span>';
		$patterns[] = "/\[email]([^;<>\*\(\)\"']*)\[\/email\]/sU";
		$replacements[] = '<a href="mailto:\\1">\\1</a>';
		$patterns[] = "/\[b](.*)\[\/b\]/sU";
		$replacements[] = '<strong>\\1</strong>';
		$patterns[] = "/\[i](.*)\[\/i\]/sU";
		$replacements[] = '<em>\\1</em>';
		$patterns[] = "/\[u](.*)\[\/u\]/sU";
		$replacements[] = '<u>\\1</u>';
		$patterns[] = "/\[d](.*)\[\/d\]/sU";
		$replacements[] = '<del>\\1</del>';
		$patterns[] = "/\[center](.*)\[\/center\]/sU";
		$replacements[] = '<div align="center">\\1</div>';
		$patterns[] = "/\[left](.*)\[\/left\]/sU";
		$replacements[] = '<div align="left">\\1</div>';
		$patterns[] = "/\[right](.*)\[\/right\]/sU";
		$replacements[] = '<div align="right">\\1</div>';
		$patterns[] = "/\[img align=center](.*)\[\/img\]/sU";
		if ($allowimage != 1) {
			$replacements[] = '<div style="margin: 0 auto; text-align: center;"><a href="\\1" rel="external">\\1</a></div>';
		} else {
			$replacements[] = '<div style="margin: 0 auto; text-align: center;"><img src="\\1" alt="" /></div>';
		}
		$patterns[] = "/\[img align=(['\"]?)(left|right)\\1]([^\"\(\)\?\&'<>]*)\[\/img\]/sU";
		$patterns[] = "/\[img]([^\"\(\)\?\&'<>]*)\[\/img\]/sU";
		$patterns[] = "/\[img align=(['\"]?)(left|right)\\1 id=(['\"]?)([0-9]*)\\3]([^\"\(\)\?\&'<>]*)\[\/img\]/sU";
		$patterns[] = "/\[img id=(['\"]?)([0-9]*)\\1]([^\"\(\)\?\&'<>]*)\[\/img\]/sU";
		if ($allowimage != 1) {
			$replacements[] = '<a href="\\3" rel="external">\\3</a>';
			$replacements[] = '<a href="\\1" rel="external">\\1</a>';
			$replacements[] = '<a href="' . ICMS_URL . '/image.php?id=\\4" rel="external">\\5</a>';
			$replacements[] = '<a href="' . ICMS_URL . '/image.php?id=\\2" rel="external">\\3</a>';
		} else {
			$replacements[] = '<img src="\\3" align="\\2" alt="" />';
			$replacements[] = '<img src="\\1" alt="" />';
			$replacements[] = '<img src="' . ICMS_URL . '/image.php?id=\\4" align="\\2" alt="\\5" />';
			$replacements[] = '<img src="' . ICMS_URL . '/image.php?id=\\2" alt="\\3" />';
		}
		$patterns[] = "/\[quote]/sU";
		$replacements[] = _QUOTEC . '<div class="icmsQuote"><blockquote><p>';
		$patterns[] = "/\[\/quote]/sU";
		$replacements[] = '</p></blockquote></div>';
		$text = str_replace("\x00", "", $text);
		$c = "[\x01-\x1f]*";
		$patterns[] = "/j{$c}a{$c}v{$c}a{$c}s{$c}c{$c}r{$c}i{$c}p{$c}t{$c}:/si";
		$replacements[] = "(script removed)";
		$patterns[] = "/a{$c}b{$c}o{$c}u{$c}t{$c}:/si";
		$replacements[] = "about :";
		$text = preg_replace($patterns, $replacements, $text);
		$text = self::codeDecode_extended($text);
		return $text;
	}

	/**
	 * Make links in the text clickable
	 *
	 * @param string $text
	 * @return string
	 */
	static public function makeClickable($text) {
		global $icmsConfigPersona;
		$text = ' ' . $text;
		$patterns = array("/(^|[^]_a-z0-9-=\"'\/])([a-z]+?):\/\/([^, \r\n\"\(\)'<>]+)/i", "/(^|[^]_a-z0-9-=\"'\/])www\.([a-z0-9\-]+)\.([^, \r\n\"\(\)'<>]+)/i", "/(^|[^]_a-z0-9-=\"'\/])ftp\.([a-z0-9\-]+)\.([^,\r\n\"\(\)'<>]+)/i" /* , "/(^|[^]_a-z0-9-=\"'\/:\.])([a-z0-9\-_\.]+?)@([^, \r\n\"\(\)'<>\[\]]+)/i" */
		);
		$replacements = array("\\1<a href=\"\\2://\\3\" rel=\"external\">\\2://\\3</a>", "\\1<a href=\"http://www.\\2.\\3\" rel=\"external\">www.\\2.\\3</a>", "\\1<a href=\"ftp://ftp.\\2.\\3\" rel=\"external\">ftp.\\2.\\3</a>" /* , "\\1<a href=\"mailto:\\2@\\3\">\\2@\\3</a>" */
		);
		$text = preg_replace($patterns, $replacements, $text);
		if ($icmsConfigPersona['shorten_url'] == 1) {
			$links = explode('<a', $text);
			$countlinks = count($links);
			for ($i = 0; $i < $countlinks; $i++ ) {
				$link = $links[$i];
				$link = (preg_match('#(.*)(href=")#is', $link)) ? '<a' . $link : $link;
				$begin = strpos($link, '>') + 1;
				$end = strpos($link, '<', $begin);
				$length = $end - $begin;
				$urlname = substr($link, $begin, $length);

				$maxlength = (int) ($icmsConfigPersona['max_url_long']);
				$cutlength = (int) ($icmsConfigPersona['pre_chars_left']);
				$endlength = -(int) ($icmsConfigPersona['last_chars_left']);
				$middleurl = " ... ";
				$chunked = (strlen($urlname) > $maxlength && preg_match('#^(https://|http://|ftp://|www\.)#is', $urlname)) ? substr_replace($urlname, $middleurl, $cutlength, $endlength) : $urlname;
				$text = str_replace('>' . $urlname . '<', '>' . $chunked . '<', $text);
			}
		}
		$text = substr($text, 1);
		return $text;
	}

	/**
	 *
	 * @param $message
	 */
	static public function smiley($message) {
		return self::priv_smiley($message);
	}

	/**
	 *
	 * @param $message
	 */
	static public function getSmileys($all = false) {
		return self::priv_getSmileys($all);
	}

	/**
	 * Replaces banned words in a string with their replacements
	 *
	 * @param string $text
	 * @return string
	 *
	 */
	static public function censorString(&$text) {
		$icmsConfigCensor = icms::$config->getConfigsByCat(ICMS_CONF_CENSOR);
		if ($icmsConfigCensor['censor_enable'] == true) {
			$replacement = $icmsConfigCensor['censor_replace'];
			if (!empty($icmsConfigCensor['censor_words'])) {
				foreach ($icmsConfigCensor['censor_words'] as $bad) {
					if (!empty($bad)) {
						$bad = quotemeta($bad);
						$patterns[] = "/(\s)" . $bad . "/siU";
						$replacements[] = "\\1" . $replacement;
						$patterns[] = "/^" . $bad . "/siU";
						$replacements[] = $replacement;
						$patterns[] = "/(\n)" . $bad . "/siU";
						$replacements[] = "\\1" . $replacement;
						$patterns[] = "/]" . $bad . "/siU";
						$replacements[] = "]" . $replacement;
						$text = preg_replace($patterns, $replacements, $text);
					}
				}
			}
		}
		return $text;
	}

	/**
	 * #@+
	 * Sanitizing of [code] tag
	 */
	static public function codePreConv($text, $imcode = 1) {
		if ($imcode != 0) {
			$patterns = "/\[code](.*)\[\/code\]/sU";
			$text = preg_replace_callback($patterns, function ($match) {
				return '[code]' . base64_encode($match[1]) . '[/code]';
			}, $text);
		}
		return $text;
	}

	/**
	 * Converts text to imcode
	 *
	 * @param string $text Text to convert
	 * @param int $imcode Is the code Xcode?
	 * @param int $image configuration for the purifier
	 * @return string $text the converted text
	 */
	static public function codeConv($text, $imcode = 1, $image = 1) {
		if ($imcode != 0) {
			$patterns = "/\[code](.*)\[\/code\]/sU";
			$text = preg_replace_callback($patterns, function ($matches) use ($image) {
				$code = icms_core_DataFilter::codeSanitizer($matches[1], ($image != 0) ? 1 : 0);
				return '<div class="icmsCode">' . $code . '</div>';
			}, $text);
		}
		return $text;
	}

	/**
	 * Sanitizes decoded string
	 *
	 * @param string $str String to sanitize
	 * @param string $image Is the string an image
	 * @return string $str The sanitized decoded string
	 */
	static public function codeSanitizer($str, $image = 1) {
		$str = self::htmlSpecialChars(str_replace('\"', '"', base64_decode($str)));
		$str = self::codeDecode($str, $image);
		return $str;
	}

	/**
	 * This function gets allowed plugins from DB and loads them in the sanitizer
	 *
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param int $id ID of the config
	 * @param bool $withoptions load the config's options now?
	 * @return object reference to the {@link IcmsConfig}
	 */
	static public function codeDecode_extended($text, $allowimage = 1) {
		global $icmsConfigPlugins;
		if (!empty($icmsConfigPlugins['sanitizer_plugins'])) {
			foreach ($icmsConfigPlugins['sanitizer_plugins'] as $item) {
				$text = self::executeExtension($item, $text);
			}
		}
		return $text;
	}

	/**
	 * loads the textsanitizer plugins
	 *
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param string $name Name of the extension to load
	 * @return bool
	 */
	static public function loadExtension($name) {
		if (empty($name) || !include_once ICMS_PLUGINS_PATH . "/textsanitizer/{$name}/{$name}.php") {
			return false;
		}
	}

	/**
	 * Executes file with a certain extension using call_user_func_array
	 *
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param string $name Name of the file to load
	 * @param string $text Text to show if the function doesn't exist
	 * @return array the return of the called function
	 */
	static public function executeExtension($name, $text) {
		self::loadExtension($name);
		$func = "textsanitizer_{$name}";
		if (!function_exists($func)) {
			return $text;
		}
		$args = array_slice(func_get_args(), 1);
		return call_user_func_array($func, $args);
	}

	/**
	 * Syntaxhighlight the code
	 *
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param string $text purifies (lightly) and then syntax highlights the text
	 * @return string $text the syntax highlighted text
	 */
	static public function textsanitizer_syntaxhighlight(&$text) {
		global $icmsConfigPlugins;
		if ($icmsConfigPlugins['code_sanitizer'] == 'php') {
			$text = self::undoHtmlSpecialChars($text);
			$text = self::textsanitizer_php_highlight($text);
		} elseif ($icmsConfigPlugins['code_sanitizer'] == 'geshi') {
			$text = self::undoHtmlSpecialChars($text);
			$text = '<code>' . self::textsanitizer_geshi_highlight($text) . '</code>';
		} else {
			$text = '<pre><code>' . $text . '</code></pre>';
		}
		return $text;
	}

	/**
	 * Syntaxhighlight the code using PHP highlight
	 *
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param string $text Text to highlight
	 * @return string $buffer the highlighted text
	 */
	static public function textsanitizer_php_highlight($text) {
		$text = trim($text);
		$addedtag_open = 0;
		if (!strpos($text, '<?php') and (substr($text, 0, 5) != '<?php')) {
			$text = "<?php\n" . $text;
			$addedtag_open = 1;
		}
		$addedtag_close = 0;
		if (!strpos($text, '?>')) {
			$text .= '?>';
			$addedtag_close = 1;
		}
		$oldlevel = error_reporting(0);
		$buffer = highlight_string($text, true);
		error_reporting($oldlevel);
		$pos_open = $pos_close = 0;
		if ($addedtag_open) {
			$pos_open = strpos($buffer, '&lt;?php');
		}
		if ($addedtag_close) {
			$pos_close = strrpos($buffer, '?&gt;');
		}

		$str_open = ($addedtag_open) ? substr($buffer, 0, $pos_open) : '';
		$str_close = ($pos_close) ? substr($buffer, $pos_close + 5) : '';

		$length_open = ($addedtag_open) ? $pos_open + 8 : 0;
		$length_text = ($pos_close) ? $pos_close - $length_open : 0;
		$str_internal = ($length_text) ? substr($buffer, $length_open, $length_text) : substr($buffer, $length_open);

		$buffer = $str_open . $str_internal . $str_close;
		return $buffer;
	}

	/**
	 * Syntaxhighlight the code using Geshi highlight
	 *
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param string $text The text to highlight
	 * @return string $code the highlighted text
	 */
	static public function textsanitizer_geshi_highlight($text) {
		global $icmsConfigPlugins;

		if (!@include_once ICMS_LIBRARIES_PATH . '/geshi/geshi.php') return false;

		$language = str_replace('.php', '', $icmsConfigPlugins['geshi_default']);

		// Create the new GeSHi object, passing relevant stuff
		$geshi = new GeSHi($text, $language);

		// Enclose the code in a <div>
		$geshi->set_header_type(GESHI_HEADER_NONE);

		// Sets the proper encoding charset other than "ISO-8859-1"
		$geshi->set_encoding(_CHARSET);

		$geshi->set_link_target('_blank');

		// Parse the code
		$code = $geshi->parse_code();

		return $code;
	}

	/**
	 * Trims certain text
	 *
	 * Replaces include/functions.php :: xoops_trim()
	 *
	 * @param string $text The Text to trim
	 * @return string $text The trimmed text
	 */
	static public function icms_trim($text) {
		if (function_exists('xoops_language_trim')) {
			return xoops_language_trim($text);
		}
		return trim($text);
	}

	/**
	 * Function to reverse given text with utf-8 character sets
	 *
	 * credit for this function should goto lwc courtesy of php.net.
	 *
	 * @param string $str The text to be reversed.
	 * @param string $reverse true will reverse everything including numbers, false will reverse text only but numbers will be left intact.
	 *        example: when true: impresscms 2008 > 8002 smcsserpmi, false: impresscms 2008 > 2008 smcsserpmi
	 * @return string
	 */
	static public function utf8_strrev($str, $reverse = false) {
		preg_match_all('/./us', $str, $ar);
		if ($reverse) {
			return join('', array_reverse($ar[0]));
		} else {
			$temp = array();
			foreach ($ar[0] as $value) {
				if (is_numeric($value) && !empty($temp[0]) && is_numeric($temp[0])) {
					foreach ($temp as $key => $value2) {
						if (is_numeric($value2)) {
							$pos = ($key + 1);
						} else {
							break;
						}
						$temp2 = array_splice($temp, $pos);
						$temp = array_merge($temp, array($value), $temp2);
					}
				} else {
					array_unshift($temp, $value);
				}
			}
			return implode('', $temp);
		}
	}

	/**
	 * Returns the portion of string specified by the start and length parameters.
	 * If $trimmarker is supplied, it is appended to the return string.
	 * This function works fine with multi-byte characters if mb_* functions exist on the server.
	 *
	 * Replaces legacy include/functions.php :: xoops_substr()
	 *
	 * @param string $str
	 * @param int $start
	 * @param int $length
	 * @param string $trimmarker
	 *
	 * @return string
	 */
	static public function icms_substr($str, $start, $length, $trimmarker = '...') {
		global $icmsConfigMultilang;

		if ($icmsConfigMultilang['ml_enable']) {
			$tags = explode(',', $icmsConfigMultilang['ml_tags']);
			$strs = array();
			$hasML = false;
			foreach ($tags as $tag) {
				if (preg_match("/\[" . $tag . "](.*)\[\/" . $tag . "\]/sU", $str, $matches)) {
					if (count($matches) > 0) {
						$hasML = true;
						$strs[] = $matches[1];
					}
				}
			}
		} else {
			$hasML = false;
		}

		if (!$hasML) {
			$strs = array($str);
		}

		for ($i = 0; $i <= count($strs) - 1; $i++ ) {
			if (!XOOPS_USE_MULTIBYTES) {
				$strs[$i] = (strlen($strs[$i]) - $start <= $length) ? substr($strs[$i], $start, $length) : substr($strs[$i], $start, $length - strlen($trimmarker)) . $trimmarker;
			}
			if (function_exists('mb_internal_encoding') && @mb_internal_encoding(_CHARSET)) {
				$str2 = mb_strcut($strs[$i], $start, $length - strlen($trimmarker));
				$strs[$i] = $str2 . (mb_strlen($strs[$i]) != mb_strlen($str2) ? $trimmarker : '');
			}

			$DEP_CHAR = 127;
			$pos_st = 0;
			$action = false;
			for ($pos_i = 0; $pos_i < strlen($strs[$i]); $pos_i++ ) {
				if (ord(substr($strs[$i], $pos_i, 1)) > 127) {
					$pos_i++ ;
				}
				if ($pos_i <= $start) {
					$pos_st = $pos_i;
				}
				if ($pos_i >= $pos_st + $length) {
					$action = true;
					break;
				}
			}
			$strs[$i] = ($action) ? substr($strs[$i], $pos_st, $pos_i - $pos_st - strlen($trimmarker)) . $trimmarker : $strs[$i];
			$strs[$i] = ($hasML) ? '[' . $tags[$i] . ']' . $strs[$i] . '[/' . $tags[$i] . ']' : $strs[$i];
		}
		$str = implode('', $strs);
		return $str;
	}

	// -------- Private Functions --------

	/**
	 * Private Function checks & Validates Data
	 *
	 * @copyright The ImpressCMS Project <http://www.impresscms.org>
	 *
	 *            See public function checkVar() for parameters
	 *
	 * @return
	 */
	static private function priv_checkVar($data, $type, $options1, $options2) {
		switch ($type) {
			case "url": // returns False if URL invalid, returns $string if Valid
				$data = filter_var($data, FILTER_SANITIZE_URL);

				switch ($options1) {
					case "scheme":
						$valid = filter_var($data, FILTER_VALIDATE_URL);
						break;

					case "host":
						$valid = filter_var($data, FILTER_VALIDATE_URL);
						break;

					case "path":
						$valid = filter_var($data, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED);
						break;

					case "query":
						$valid = filter_var($data, FILTER_VALIDATE_URL, FILTER_FLAG_QUERY_REQUIRED);
						break;

					default:
						$valid = filter_var($data, FILTER_VALIDATE_URL);
						break;
				}
				if ($valid) {
					if (isset($options2) && $options2 == 1) {
						return filter_var($data, FILTER_SANITIZE_ENCODED);
					}
					return $data;
				}
				return false;
				break;

			case "email": // returns False if email is invalid, returns $string if valid
				global $icmsConfigUser;

				$data = filter_var($data, FILTER_SANITIZE_EMAIL);

				if (filter_var($data, FILTER_VALIDATE_EMAIL)) {
					if ($options2 == 1 && is_array($icmsConfigUser['bad_emails'])) {
						foreach ($icmsConfigUser['bad_emails'] as $be) {
							if ((!empty($be) && preg_match('/' . $be . '/i', $data))) return false;
						}
						$icmsStopSpammers = new icms_core_StopSpammer();
						if ($icmsStopSpammers->badEmail($data)) return false;
					}
				} else {
					return false;
				}
				if ($options1 == 1) {
					$data = str_replace('@', ' at ', $data);
					$data = str_replace('.', ' dot ', $data);
				}
				return $data;
				break;

			case "ip": // returns False if IP is invalid, returns true if valid
				switch ($options1) {
					case "ipv4":
						return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
						break;

					case "ipv6":
						return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
						break;

					case "rfc":
						return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE);
						break;

					case "res":
						return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE);
						break;

					default:
						return filter_var($data, FILTER_VALIDATE_IP);
						break;
				}
				break;

			case 'str': // returns $string
				switch ($options1) {
					case "noencode":
						//return filter_var($data, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
						return htmlspecialchars(strip_tags($data), ENT_NOQUOTES);
						break;

					case "striplow":
						return filter_var(strip_tags($data), FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW);
						break;

					case "striphigh":
						return filter_var(strip_tags($data), FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_HIGH);
						break;

					case "encodelow":
						return filter_var(strip_tags($data), FILTER_UNSAFE_RAW, FILTER_FLAG_ENCODE_LOW);
						break;

					case "encodehigh":
						return filter_var(strip_tags($data), FILTER_UNSAFE_RAW, FILTER_FLAG_ENCODE_HIGH);
						break;

					case "encodeamp":
						return filter_var(strip_tags($data), FILTER_UNSAFE_RAW, FILTER_FLAG_ENCODE_AMP);
						break;

					default:
						return htmlspecialchars(strip_tags($data), ENT_NOQUOTES);
						break;
				}
				break;

			case "int": // returns $int, returns false if $opt1 & 2 set & $data is not inbetween values of $opt1 & 2
				if ((isset($options1) && is_int($options1)) && (isset($options2) && is_int($options2))) {
					$option = array('options' => array('min_range' => $options1, 'max_range' => $options2));

					return filter_var($data, FILTER_VALIDATE_INT, $option);
				} else {
					return filter_var($data, FILTER_VALIDATE_INT);
				}
				break;

			case "special": // returns $string
				switch ($options1) {
					case "striplow":
						return filter_var($data, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_LOW);
						break;

					case "striphigh":
						return filter_var($data, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_STRIP_HIGH);
						break;

					case "encodehigh":
						return filter_var($data, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_FLAG_ENCODE_HIGH);
						break;

					default:
						return filter_var($data, FILTER_SANITIZE_SPECIAL_CHARS);
						break;
				}
				break;

			case "html": // returns $string
				switch ($options1) {
					case 'input':
					default:
						return self::filterHTMLinput($data);
						break;

					case 'output':
						return self::filterHTMLdisplay($data);
						break;

					case 'edit':
						$filtered = strpos($data, '<!-- input filtered -->');
						if ($filtered !== false) {
							$data = str_replace('<!-- input filtered -->', '', $data);
							$data = str_replace('<!-- filtered with htmlpurifier -->', '', $data);
						}
						return htmlspecialchars($data, ENT_QUOTES, _CHARSET, false);
						break;

					case 'print':
						// do nothing yet
						break;
				}
				break;

			case "text": // returns $string
				switch ($options1) {
					case 'input':
					default:
						return self::filterTextareaInput($data);
						break;

					case 'output':
						return self::filterTextareaDisplay($data);
						break;

					case 'print':
						// do nothing yet
						break;
				}
				break;
		}
	}

	/**
	 * Replace emoticons in the message with smiley images
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param string $message
	 * @return string
	 */
	static private function priv_smiley($message) {
		$smileys = self::priv_getSmileys(true);
		foreach ($smileys as $smile) {
			$message = str_replace($smile['code'], '<img src="' . ICMS_UPLOAD_URL . '/' . htmlspecialchars($smile['smile_url']) . '" alt="" />', $message);
		}
		return $message;
	}

	/**
	 * Get the smileys
	 *
	 * @author vaughan montgomery (vaughan@impresscms.org)
	 * @copyright (c) 2007-2010 The ImpressCMS Project - www.impresscms.org
	 *
	 * @param bool $all
	 * @return array
	 */
	static private function priv_getSmileys($all = false) {
		if (count(self::$allSmileys) == 0) {
			if ($result = icms::$xoopsDB->query("SELECT * FROM " . icms::$xoopsDB->prefix('smiles'))) {
				while ($smiley = icms::$xoopsDB->fetchArray($result)) {
					if ($smiley['display']) {
						array_push(self::$displaySmileys, $smiley);
					}
					array_push(self::$allSmileys, $smiley);
				}
			}
		}
		return $all ? self::$allSmileys : self::$displaySmileys;
	}
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions