PageRenderTime 55ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 1ms

/wire/core/Sanitizer.php

http://github.com/ryancramerdesign/ProcessWire
PHP | 1595 lines | 819 code | 159 blank | 617 comment | 282 complexity | b81c5530dfb74248b4ad6da683bc772e MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * ProcessWire Sanitizer
  4. *
  5. * Sanitizer provides shared sanitization functions as commonly used throughout ProcessWire core and modules
  6. *
  7. * Modules may also add methods to the Sanitizer as needed i.e. $this->sanitizer->addHook('myMethod', $myClass, 'myMethod');
  8. * See the Wire class definition for more details about the addHook method.
  9. *
  10. * ProcessWire 2.x
  11. * Copyright (C) 2015 by Ryan Cramer
  12. * This file licensed under Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/
  13. *
  14. * https://processwire.com
  15. *
  16. * @link https://processwire.com/api/variables/sanitizer/ Offical $sanitizer API variable Documentation
  17. *
  18. * @method array($value, $sanitizer = null, array $options = array())
  19. *
  20. */
  21. class Sanitizer extends Wire {
  22. /**
  23. * May be passed to pageName for the $beautify param, see pageName for details.
  24. *
  25. */
  26. const translate = 2;
  27. /**
  28. * Caches the status of multibyte support.
  29. *
  30. */
  31. protected $multibyteSupport = false;
  32. /**
  33. * Array of allowed ascii characters for name filters
  34. *
  35. */
  36. protected $allowedASCII = array();
  37. /**
  38. * Construct the sanitizer
  39. *
  40. */
  41. public function __construct() {
  42. $this->multibyteSupport = function_exists("mb_strlen");
  43. if($this->multibyteSupport) mb_internal_encoding("UTF-8");
  44. $this->allowedASCII = str_split('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789');
  45. }
  46. /*************************************************************************************************************
  47. * STRING SANITIZERS
  48. *
  49. */
  50. /**
  51. * Internal filter used by other name filtering methods in this class
  52. *
  53. * @param string $value Value to filter
  54. * @param array $allowedExtras Additional characters that are allowed in the value
  55. * @param string 1 character replacement value for invalid characters
  56. * @param bool $beautify Whether to beautify the string, specify Sanitizer::translate to perform transliteration.
  57. * @param int $maxLength
  58. * @return string
  59. *
  60. */
  61. public function nameFilter($value, array $allowedExtras, $replacementChar, $beautify = false, $maxLength = 128) {
  62. static $replacements = array();
  63. if(!is_string($value)) $value = $this->string($value);
  64. $allowed = array_merge($this->allowedASCII, $allowedExtras);
  65. $needsWork = strlen(str_replace($allowed, '', $value));
  66. $extras = implode('', $allowedExtras);
  67. if($beautify && $needsWork) {
  68. if($beautify === self::translate && $this->multibyteSupport) {
  69. $value = mb_strtolower($value);
  70. if(empty($replacements)) {
  71. $configData = $this->wire('modules')->getModuleConfigData('InputfieldPageName');
  72. $replacements = empty($configData['replacements']) ? InputfieldPageName::$defaultReplacements : $configData['replacements'];
  73. }
  74. foreach($replacements as $from => $to) {
  75. if(mb_strpos($value, $from) !== false) {
  76. $value = mb_eregi_replace($from, $to, $value);
  77. }
  78. }
  79. }
  80. $v = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $value);
  81. if($v) $value = $v;
  82. $needsWork = strlen(str_replace($allowed, '', $value));
  83. }
  84. if(strlen($value) > $maxLength) $value = substr($value, 0, $maxLength);
  85. if($needsWork) {
  86. $value = str_replace(array("'", '"'), '', $value); // blank out any quotes
  87. $value = filter_var($value, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_NO_ENCODE_QUOTES);
  88. $hyphenPos = strpos($extras, '-');
  89. if($hyphenPos !== false && $hyphenPos !== 0) {
  90. // if hyphen present, ensure it's first (per PCRE requirements)
  91. $extras = '-' . str_replace('-', '', $extras);
  92. }
  93. $chars = $extras . 'a-zA-Z0-9';
  94. $value = preg_replace('{[^' . $chars . ']}', $replacementChar, $value);
  95. }
  96. // remove leading or trailing dashes, underscores, dots
  97. if($beautify) {
  98. if(strpos($extras, $replacementChar) === false) $extras .= $replacementChar;
  99. $value = trim($value, $extras);
  100. }
  101. return $value;
  102. }
  103. /**
  104. * Standard alphanumeric and dash, underscore, dot name
  105. *
  106. * @param string $value
  107. * @param bool|int $beautify Should be true when creating a name for the first time. Default is false.
  108. * You may also specify Sanitizer::translate (or number 2) for the $beautify param, which will make it translate letters
  109. * based on the InputfieldPageName custom config settings.
  110. * @param int $maxLength Maximum number of characters allowed in the name
  111. * @param string $replacement Replacement character for invalid chars: one of -_.
  112. * @param array $options Extra options to replace default 'beautify' behaviors
  113. * - allowAdjacentExtras (bool): Whether to allow [-_.] chars next to each other (default=false)
  114. * - allowDoubledReplacement (bool): Whether to allow two of the same replacement chars [-_] next to each other (default=false)
  115. * - allowedExtras (array): Specify allowed characters (default=[-_.], not including the brackets)
  116. * @return string
  117. *
  118. */
  119. public function name($value, $beautify = false, $maxLength = 128, $replacement = '_', $options = array()) {
  120. if(!empty($options['allowedExtras']) && is_array($options['allowedExtras'])) {
  121. $allowedExtras = $options['allowedExtras'];
  122. $allowedExtrasStr = implode('', $allowedExtras);
  123. } else {
  124. $allowedExtras = array('-', '_', '.');
  125. $allowedExtrasStr = '-_.';
  126. }
  127. $value = $this->nameFilter($value, $allowedExtras, $replacement, $beautify, $maxLength);
  128. if($beautify) {
  129. $hasExtras = false;
  130. foreach($allowedExtras as $c) {
  131. $hasExtras = strpos($value, $c) !== false;
  132. if($hasExtras) break;
  133. }
  134. if($hasExtras) {
  135. if(empty($options['allowAdjacentExtras'])) {
  136. // replace any of '-_.' next to each other with a single $replacement
  137. $value = preg_replace('/[' . $allowedExtrasStr . ']{2,}/', $replacement, $value);
  138. }
  139. if(empty($options['allowDoubledReplacement'])) {
  140. // replace double'd replacements
  141. $r = "$replacement$replacement";
  142. if(strpos($value, $r) !== false) $value = preg_replace('/' . $r . '+/', $replacement, $value);
  143. }
  144. // replace double dots
  145. if(strpos($value, '..') !== false) $value = preg_replace('/\.\.+/', '.', $value);
  146. }
  147. if(strlen($value) > $maxLength) $value = substr($value, 0, $maxLength);
  148. }
  149. return $value;
  150. }
  151. /**
  152. * Standard alphanumeric and dash, underscore, dot name plus multiple names may be separated by a delimeter
  153. *
  154. * @param string|array $value Value(s) to filter
  155. * @param string $delimeter Character that delimits values (optional)
  156. * @param array $allowedExtras Additional characters that are allowed in the value (optional)
  157. * @param string 1 character replacement value for invalid characters (optional)
  158. * @param bool $beautify
  159. * @return string|array Return string if given a string for $value, returns array if given an array for $value
  160. *
  161. */
  162. public function names($value, $delimeter = ' ', $allowedExtras = array('-', '_', '.'), $replacementChar = '_', $beautify = false) {
  163. $isArray = false;
  164. if(is_array($value)) {
  165. $isArray = true;
  166. $value = implode(' ', $value);
  167. }
  168. $replace = array(',', '|', ' ');
  169. if($delimeter != ' ' && !in_array($delimeter, $replace)) $replace[] = $delimeter;
  170. $value = str_replace($replace, ' ', $value);
  171. $allowedExtras[] = ' ';
  172. $value = $this->nameFilter($value, $allowedExtras, $replacementChar, $beautify, 8192);
  173. if($delimeter != ' ') $value = str_replace(' ', $delimeter, $value);
  174. if($isArray) $value = explode($delimeter, $value);
  175. return $value;
  176. }
  177. /**
  178. * Standard alphanumeric and underscore, per class or variable names in PHP
  179. *
  180. * @param string $value
  181. * @return string
  182. *
  183. */
  184. public function varName($value) {
  185. return $this->nameFilter($value, array('_'), '_');
  186. }
  187. /**
  188. * Name filter as used by ProcessWire Fields
  189. *
  190. * Note that dash and dot are excluded because they aren't allowed characters in PHP variables
  191. *
  192. * @param string $value
  193. * @param bool|int $beautify Should be true when creating a name for the first time. Default is false.
  194. * You may also specify Sanitizer::translate (or number 2) for the $beautify param, which will make it translate letters
  195. * based on the InputfieldPageName custom config settings.
  196. * @param int $maxLength Maximum number of characters allowed in the name
  197. * @return string
  198. *
  199. */
  200. public function fieldName($value, $beautify = false, $maxLength = 128) {
  201. return $this->nameFilter($value, array('_'), '_', $beautify, $maxLength);
  202. }
  203. /**
  204. * Name filter as used by ProcessWire Templates
  205. *
  206. * @param string $value
  207. * @param bool|int $beautify Should be true when creating a name for the first time. Default is false.
  208. * You may also specify Sanitizer::translate (or number 2) for the $beautify param, which will make it translate letters
  209. * based on the InputfieldPageName custom config settings.
  210. * @param int $maxLength Maximum number of characters allowed in the name
  211. * @return string
  212. *
  213. */
  214. public function templateName($value, $beautify = false, $maxLength = 128) {
  215. return $this->nameFilter($value, array('_', '-'), '-', $beautify, $maxLength);
  216. }
  217. /**
  218. * Name filter for ProcessWire Page names
  219. *
  220. * Because page names are often generated from a UTF-8 title, UTF8 to ASCII conversion will take place when $beautify is on
  221. *
  222. * @param string $value
  223. * @param bool|int $beautify Should be true when creating a Page's name for the first time. Default is false.
  224. * You may also specify Sanitizer::translate (or number 2) for the $beautify param, which will make it translate letters
  225. * based on the InputfieldPageName custom config settings.
  226. * @param int $maxLength Maximum number of characters allowed in the name
  227. * @return string
  228. *
  229. */
  230. public function pageName($value, $beautify = false, $maxLength = 128) {
  231. return strtolower($this->name($value, $beautify, $maxLength, '-'));
  232. }
  233. /**
  234. * Name filter for ProcessWire Page names with transliteration
  235. *
  236. * This is the same as calling pageName with the Sanitizer::translate option for $beautify.
  237. *
  238. * @param string $value
  239. * @param int $maxLength Maximum number of characters allowed in the name
  240. * @return string
  241. *
  242. */
  243. public function pageNameTranslate($value, $maxLength = 128) {
  244. return $this->pageName($value, self::translate, $maxLength);
  245. }
  246. /**
  247. * Format required by ProcessWire user names
  248. *
  249. * @deprecated, use pageName instead.
  250. * @param string $value
  251. * @return string
  252. *
  253. */
  254. public function username($value) {
  255. return $this->pageName($value);
  256. }
  257. /**
  258. * Name filter for ProcessWire filenames (basenames only, not paths)
  259. *
  260. * @param string $value
  261. * @param bool|int $beautify Should be true when creating a file's name for the first time. Default is false.
  262. * You may also specify Sanitizer::translate (or number 2) for the $beautify param, which will make it translate letters
  263. * based on the InputfieldPageName custom config settings.
  264. * @param int $maxLength Maximum number of characters allowed in the name
  265. * @return string
  266. *
  267. */
  268. public function filename($value, $beautify = false, $maxLength = 128) {
  269. if(!is_string($value)) return '';
  270. $value = basename($value);
  271. if(strlen($value) > $maxLength) {
  272. // truncate, while keeping extension in tact
  273. $pathinfo = pathinfo($value);
  274. $extLen = strlen($pathinfo['extension']) + 1; // +1 includes period
  275. $basename = substr($pathinfo['filename'], 0, $maxLength - $extLen);
  276. $value = "$basename.$pathinfo[extension]";
  277. }
  278. return $this->name($value, $beautify, $maxLength, '_', array(
  279. 'allowAdjacentExtras' => true, // language translation filenames require doubled "--" chars, others may too
  280. )
  281. );
  282. }
  283. /**
  284. * Hookable alias of filename method for case consistency with other name methods (preferable to use filename)
  285. *
  286. */
  287. public function ___fileName($value, $beautify = false, $maxLength = 128) {
  288. return $this->filename($value, $beautify, $maxLength);
  289. }
  290. /**
  291. * Return the given path if valid, or boolean false if not.
  292. *
  293. * Path is validated per ProcessWire "name" convention of ascii only [-_./a-z0-9]
  294. * As a result, this function is primarily useful for validating ProcessWire paths,
  295. * and won't always work with paths outside ProcessWire.
  296. *
  297. * This method validates only and does not sanitize. See pagePathName() for a similar
  298. * method that does sanitiation.
  299. *
  300. * @param string $value Path
  301. * @param int|array $options Options to modify behavior, or maxLength (int) may be specified.
  302. * - allowDotDot: Whether to allow ".." in a path (default=false)
  303. * - maxLength: Maximum length of allowed path (default=1024)
  304. * @return bool|string Returns false if invalid, actual path (string) if valid.
  305. *
  306. */
  307. public function path($value, $options = array()) {
  308. if(!is_string($value)) return false;
  309. if(is_int($options)) $options = array('maxLength' => $options);
  310. $defaults = array(
  311. 'allowDotDot' => false,
  312. 'maxLength' => 1024
  313. );
  314. $options = array_merge($defaults, $options);
  315. if(DIRECTORY_SEPARATOR != '/') $value = str_replace(DIRECTORY_SEPARATOR, '/', $value);
  316. if(strlen($value) > $options['maxLength']) return false;
  317. if(strpos($value, '/./') !== false || strpos($value, '//') !== false) return false;
  318. if(!$options['allowDotDot'] && strpos($value, '..') !== false) return false;
  319. if(!preg_match('{^[-_./a-z0-9]+$}iD', $value)) return false;
  320. return $value;
  321. }
  322. /**
  323. * Sanitize a page path name
  324. *
  325. * Returned path is not guaranteed to be valid or match a page, just sanitized.
  326. *
  327. * @param string $value
  328. * @param bool $beautify
  329. * @param int $maxLength
  330. * @return string
  331. *
  332. */
  333. public function pagePathName($value, $beautify = false, $maxLength = 1024) {
  334. $options = array(
  335. 'allowedExtras' => array('/', '-', '_', '.')
  336. );
  337. $value = $this->name($value, $beautify, $maxLength, '-', $options);
  338. // disallow double slashes
  339. while(strpos($value, '//') !== false) $value = str_replace('//', '/', $value);
  340. // disallow relative paths
  341. while(strpos($value, '..') !== false) $value = str_replace('..', '.', $value);
  342. // disallow names that start with a period
  343. while(strpos($value, '/.') !== false) $value = str_replace('/.', '/', $value);
  344. return $value;
  345. }
  346. /**
  347. * Returns valid email address, or blank if it isn't valid
  348. *
  349. * @param string $value
  350. * @return string
  351. *
  352. */
  353. public function email($value) {
  354. $value = filter_var($value, FILTER_SANITIZE_EMAIL);
  355. if(filter_var($value, FILTER_VALIDATE_EMAIL)) return $value;
  356. return '';
  357. }
  358. /**
  359. * Returns a value that may be used in an email header
  360. *
  361. * @param string $value
  362. * @return string
  363. *
  364. */
  365. public function emailHeader($value) {
  366. if(!is_string($value)) return '';
  367. $a = array("\n", "\r", "<CR>", "<LF>", "0x0A", "0x0D", "%0A", "%0D", 'content-type:', 'bcc:', 'cc:', 'to:', 'reply-to:');
  368. return trim(str_ireplace($a, ' ', $value));
  369. }
  370. /**
  371. * Sanitize input text and remove tags
  372. *
  373. * @param string $value
  374. * @param array $options See the $defaultOptions array in the method for options
  375. * @return string
  376. *
  377. */
  378. public function text($value, $options = array()) {
  379. $defaultOptions = array(
  380. 'maxLength' => 255, // maximum characters allowed, or 0=no max
  381. 'maxBytes' => 0, // maximum bytes allowed (0 = default, which is maxLength*4)
  382. 'stripTags' => true, // strip markup tags
  383. 'allowableTags' => '', // tags that are allowed, if stripTags is true (use same format as for PHP's strip_tags function)
  384. 'multiLine' => false, // allow multiple lines? if false, then $newlineReplacement below is applicable
  385. 'newlineReplacement' => ' ', // character to replace newlines with, OR specify boolean TRUE to remove extra lines
  386. 'inCharset' => 'UTF-8', // input charset
  387. 'outCharset' => 'UTF-8', // output charset
  388. );
  389. $options = array_merge($defaultOptions, $options);
  390. if(!is_string($value)) $value = $this->string($value);
  391. if(!$options['multiLine']) {
  392. if(strpos($value, "\r") !== false) {
  393. $value = str_replace("\r", "\n", $value); // normalize to LF
  394. }
  395. $pos = strpos($value, "\n");
  396. if($pos !== false) {
  397. if($options['newlineReplacement'] === true) {
  398. // remove extra lines
  399. $value = rtrim(substr($value, 0, $pos));
  400. } else {
  401. // remove linefeeds
  402. $value = str_replace(array("\n\n", "\n"), $options['newlineReplacement'], $value);
  403. }
  404. }
  405. }
  406. if($options['stripTags']) $value = strip_tags($value, $options['allowableTags']);
  407. if($options['inCharset'] != $options['outCharset']) $value = iconv($options['inCharset'], $options['outCharset'], $value);
  408. if($options['maxLength']) {
  409. if(empty($options['maxBytes'])) $options['maxBytes'] = $options['maxLength'] * 4;
  410. if($this->multibyteSupport) {
  411. if(mb_strlen($value, $options['outCharset']) > $options['maxLength']) {
  412. $value = mb_substr($value, 0, $options['maxLength'], $options['outCharset']);
  413. }
  414. } else {
  415. if(strlen($value) > $options['maxLength']) {
  416. $value = substr($value, 0, $options['maxLength']);
  417. }
  418. }
  419. }
  420. if($options['maxBytes']) {
  421. $n = $options['maxBytes'];
  422. while(strlen($value) > $options['maxBytes']) {
  423. $n--;
  424. if($this->multibyteSupport) {
  425. $value = mb_substr($value, 0, $n, $options['outCharset']);
  426. } else {
  427. $value = substr($value, 0, $n);
  428. }
  429. }
  430. }
  431. return trim($value);
  432. }
  433. /**
  434. * Sanitize input multiline text and remove tags
  435. *
  436. * @param string $value
  437. * @param array $options See Sanitizer::text and $defaultOptions array for an explanation of options
  438. * @return string
  439. *
  440. */
  441. public function textarea($value, $options = array()) {
  442. if(!is_string($value)) $value = $this->string($value);
  443. if(!isset($options['multiLine'])) $options['multiLine'] = true;
  444. if(!isset($options['maxLength'])) $options['maxLength'] = 16384;
  445. if(!isset($options['maxBytes'])) $options['maxBytes'] = $options['maxLength'] * 3;
  446. // convert \r\n to just \n
  447. if(empty($options['allowCRLF']) && strpos($value, "\r\n") !== false) $value = str_replace("\r\n", "\n", $value);
  448. return $this->text($value, $options);
  449. }
  450. /**
  451. * Returns a valid URL, or blank if it can't be made valid
  452. *
  453. * Performs some basic sanitization like adding a protocol to the front if it's missing, but leaves alone local/relative URLs.
  454. *
  455. * URL is not required to confirm to ProcessWire conventions unless a relative path is given.
  456. *
  457. * Please note that URLs should always be entity encoded in your output. <script> is technically allowed in a valid URL, so
  458. * your output should always entity encoded any URLs that came from user input.
  459. *
  460. * @param string $value URL
  461. * @param bool|array $options Array of options including:
  462. * - allowRelative (boolean) Whether to allow relative URLs, i.e. those without domains (default=true)
  463. * - allowIDN (boolean) Whether to allow internationalized domain names (default=false)
  464. * - allowQuerystring (boolean) Whether to allow query strings (default=true)
  465. * - allowSchemes (array) Array of allowed schemes, lowercase (default=[] any)
  466. * - disallowSchemes (array) Array of disallowed schemes, lowercase (default=[file])
  467. * - requireScheme (bool) Specify true to require a scheme in the URL, if one not present, it will be added to non-relative URLs (default=true)
  468. * - stripTags (bool) Specify false to prevent tags from being stripped (default=true)
  469. * - stripQuotes (bool) Specify false to prevent quotes from being stripped (default=true)
  470. * - throw (bool) Throw exceptions on invalid URLs (default=false)
  471. * Previously this was the boolean $allowRelative, and that usage will still work for backwards compatibility.
  472. * @return string
  473. * @throws WireException on invalid URLs, only if $options['throw'] is true.
  474. * @todo add TLD validation
  475. *
  476. */
  477. public function url($value, $options = array()) {
  478. $defaultOptions = array(
  479. 'allowRelative' => true,
  480. 'allowIDN' => false,
  481. 'allowQuerystring' => true,
  482. 'allowSchemes' => array(),
  483. 'disallowSchemes' => array('file', 'javascript'),
  484. 'requireScheme' => true,
  485. 'stripTags' => true,
  486. 'stripQuotes' => true,
  487. 'maxLength' => 4096,
  488. 'throw' => false,
  489. );
  490. if(!is_array($options)) {
  491. $defaultOptions['allowRelative'] = (bool) $options; // backwards compatibility with old API
  492. $options = array();
  493. }
  494. $options = array_merge($defaultOptions, $options);
  495. $textOptions = array(
  496. 'stripTags' => $options['stripTags'],
  497. 'maxLength' => $options['maxLength'],
  498. 'newlineReplacement' => true,
  499. );
  500. $value = $this->text($value, $textOptions);
  501. if(!strlen($value)) return '';
  502. $scheme = parse_url($value, PHP_URL_SCHEME);
  503. if($scheme !== false && strlen($scheme)) {
  504. $_scheme = $scheme;
  505. $scheme = strtolower($scheme);
  506. $schemeError = false;
  507. if(!empty($options['allowSchemes']) && !in_array($scheme, $options['allowSchemes'])) $schemeError = true;
  508. if(!empty($options['disallowSchemes']) && in_array($scheme, $options['disallowSchemes'])) $schemeError = true;
  509. if($schemeError) {
  510. $error = sprintf($this->_('URL: Scheme "%s" is not allowed'), $scheme);
  511. if($options['throw']) throw new WireException($error);
  512. $this->error($error);
  513. $value = str_ireplace(array("$scheme:///", "$scheme://"), '', $value);
  514. } else if($_scheme !== $scheme) {
  515. $value = str_replace("$_scheme://", "$scheme://", $value); // lowercase scheme
  516. }
  517. }
  518. // separate scheme+domain+path from query string temporarily
  519. if(strpos($value, '?') !== false) {
  520. list($domainPath, $queryString) = explode('?', $value);
  521. if(!$options['allowQuerystring']) $queryString = '';
  522. } else {
  523. $domainPath = $value;
  524. $queryString = '';
  525. }
  526. $pathIsEncoded = strpos($domainPath, '%') !== false;
  527. if($pathIsEncoded || filter_var($domainPath, FILTER_SANITIZE_URL) !== $domainPath) {
  528. // the domain and/or path contains extended characters not supported by FILTER_SANITIZE_URL
  529. // Example: https://de.wikipedia.org/wiki/Linkshänder
  530. // OR it is already rawurlencode()'d
  531. // Example: https://de.wikipedia.org/wiki/Linksh%C3%A4nder
  532. // we convert the URL to be FILTER_SANITIZE_URL compatible
  533. // if already encoded, first remove encoding:
  534. if(strpos($domainPath, '%') !== false) $domainPath = rawurldecode($domainPath);
  535. // Next, encode it, for example: https%3A%2F%2Fde.wikipedia.org%2Fwiki%2FLinksh%C3%A4nder
  536. $domainPath = rawurlencode($domainPath);
  537. // restore characters allowed in domain/path
  538. $domainPath = str_replace(array('%2F', '%3A'), array('/', ':'), $domainPath);
  539. // restore value that is now FILTER_SANITIZE_URL compatible
  540. $value = $domainPath . (strlen($queryString) ? "?$queryString" : "");
  541. $pathIsEncoded = true;
  542. }
  543. // this filter_var sanitizer just removes invalid characters that don't appear in domains or paths
  544. $value = filter_var($value, FILTER_SANITIZE_URL);
  545. if(!$scheme) {
  546. // URL is missing scheme/protocol, or is local/relative
  547. if(strpos($value, '://') !== false) {
  548. // apparently there is an attempted, but unrecognized scheme, so remove it
  549. $value = preg_replace('!^[^?]*?://!', '', $value);
  550. }
  551. if($options['allowRelative']) {
  552. // determine if this is a domain name
  553. // regex legend: (www.)? company. com ( .uk or / or end)
  554. $dotPos = strpos($value, '.');
  555. $slashPos = strpos($value, '/');
  556. if($slashPos === false) $slashPos = $dotPos+1;
  557. // if the first slash comes after the first dot, the dot is likely part of a domain.com/path/
  558. // if the first slash comes before the first dot, then it's likely a /path/product.html
  559. if($dotPos && $slashPos > $dotPos && preg_match('{^([^\s_.]+\.)?[^-_\s.][^\s_.]+\.([a-z]{2,6})([./:#]|$)}i', $value, $matches)) {
  560. // most likely a domain name
  561. // $tld = $matches[3]; // TODO add TLD validation to confirm it's a domain name
  562. $value = $this->filterValidateURL("http://$value", $options); // add scheme for validation
  563. } else if($options['allowQuerystring']) {
  564. // we'll construct a fake domain so we can use FILTER_VALIDATE_URL rules
  565. $fake = 'http://processwire.com/';
  566. $slash = strpos($value, '/') === 0 ? '/' : '';
  567. $value = $fake . ltrim($value, '/');
  568. $value = $this->filterValidateURL($value, $options);
  569. $value = str_replace($fake, $slash, $value);
  570. } else {
  571. // most likely a relative path
  572. $value = $this->path($value);
  573. }
  574. } else {
  575. // relative urls aren't allowed, so add the scheme/protocol and validate
  576. $value = $this->filterValidateURL("http://$value", $options);
  577. }
  578. if(!$options['requireScheme']) {
  579. // if a scheme was added above (for filter_var validation) and it's not required, remove it
  580. $value = str_replace('http://', '', $value);
  581. }
  582. } else if($scheme == 'tel') {
  583. // tel: scheme is not supported by filter_var
  584. if(!preg_match('/^tel:\+?\d+$/', $value)) {
  585. $value = str_replace(' ', '', $value);
  586. /** @noinspection PhpUnusedLocalVariableInspection */
  587. list($tel, $num) = explode(':', $value);
  588. $value = 'tel:';
  589. if(strpos($num, '+') === 0) $value .= '+';
  590. $value .= preg_replace('/[^\d]/', '', $num);
  591. }
  592. } else {
  593. // URL already has a scheme
  594. $value = $this->filterValidateURL($value, $options);
  595. }
  596. if($pathIsEncoded && strlen($value)) {
  597. // restore to non-encoded, UTF-8 version
  598. if(strpos('?', $value) !== false) {
  599. list($domainPath, $queryString) = explode('?', $value);
  600. } else {
  601. $domainPath = $value;
  602. $queryString = '';
  603. }
  604. $domainPath = rawurldecode($domainPath);
  605. if(strpos($domainPath, '%') !== false) {
  606. $domainPath = preg_replace('/%[0-9ABCDEF]{1,2}/i', '', $domainPath);
  607. $domainPath = str_replace('%', '', $domainPath);
  608. }
  609. $domainPath = $this->text($domainPath, $textOptions);
  610. $value = $domainPath . (strlen($queryString) ? "?$queryString" : "");
  611. }
  612. if(strlen($value)) {
  613. if($options['stripTags']) {
  614. if(stripos($value, '%3') !== false) {
  615. $value = str_ireplace(array('%3C', '%3E'), array('!~!<', '>!~!'), $value);
  616. $value = strip_tags($value);
  617. $value = str_ireplace(array('!~!<', '>!~!', '!~!'), array('%3C', '%3E', ''), $value); // restore, in case valid/non-tag
  618. } else {
  619. $value = strip_tags($value);
  620. }
  621. }
  622. if($options['stripQuotes']) {
  623. $value = str_replace(array('"', "'", "%22", "%27"), '', $value);
  624. }
  625. return $value;
  626. }
  627. return '';
  628. }
  629. /**
  630. * Implementation of PHP's FILTER_VALIDATE_URL with IDN support (will convert to valid)
  631. *
  632. * Example: http://трикотаж-леко.рф
  633. *
  634. * @param string $url
  635. * @param array $options Specify ('allowIDN' => false) to disallow internationalized domain names
  636. * @return string
  637. *
  638. */
  639. protected function filterValidateURL($url, array $options) {
  640. $_url = $url;
  641. $url = filter_var($url, FILTER_VALIDATE_URL);
  642. if($url !== false && strlen($url)) return $url;
  643. // if allowIDN was specifically set false, don't proceed further
  644. if(isset($options['allowIDN']) && !$options['allowIDN']) return $url;
  645. // if PHP doesn't support idn_* functions, we can't do anything further here
  646. if(!function_exists('idn_to_ascii') || !function_exists('idn_to_utf8')) return $url;
  647. // extract scheme
  648. if(strpos($_url, '//') !== false) {
  649. list($scheme, $_url) = explode('//', $_url, 2);
  650. $scheme .= '//';
  651. } else {
  652. $scheme = '';
  653. }
  654. // extract domain, and everything else (rest)
  655. if(strpos($_url, '/') > 0) {
  656. list($domain, $rest) = explode('/', $_url, 2);
  657. $rest = "/$rest";
  658. } else {
  659. $domain = $_url;
  660. $rest = '';
  661. }
  662. if(strpos($domain, '%') !== false) {
  663. // domain is URL encoded
  664. $domain = rawurldecode($domain);
  665. }
  666. // extract port, if present, and prepend to $rest
  667. if(strpos($domain, ':') !== false && preg_match('/^([^:]+):(\d+)$/', $domain, $matches)) {
  668. $domain = $matches[1];
  669. $rest = ":$matches[2]$rest";
  670. }
  671. if($this->nameFilter($domain, array('-', '.'), '_', false, 1024) === $domain) {
  672. // domain contains no extended characters
  673. $url = $scheme . $domain . $rest;
  674. $url = filter_var($url, FILTER_VALIDATE_URL);
  675. } else {
  676. // domain contains utf8
  677. $domain = idn_to_ascii($domain);
  678. if($domain === false || !strlen($domain)) return '';
  679. $url = $scheme . $domain . $rest;
  680. $url = filter_var($url, FILTER_VALIDATE_URL);
  681. if(strlen($url)) {
  682. // convert back to utf8 domain
  683. $domain = idn_to_utf8($domain);
  684. if($domain === false) return '';
  685. $url = $scheme . $domain . $rest;
  686. }
  687. }
  688. return $url;
  689. }
  690. /**
  691. * Field name filter as used by ProcessWire Fields
  692. *
  693. * Note that dash and dot are excluded because they aren't allowed characters in PHP variables
  694. *
  695. * @param string $value
  696. * @return string
  697. *
  698. */
  699. public function selectorField($value) {
  700. return $this->nameFilter($value, array('_'), '_');
  701. }
  702. /**
  703. * Sanitizes a string value that needs to go in a ProcessWire selector
  704. *
  705. * String value is assumed to be UTF-8. Replaces non-alphanumeric and non-space with space
  706. *
  707. * @param string $value
  708. * @param int $maxLength Maximum number of allowed characters
  709. * @return string
  710. *
  711. */
  712. public function selectorValue($value, $maxLength = 100) {
  713. if(!is_string($value)) $value = $this->string($value);
  714. $value = trim($value);
  715. $quoteChar = '"';
  716. $needsQuotes = false;
  717. // determine if value is already quoted and set initial value of needsQuotes
  718. // also pick out the initial quote style
  719. if(strlen($value) && ($value[0] == "'" || $value[0] == '"')) {
  720. $needsQuotes = true;
  721. }
  722. // trim off leading or trailing quotes
  723. $value = trim($value, "\"'");
  724. // if an apostrophe is present, value must be quoted
  725. if(strpos($value, "'") !== false) $needsQuotes = true;
  726. // if commas are present, then the selector needs to be quoted
  727. if(strpos($value, ',') !== false) $needsQuotes = true;
  728. // disallow double quotes -- remove any if they are present
  729. if(strpos($value, '"') !== false) $value = str_replace('"', '', $value);
  730. // selector value is limited to 100 chars
  731. if(strlen($value) > $maxLength) {
  732. if($this->multibyteSupport) $value = mb_substr($value, 0, $maxLength, 'UTF-8');
  733. else $value = substr($value, 0, $maxLength);
  734. }
  735. // disallow some characters in selector values
  736. // @todo technically we only need to disallow at begin/end of string
  737. $value = str_replace(array('*', '~', '`', '$', '^', '|', '<', '>', '=', '[', ']', '{', '}'), ' ', $value);
  738. // disallow greater/less than signs, unless they aren't forming a tag
  739. // if(strpos($value, '<') !== false) $value = preg_replace('/<[^>]+>/su', ' ', $value);
  740. // more disallowed chars, these may not appear anywhere in selector value
  741. $value = str_replace(array("\r", "\n", "#", "%"), ' ', $value);
  742. // see if we can avoid the preg_matches and do a quick filter
  743. $test = str_replace(array(',', ' ', '-'), '', $value);
  744. if(!ctype_alnum($test)) {
  745. // value needs more filtering, replace all non-alphanumeric, non-single-quote and space chars
  746. // See: http://php.net/manual/en/regexp.reference.unicode.php
  747. // See: http://www.regular-expressions.info/unicode.html
  748. $value = preg_replace('/[^[:alnum:]\pL\pN\pP\pM\p{Sm}\p{Sc}\p{Sk} \'\/]/u', ' ', $value);
  749. // disallow ampersands from beginning entity sequences
  750. if(strpos($value, '&') !== false) $value = str_replace('&', '& ', $value);
  751. // replace multiple space characters in sequence with just 1
  752. $value = preg_replace('/\s\s+/u', ' ', $value);
  753. }
  754. $value = trim($value); // trim any kind of whitespace
  755. $value = trim($value, '+!,'); // chars to remove from begin and end
  756. if(!$needsQuotes && strlen($value)) {
  757. $a = substr($value, 0, 1);
  758. $b = substr($value, -1);
  759. if((!ctype_alnum($a) && $a != '/') || (!ctype_alnum($b) && $b != '/')) $needsQuotes = true;
  760. }
  761. if($needsQuotes) $value = $quoteChar . $value . $quoteChar;
  762. return $value;
  763. }
  764. /**
  765. * Entity encode a string
  766. *
  767. * Wrapper for PHP's htmlentities function that contains typical ProcessWire usage defaults
  768. *
  769. * The arguments used hre are identical to those for PHP's htmlentities function:
  770. * http://www.php.net/manual/en/function.htmlentities.php
  771. *
  772. * @param string $str
  773. * @param int|bool $flags See PHP htmlentities function for flags.
  774. * @param string $encoding
  775. * @param bool $doubleEncode
  776. * @return string
  777. *
  778. */
  779. public function entities($str, $flags = ENT_QUOTES, $encoding = 'UTF-8', $doubleEncode = true) {
  780. if(!is_string($str)) $str = $this->string($str);
  781. return htmlentities($str, $flags, $encoding, $doubleEncode);
  782. }
  783. /**
  784. * Entity encode a string and don't double encode something if already encoded
  785. *
  786. * @param string $str
  787. * @param int|bool $flags See PHP htmlentities function for flags.
  788. * @param string $encoding
  789. * @return string
  790. *
  791. */
  792. public function entities1($str, $flags = ENT_QUOTES, $encoding = 'UTF-8') {
  793. if(!is_string($str)) $str = $this->string($str);
  794. return htmlentities($str, $flags, $encoding, false);
  795. }
  796. /**
  797. * Entity encode while translating some markdown tags to HTML equivalents
  798. *
  799. * If you specify boolean TRUE for the $options argument, full markdown is applied. Otherwise,
  800. * only basic markdown allowed, as outlined below:
  801. *
  802. * Basic allowed markdown currently includes:
  803. * **strong**
  804. * *emphasis*
  805. * [anchor-text](url)
  806. * ~~strikethrough~~
  807. * `code`
  808. *
  809. * The primary reason to use this over full-on Markdown is that it has less overhead
  810. * and is faster then full-blown Markdowon, for when you don't need it. It's also safer
  811. * for text coming from user input since it doesn't allow any other HTML. But if you just
  812. * want full markdown, then specify TRUE for the $options argument.
  813. *
  814. * @param string $str
  815. * @param array|bool|int $options Options include the following, or specify boolean TRUE to apply full markdown.
  816. * - flags (int): See htmlentities() flags. Default is ENT_QUOTES.
  817. * - encoding (string): PHP encoding type. Default is 'UTF-8'.
  818. * - doubleEncode (bool): Whether to double encode (if already encoded). Default is true.
  819. * - allow (array): Only markdown that translates to these tags will be allowed. Default=array('a', 'strong', 'em', 'code', 's')
  820. * - disallow (array): Specified tags (in the default allow list) won't be allowed. Default=array().
  821. * Note: The 'disallow' is an alternative to the default 'allow'. No point in using them both.
  822. * - linkMarkup (string): Markup to use for links. Default='<a href="{url}" rel="nofollow" target="_blank">{text}</a>'
  823. * @return string
  824. *
  825. */
  826. public function entitiesMarkdown($str, $options = array()) {
  827. if($options === true || (is_int($options) && $options > 0)) {
  828. $markdown = $this->wire('modules')->get('TextformatterMarkdownExtra');
  829. if(is_int($options)) {
  830. $markdown->flavor = $options;
  831. } else {
  832. $markdown->flavor = TextformatterMarkdownExtra::flavorParsedown;
  833. }
  834. $markdown->format($str);
  835. return $str;
  836. }
  837. if(!is_array($options)) $options = array();
  838. $defaults = array(
  839. 'flags' => ENT_QUOTES,
  840. 'encoding' => 'UTF-8',
  841. 'doubleEncode' => true,
  842. 'allow' => array('a', 'strong', 'em', 'code', 's'),
  843. 'disallow' => array(),
  844. 'linkMarkup' => '<a href="{url}" rel="nofollow" target="_blank">{text}</a>',
  845. );
  846. $options = array_merge($defaults, $options);
  847. $str = $this->entities($str, $options['flags'], $options['encoding'], $options['doubleEncode']);
  848. if(strpos($str, '](') && in_array('a', $options['allow']) && !in_array('a', $options['disallow'])) {
  849. // link
  850. $linkMarkup = str_replace(array('{url}', '{text}'), array('$2', '$1'), $options['linkMarkup']);
  851. $str = preg_replace('/\[(.+?)\]\(([^)]+)\)/', $linkMarkup, $str);
  852. }
  853. if(strpos($str, '**') !== false && in_array('strong', $options['allow']) && !in_array('strong', $options['disallow'])) {
  854. // strong
  855. $str = preg_replace('/\*\*(.*?)\*\*/', '<strong>$1</strong>', $str);
  856. }
  857. if(strpos($str, '*') !== false && in_array('em', $options['allow']) && !in_array('em', $options['disallow'])) {
  858. // em
  859. $str = preg_replace('/\*([^*\n]+)\*/', '<em>$1</em>', $str);
  860. }
  861. if(strpos($str, "`") !== false && in_array('code', $options['allow']) && !in_array('code', $options['disallow'])) {
  862. // code
  863. $str = preg_replace('/`+([^`]+)`+/', '<code>$1</code>', $str);
  864. }
  865. if(strpos($str, '~~') !== false && in_array('s', $options['allow']) && !in_array('s', $options['disallow'])) {
  866. // strikethrough
  867. $str = preg_replace('/~~(.+?)~~/', '<s>$1</s>', $str);
  868. }
  869. return $str;
  870. }
  871. /**
  872. * Remove entity encoded characters from a string.
  873. *
  874. * Wrapper for PHP's html_entity_decode function that contains typical ProcessWire usage defaults
  875. *
  876. * The arguments used hre are identical to those for PHP's heml_entity_decode function:
  877. * http://www.php.net/manual/en/function.html-entity-decode.php
  878. *
  879. * @param string $str
  880. * @param int|bool $flags See PHP html_entity_decode function for flags.
  881. * @param string $encoding
  882. * @return string
  883. *
  884. */
  885. public function unentities($str, $flags = ENT_QUOTES, $encoding = 'UTF-8') {
  886. if(!is_string($str)) $str = $this->string($str);
  887. return html_entity_decode($str, $flags, $encoding);
  888. }
  889. /**
  890. * Alias for unentities
  891. *
  892. * @param $str
  893. * @param $flags
  894. * @param $encoding
  895. * @return string
  896. * @deprecated
  897. *
  898. */
  899. public function removeEntities($str, $flags, $encoding) {
  900. return $this->unentities($str, $flags, $encoding);
  901. }
  902. /**
  903. * Purify HTML markup using HTML Purifier
  904. *
  905. * See: http://htmlpurifier.org
  906. *
  907. * @param string $str String to purify
  908. * @param array $options See config options at: http://htmlpurifier.org/live/configdoc/plain.html
  909. * @return string
  910. * @throws WireException if given something other than a string
  911. *
  912. */
  913. public function purify($str, array $options = array()) {
  914. static $purifier = null;
  915. static $_options = array();
  916. if(!is_string($str)) $str = $this->string($str);
  917. if(is_null($purifier) || print_r($options, true) != print_r($_options, true)) {
  918. $purifier = $this->purifier($options);
  919. $_options = $options;
  920. }
  921. return $purifier->purify($str);
  922. }
  923. /**
  924. * Return a new HTML Purifier instance
  925. *
  926. * See: http://htmlpurifier.org
  927. *
  928. * @param array $options See config options at: http://htmlpurifier.org/live/configdoc/plain.html
  929. * @return MarkupHTMLPurifier
  930. *
  931. */
  932. public function purifier(array $options = array()) {
  933. $purifier = $this->wire('modules')->get('MarkupHTMLPurifier');
  934. foreach($options as $key => $value) $purifier->set($key, $value);
  935. return $purifier;
  936. }
  937. /**
  938. * Sanitize value to string
  939. *
  940. * Note that this makes no assumptions about what is a "safe" string, so you should always apply another
  941. * sanitizer to it.
  942. *
  943. * @param string|int|array|object|bool|float $value
  944. * @param string|null Optional sanitizer method (from this class) to apply to the string before returning
  945. * @return string
  946. *
  947. */
  948. public function string($value, $sanitizer = null) {
  949. if(is_object($value)) {
  950. if(method_exists($value, '__toString')) {
  951. $value = (string) $value;
  952. } else {
  953. $value = get_class($value);
  954. }
  955. } else if(is_null($value)) {
  956. $value = "";
  957. } else if(is_bool($value)) {
  958. $value = $value ? "1" : "";
  959. }
  960. if(is_array($value)) $value = "array-" . count($value);
  961. if(!is_string($value)) $value = (string) $value;
  962. if(!is_null($sanitizer) && is_string($sanitizer) && (method_exists($this, $sanitizer) || method_exists($this, "___$sanitizer"))) {
  963. $value = $this->$sanitizer($value);
  964. if(!is_string($value)) $value = (string) $value;
  965. }
  966. return $value;
  967. }
  968. /**
  969. * Sanitize a date or date/time string, making sure it is valid, and return it
  970. *
  971. * If no dateFormat is specified, date will be returned as a unix timestamp.
  972. * If given date is invalid or empty, NULL will be returned.
  973. * If $value is an integer or string of all numbers, it is always assumed to be a unix timestamp.
  974. *
  975. * @param string|int $value Date string or unix timestamp
  976. * @param string|null $format Format of date string ($value) in any wireDate(), date() or strftime() format.
  977. * @param array $options Options to modify behavior:
  978. * - returnFormat: wireDate() format to return date in. If not specified, then the $format argument is used.
  979. * - min: Minimum allowed date in $format or unix timestamp format. Null is returned when date is less than this.
  980. * - max: Maximum allowed date in $format or unix timestamp format. Null is returned when date is more than this.
  981. * - default: Default value to return if no value specified.
  982. * @return string|int|null
  983. *
  984. */
  985. public function date($value, $format = null, array $options = array()) {
  986. $defaults = array(
  987. 'returnFormat' => $format, // date format to return in, if different from $dateFormat
  988. 'min' => '', // Minimum date allowed (in $dateFormat format, or a unix timestamp)
  989. 'max' => '', // Maximum date allowed (in $dateFormat format, or a unix timestamp)
  990. 'default' => null, // Default value, if date didn't resolve
  991. );
  992. $options = array_merge($defaults, $options);
  993. if(empty($value)) return $options['default'];
  994. if(!is_string($value) && !is_int($value)) $value = $this->string($value);
  995. if(ctype_digit("$value")) {
  996. // value is in unix timestamp format
  997. // make sure it resolves to a valid date
  998. $value = strtotime(date('Y-m-d H:i:s', (int) $value));
  999. } else {
  1000. $value = strtotime($value);
  1001. }
  1002. // value is now a unix timestamp
  1003. if(empty($value)) return null;
  1004. if(!empty($options['min'])) {
  1005. // if value is less than minimum required, return null/error
  1006. $min = ctype_digit("$options[min]") ? (int) $options['min'] : (int) wireDate('ts', $options['min']);
  1007. if($value < $min) return null;
  1008. }
  1009. if(!empty($options['max'])) {
  1010. // if value is more than max allowed, return null/error
  1011. $max = ctype_digit("$options[max]") ? (int) $options['max'] : (int) wireDate('ts', $options['max']);
  1012. if($value > $max) return null;
  1013. }
  1014. if(!empty($options['returnFormat'])) $value = wireDate($options['returnFormat'], $value);
  1015. return empty($value) ? null : $value;
  1016. }
  1017. /**
  1018. * Validate that $value matches regex pattern. If it does, value is returned. If not, blank is returned.
  1019. *
  1020. * @param string $value
  1021. * @param string $regex PCRE regex pattern (same as you would provide to preg_match)
  1022. * @return string
  1023. *
  1024. */
  1025. public function match($value, $regex) {
  1026. if(!is_string($value)) $value = $this->string($value);
  1027. return preg_match($regex, $value) ? $value : '';
  1028. }
  1029. /*************************************************************************************************************************
  1030. * NUMBER SANITIZERS
  1031. *
  1032. */
  1033. /**
  1034. * Sanitized an integer (unsigned, unless you specify a negative minimum value)
  1035. *
  1036. * @param mixed $value
  1037. * @param array $options Optionally specify any one or more of the following to modify behavior:
  1038. * - min (int|null) Minimum allowed value (default=0)
  1039. * - max (int|null) Maximum allowed value (default=PHP_INT_MAX)
  1040. * - blankValue (mixed) Value that you want to use when provided value is null or blank string (default=0)
  1041. * @return int Returns integer, or specified blankValue (which doesn't necessarily have to be an integer)
  1042. *
  1043. */
  1044. public function int($value, array $options = array()) {
  1045. $defaults = array(
  1046. 'min' => 0,
  1047. 'max' => PHP_INT_MAX,
  1048. 'blankValue' => 0,
  1049. );
  1050. $options = array_merge($defaults, $options);
  1051. if(is_null($value) || $value === "") return $options['blankValue'];
  1052. if(is_object($value)) $value = 1;
  1053. $value = (int) $value;
  1054. if(!is_null($options['min']) && $value < $options['min']) {
  1055. $value = (int) $options['min'];
  1056. } else if(!is_null($options['max']) && $value > $options['max']) {
  1057. $value = (int) $options['max'];
  1058. }
  1059. return $value;
  1060. }
  1061. /**
  1062. * Sanitize to unsigned (0 or positive) integer
  1063. *
  1064. * This is an alias to the int() method with default min/max arguments
  1065. *
  1066. * @param mixed $value
  1067. * @param array $options Optionally specify any one or more of the following to modify behavior:
  1068. * - min (int|null) Minimum allowed value (default=0)
  1069. * - max (int|null) Maximum allowed value (default=PHP_INT_MAX)
  1070. * - blankValue (mixed) Value that you want to use when provided value is null or blank string (default=0)
  1071. * @return int Returns integer, or specified blankValue (which doesn't necessarily have to be an integer)
  1072. * @return int
  1073. *
  1074. */
  1075. public function intUnsigned($value, array $options = array()) {
  1076. return $this->int($value, $options);
  1077. }
  1078. /**
  1079. * Sanitize to signed integer (negative or positive)
  1080. *
  1081. * @param mixed $value
  1082. * @param array $options Optionally specify any one or more of the following to modify behavior:
  1083. * - min (int|null) Minimum allowed value (default=negative PHP_INT_MAX)
  1084. * - max (int|null) Maximum allowed value (default=PHP_INT_MAX)
  1085. * - blankValue (mixed) Value that you want to use when provided value is null or blank string (default=0)
  1086. * @return int
  1087. *
  1088. */
  1089. public function intSigned($value, array $options = array()) {
  1090. if(!isset($options['min'])) $options['min'] = PHP_INT_MAX * -1;
  1091. return $this->int($value, $options);
  1092. }
  1093. /**
  1094. * Sanitize to floating point value
  1095. *
  1096. * @param float|string|int $value
  1097. * @param array $options Optionally specify one or more options in an associative array:
  1098. * - precision (int|null): Optional number of digits to round to (default=null)
  1099. * - mode (int): Mode to use for rounding precision (default=PHP_ROUND_HALF_UP);
  1100. * - blankValue (null|int|string|float): Value to return (whether float or non-float) if provided $value is an empty non-float (default=0.0)
  1101. * - min (float|null): Minimum allowed value, excluding blankValue (default=null)
  1102. * - max (float|null): Maximum allowed value, excluding blankValue (default=null)
  1103. * @return float
  1104. *
  1105. */
  1106. public function float($value, array $options = array()) {
  1107. $defaults = array(
  1108. 'precision' => null, // Optional number of digits to round to
  1109. 'mode' => PHP_ROUND_HALF_UP, // Mode to use for rounding precision (default=PHP_ROUND_HALF_UP)
  1110. 'blankValue' => 0.0, // Value to return (whether float or non-float) if provided $value is an empty non-float (default=0.0)
  1111. 'min' => null, // Minimum allowed value (excluding blankValue)
  1112. 'max' => null, // Maximum allowed value (excluding blankValue)
  1113. );
  1114. $options = array_merge($defaults, $options);
  1115. if($value === null || $value === false) return $options['blankValue'];
  1116. if(!is_float($value) && !is_string($value)) $value = $this->string($value);
  1117. if(is_string($value)) {
  1118. $str = trim($value);
  1119. $prepend = '';
  1120. if(strpos($str, '-') === 0) {
  1121. $prepend = '-';
  1122. $str = ltrim($str, '-');
  1123. }
  1124. if(!strlen($str)) return $options['blankValue'];
  1125. $dotPos = strrpos($str, '.');
  1126. $commaPos = strrpos($str, ',');
  1127. $decimalType = substr(floatval("9.9"), 1, 1);
  1128. $pos = null;
  1129. if($dotPos === 0 || ($commaPos === 0 && $decimalType == ',')) {
  1130. // .123 or ,123
  1131. $value = "0." . ltrim($str, ',.');
  1132. } else if($dotPos > $commaPos) {
  1133. // 123123.123
  1134. // 123,123.123
  1135. // dot assumed to be decimal
  1136. $pos = $dotPos;
  1137. } else if($commaPos > $dotPos) {
  1138. // 123,123
  1139. // 123123,123
  1140. // 123.123,123
  1141. if($dotPos === false && $decimalType === '.' && preg_match('/^\d+(,\d{3})+([^,]|$)/', $str)) {
  1142. // US or GB style thousands separator with commas separating 3 digit sequences
  1143. $pos = strlen($str);
  1144. } else {
  1145. // the rest of the world
  1146. $pos = $commaPos;
  1147. }
  1148. } else {
  1149. $value = preg_replace('/[^0-9]/', '', $str);
  1150. }
  1151. if($pos !== null) {
  1152. $value =
  1153. // part before dot
  1154. preg_replace('/[^0-9]/', '', substr($str, 0, $pos)) . '.' .
  1155. // part after dot
  1156. preg_replace('/[^0-9]/', '', substr($str, $pos + 1));
  1157. }
  1158. $value = floatval($prepend . $value);
  1159. }
  1160. if(!is_float($value)) $value = (float) $value;
  1161. if(!is_null($options['min']) && $value < $options['min']) $value = $options['min'];
  1162. if(!is_null($options['max']) && $value > $options['max']) $value = $options['max'];
  1163. if(!is_null($options['precision'])) $value = round($value, (int) $options['precision'], (int) $options['mode']);
  1164. return $value;
  1165. }
  1166. /***********************************************************************************************************************
  1167. * ARRAY SANITIZERS
  1168. *
  1169. */
  1170. /**
  1171. * Sanitize array or CSV string to array of strings
  1172. *
  1173. * If string specified, string delimiter may be pipe ("|"), or comma (","), unless overridden with the 'delimiter'
  1174. * or 'delimiters' option.
  1175. *
  1176. * @param array|string|mixed $value Accepts an array or CSV string. If given something else, it becomes first item in array.
  1177. * @param string $sanitizer Optional Sanitizer method to apply to items in the array (default=null, aka none)
  1178. * @param array $options Optional modifications to default behavior:
  1179. * - maxItems (int): Maximum items allowed in array (default=0, which means no limit)
  1180. * The following options are only used if the provided $value is a string:
  1181. * - delimiter (string): Single delimiter to use to identify CSV strings. Overrides the 'delimiters' option when specified (default=null)
  1182. * - delimiters (array): Delimiters to identify CSV strings. First found delimiter will be used, default=array("|", ",")
  1183. * - enclosure (string): Enclosure to use for CSV strings (default=double quote, i.e. ")
  1184. * @return array
  1185. * @throws WireException if an unknown $sanitizer method is given
  1186. *
  1187. */
  1188. public function ___array($value, $sanitizer = null, array $options = array()) {
  1189. $defaults = array(
  1190. 'delimiter' => null,
  1191. 'delimiters' => array('|', ','),
  1192. 'enclosure' => '"',
  1193. 'maxItems' => 0,
  1194. );
  1195. $options = array_merge($defaults, $options);
  1196. if(!is_array($value)) {
  1197. if(is_null($value)) return array();
  1198. if(is_object($value)) {
  1199. // value is object: convert to string or array
  1200. if(method_exists($value, '__toString')) {
  1201. $value = (string) $value;
  1202. } else {
  1203. $value = array(get_class($value));
  1204. }
  1205. }
  1206. if(is_string($value)) {
  1207. // value is string
  1208. $hasDelimiter =

Large files files are truncated, but you can click here to view the full file