PageRenderTime 57ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/ajaxUnit.php

https://bitbucket.org/dominicsayers/ajaxunit
PHP | 4427 lines | 2914 code | 503 blank | 1010 comment | 488 complexity | 218e5d8e648c15ba82e3193d1b9ac15e MD5 | raw file

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

  1. <?php
  2. /**
  3. * Testing PHP and javascript by controlling the interactions automatically
  4. *
  5. * Copyright (c) 2008-2010, Dominic Sayers <br>
  6. * All rights reserved.
  7. *
  8. * Redistribution and use in source and binary forms, with or without modification,
  9. * are permitted provided that the following conditions are met:
  10. *
  11. * - Redistributions of source code must retain the above copyright notice,
  12. * this list of conditions and the following disclaimer.
  13. * - Redistributions in binary form must reproduce the above copyright notice,
  14. * this list of conditions and the following disclaimer in the documentation
  15. * and/or other materials provided with the distribution.
  16. * - Neither the name of Dominic Sayers nor the names of its contributors may be
  17. * used to endorse or promote products derived from this software without
  18. * specific prior written permission.
  19. *
  20. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  21. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  22. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  23. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
  24. * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  25. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  26. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  27. * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  28. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  29. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  30. *
  31. * @package ajaxUnit
  32. * @author Dominic Sayers <dominic@sayers.cc>
  33. * @copyright 2008-2010 Dominic Sayers
  34. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  35. * @link http://code.google.com/p/ajaxUnit/
  36. * @version 0.17.30 - Added 'ignore' tag - just put 'ignore' before any test element to ignore it
  37. */
  38. // The quality of this code has been improved greatly by using PHPLint
  39. // Copyright (c) 2009 Umberto Salsi
  40. // This is free software; see the license for copying conditions.
  41. // More info: http://www.icosaedro.it/phplint/
  42. /*.
  43. require_module 'standard';
  44. require_module 'dom';
  45. require_module 'session';
  46. require_module 'pcre';
  47. require_module 'hash';
  48. .*/
  49. error_reporting(E_ALL | E_STRICT);
  50. ini_set('display_errors', '1');
  51. /**
  52. * Common utility functions
  53. *
  54. * @package ajaxUnit
  55. * @version 1.14 (revision number of this common functions class only)
  56. */
  57. interface I_ajaxUnit_common {
  58. // const PACKAGE = 'ajaxUnit',
  59. // VERSION = '0.17', // Version 1.13: added
  60. // Version 1.14: PACKAGE & VERSION now hard-coded by build process.
  61. const HASH_FUNCTION = 'SHA256',
  62. URL_SEPARATOR = '/',
  63. // Behaviour settings for strleft()
  64. STRLEFT_MODE_NONE = 0,
  65. STRLEFT_MODE_ALL = 1,
  66. // Behaviour settings for getURL()
  67. URL_MODE_PROTOCOL = 1,
  68. URL_MODE_HOST = 2,
  69. URL_MODE_PORT = 4,
  70. URL_MODE_PATH = 8,
  71. URL_MODE_ALL = 15,
  72. // Behaviour settings for getPackage()
  73. // PACKAGE_CASE_DEFAULT = 0,
  74. //// PACKAGE_CASE_LOWER = 0,
  75. // PACKAGE_CASE_CAMEL = 1,
  76. // PACKAGE_CASE_UPPER = 2,
  77. // Version 1.14: PACKAGE & VERSION now hard-coded by build process.
  78. // Extra GLOB constant for safe_glob()
  79. GLOB_NODIR = 256,
  80. GLOB_PATH = 512,
  81. GLOB_NODOTS = 1024,
  82. GLOB_RECURSE = 2048,
  83. // Email validation constants
  84. ISEMAIL_VALID = 0,
  85. ISEMAIL_TOOLONG = 1,
  86. ISEMAIL_NOAT = 2,
  87. ISEMAIL_NOLOCALPART = 3,
  88. ISEMAIL_NODOMAIN = 4,
  89. ISEMAIL_ZEROLENGTHELEMENT = 5,
  90. ISEMAIL_BADCOMMENT_START = 6,
  91. ISEMAIL_BADCOMMENT_END = 7,
  92. ISEMAIL_UNESCAPEDDELIM = 8,
  93. ISEMAIL_EMPTYELEMENT = 9,
  94. ISEMAIL_UNESCAPEDSPECIAL = 10,
  95. ISEMAIL_LOCALTOOLONG = 11,
  96. ISEMAIL_IPV4BADPREFIX = 12,
  97. ISEMAIL_IPV6BADPREFIXMIXED = 13,
  98. ISEMAIL_IPV6BADPREFIX = 14,
  99. ISEMAIL_IPV6GROUPCOUNT = 15,
  100. ISEMAIL_IPV6DOUBLEDOUBLECOLON = 16,
  101. ISEMAIL_IPV6BADCHAR = 17,
  102. ISEMAIL_IPV6TOOMANYGROUPS = 18,
  103. ISEMAIL_TLD = 19,
  104. ISEMAIL_DOMAINEMPTYELEMENT = 20,
  105. ISEMAIL_DOMAINELEMENTTOOLONG = 21,
  106. ISEMAIL_DOMAINBADCHAR = 22,
  107. ISEMAIL_DOMAINTOOLONG = 23,
  108. ISEMAIL_TLDNUMERIC = 24,
  109. ISEMAIL_DOMAINNOTFOUND = 25;
  110. // ISEMAIL_NOTDEFINED = 99;
  111. // Basic utility functions
  112. public static /*.string.*/ function strleft(/*.string.*/ $haystack, /*.string.*/ $needle);
  113. public static /*.mixed.*/ function getInnerHTML(/*.string.*/ $html, /*.string.*/ $tag);
  114. public static /*.array[string][string]string.*/ function meta_to_array(/*.string.*/ $html);
  115. public static /*.string.*/ function var_dump_to_HTML(/*.string.*/ $var_dump, $offset = 0);
  116. public static /*.string.*/ function array_to_HTML(/*.array[]mixed.*/ $source = NULL);
  117. // Environment functions
  118. // public static /*.string.*/ function getPackage($mode = self::PACKAGE_CASE_DEFAULT); // Version 1.14: PACKAGE & VERSION now hard-coded by build process.
  119. public static /*.string.*/ function getURL($mode = self::URL_MODE_PATH, $filename = '');
  120. public static /*.string.*/ function docBlock_to_HTML(/*.string.*/ $php);
  121. // File system functions
  122. public static /*.mixed.*/ function safe_glob(/*.string.*/ $pattern, /*.int.*/ $flags = 0);
  123. public static /*.string.*/ function getFileContents(/*.string.*/ $filename, /*.int.*/ $flags = 0, /*.object.*/ $context = NULL, /*.int.*/ $offset = -1, /*.int.*/ $maxLen = -1);
  124. public static /*.string.*/ function findIndexFile(/*.string.*/ $folder);
  125. public static /*.string.*/ function findTarget(/*.string.*/ $target);
  126. // Data functions
  127. public static /*.string.*/ function makeId();
  128. public static /*.string.*/ function makeUniqueKey(/*.string.*/ $id);
  129. public static /*.string.*/ function mt_shuffle(/*.string.*/ $str, /*.int.*/ $seed = 0);
  130. // public static /*.void.*/ function mt_shuffle_array(/*.array.*/ &$arr, /*.int.*/ $seed = 0);
  131. public static /*.string.*/ function prkg(/*.int.*/ $index, /*.int.*/ $length = 6, /*.int.*/ $base = 34, /*.int.*/ $seed = 0);
  132. // Validation functions
  133. // public static /*.boolean.*/ function is_email(/*.string.*/ $email, $checkDNS = false);
  134. public static /*.mixed.*/ function is_email(/*.string.*/ $email, $checkDNS = false, $diagnose = false); // New parameters from version 1.8
  135. }
  136. /**
  137. * Common utility functions
  138. */
  139. abstract class ajaxUnit_common implements I_ajaxUnit_common {
  140. /**
  141. * Return the beginning of a string, up to but not including the search term.
  142. *
  143. * @param string $haystack The string containing the search term
  144. * @param string $needle The end point of the returned string. In other words, if <var>needle</var> is found then the begging of <var>haystack</var> is returned up to the character before <needle>.
  145. * @param int $mode If <var>needle</var> is not found then <pre>FALSE</pre> will be returned. */
  146. public static /*.string.*/ function strleft(/*.string.*/ $haystack, /*.string.*/ $needle, /*.int.*/ $mode = self::STRLEFT_MODE_NONE) {
  147. $posNeedle = strpos($haystack, $needle);
  148. if ($posNeedle === false) {
  149. if ($mode === self::STRLEFT_MODE_ALL)
  150. return $haystack;
  151. else
  152. return (string) $posNeedle;
  153. } else
  154. return substr($haystack, 0, $posNeedle);
  155. }
  156. /**
  157. * Return the contents of an HTML element, the first one matching the <var>tag</var> parameter.
  158. *
  159. * @param string $html The string containing the html to be searched
  160. * @param string $tag The type of element to search for. The contents of first matching element will be returned. If the element doesn't exist then <var>false</var> is returned.
  161. */
  162. public static /*.mixed.*/ function getInnerHTML(/*.string.*/ $html, /*.string.*/ $tag) {
  163. $pos_tag_open_start = stripos($html, "<$tag") ; if ($pos_tag_open_start === false) return false;
  164. $pos_tag_open_end = strpos($html, '>', $pos_tag_open_start) ; if ($pos_tag_open_end === false) return false;
  165. $pos_tag_close = stripos($html, "</$tag>", $pos_tag_open_end) ; if ($pos_tag_close === false) return false;
  166. return substr($html, $pos_tag_open_end + 1, $pos_tag_close - $pos_tag_open_end - 1);
  167. }
  168. /**
  169. * Return the <var>meta</var> tags from an HTML document as an array.
  170. *
  171. * The array returned will have a 'key' element which is an array of name/value pairs representing all the metadata
  172. * from the HTML document. If there are any <var>name</var> or <var>http-equiv</var> meta elements
  173. * these will be in their own sub-array. The 'key' sub-array combines all meta tags.
  174. *
  175. * Qualifying attributes such as <var>lang</var> and <var>scheme</var> have their own sub-arrays with the same key
  176. * as the main sub-array.
  177. *
  178. * Here are some example meta tags:
  179. *
  180. * <pre>
  181. * <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
  182. * <meta name="description" content="Free Web tutorials" />
  183. * <meta name="keywords" content="HTML,CSS,XML,JavaScript" />
  184. * <meta name="author" content="Hege Refsnes" />
  185. * <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" />
  186. * <META NAME="ROBOTS" CONTENT="NOYDIR">
  187. * <META NAME="Slurp" CONTENT="NOYDIR">
  188. * <META name="author" content="John Doe">
  189. * <META name ="copyright" content="&copy; 1997 Acme Corp.">
  190. * <META name= "keywords" content="corporate,guidelines,cataloging">
  191. * <META name = "date" content="1994-11-06T08:49:37+00:00">
  192. * <meta name="DC.title" lang="en" content="Services to Government" >
  193. * <meta name="DCTERMS.modified" scheme="XSD.date" content="2007-07-22" >
  194. * <META http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
  195. * <META name="geo.position" content="26.367559;-80.12172">
  196. * <META name="geo.region" content="US-FL">
  197. * <META name="geo.placename" content="Boca Raton, FL">
  198. * <META name="ICBM" content="26.367559, -80.12172">
  199. * <META name="DC.title" content="THE NAME OF YOUR SITE">
  200. * </pre>
  201. *
  202. * Here is a dump of the returned array:
  203. *
  204. * <pre>
  205. * array (
  206. * 'key' =>
  207. * array (
  208. * 'Content-Type' => 'text/html; charset=iso-8859-1',
  209. * 'description' => 'Free Web tutorials',
  210. * 'keywords' => 'corporate,guidelines,cataloging',
  211. * 'author' => 'John Doe',
  212. * 'ROBOTS' => 'NOYDIR',
  213. * 'Slurp' => 'NOYDIR',
  214. * 'copyright' => '&copy; 1997 Acme Corp.',
  215. * 'date' => '1994-11-06T08:49:37+00:00',
  216. * 'DC.title' => 'THE NAME OF YOUR SITE',
  217. * 'DCTERMS.modified' => '2007-07-22',
  218. * 'geo.position' => '26.367559;-80.12172',
  219. * 'geo.region' => 'US-FL',
  220. * 'geo.placename' => 'Boca Raton, FL',
  221. * 'ICBM' => '26.367559, -80.12172',
  222. * ),
  223. * 'http-equiv' =>
  224. * array (
  225. * 'Content-Type' => 'text/html; charset=iso-8859-1',
  226. * ),
  227. * 'name' =>
  228. * array (
  229. * 'description' => 'Free Web tutorials',
  230. * 'keywords' => 'corporate,guidelines,cataloging',
  231. * 'author' => 'John Doe',
  232. * 'ROBOTS' => 'NOYDIR',
  233. * 'Slurp' => 'NOYDIR',
  234. * 'copyright' => '&copy; 1997 Acme Corp.',
  235. * 'date' => '1994-11-06T08:49:37+00:00',
  236. * 'DC.title' => 'THE NAME OF YOUR SITE',
  237. * 'DCTERMS.modified' => '2007-07-22',
  238. * 'geo.position' => '26.367559;-80.12172',
  239. * 'geo.region' => 'US-FL',
  240. * 'geo.placename' => 'Boca Raton, FL',
  241. * 'ICBM' => '26.367559, -80.12172',
  242. * ),
  243. * 'lang' =>
  244. * array (
  245. * 'DC.title' => 'en',
  246. * ),
  247. * 'scheme' =>
  248. * array (
  249. * 'DCTERMS.modified' => 'XSD.date',
  250. * ),
  251. * </pre>
  252. *
  253. * Note how repeated tags cause the previous value to be overwritten in the resulting array
  254. * (for example the <var>Content-Type</var> and <var>keywords</var> tags appear twice but the
  255. * final array only has one element for each - the lowest one in the original list).
  256. *
  257. * @param string $html The string containing the html to be parsed
  258. */
  259. public static /*.array[string][string]string.*/ function meta_to_array(/*.string.*/ $html) {
  260. $keyAttributes = array('name', 'http-equiv', 'charset', 'itemprop');
  261. $tags = /*.(array[int][int]string).*/ array();
  262. $query = '?';
  263. preg_match_all("|<meta.+/$query>|i", $html, $tags);
  264. $meta = /*.(array[string][string]string).*/ array();
  265. $key_type = '';
  266. $key = '';
  267. $content = '';
  268. foreach ($tags[0] as $tag) {
  269. $attributes = array();
  270. $wip = /*.(array[string]string).*/ array();
  271. preg_match_all('|\\s(\\S+?)\\s*=\\s*"(.*?)"|', $tag, $attributes);
  272. unset($key_type);
  273. unset($key);
  274. unset($content);
  275. for ($i = 0; $i < count($attributes[1]); $i++) {
  276. $attribute = strtolower($attributes[1][$i]);
  277. $value = $attributes[2][$i];
  278. if (in_array($attribute, $keyAttributes)) {
  279. $key_type = $attribute;
  280. $key = $value;
  281. } elseif ($attribute === 'content') {
  282. $content = $value;
  283. } else {
  284. $wip[$attribute] = $value;
  285. }
  286. }
  287. if (isset($key_type)) {
  288. $meta['key'][$key] = $content;
  289. $meta[$key_type][$key] = $content;
  290. foreach ($wip as $attribute => $value) {
  291. $meta[$attribute][$key] = $value;
  292. }
  293. }
  294. }
  295. return $meta;
  296. }
  297. /**
  298. * Return the contents of a captured var_dump() as HTML. This is a recursive function.
  299. *
  300. * @param string $var_dump The captured <var>var_dump()</var>.
  301. * @param int $offset Whereabouts to start in the captured string. Defaults to the beginning of the string.
  302. */
  303. public static /*.string.*/ function var_dump_to_HTML(/*.string.*/ $var_dump, $offset = 0) {
  304. $indent = '';
  305. $value = '';
  306. while ((boolean) ($posStart = strpos($var_dump, '(', $offset))) {
  307. $type = substr($var_dump, $offset, $posStart - $offset);
  308. $nests = strrpos($type, ' ');
  309. if ($nests === false) $nests = 0; else $nests = intval(($nests + 1) / 2);
  310. $indent = str_pad('', $nests * 3, "\t");
  311. $type = trim($type);
  312. $offset = ++$posStart;
  313. $posEnd = strpos($var_dump, ')', $offset); if ($posEnd === false) break;
  314. $offset = $posEnd + 1;
  315. $value = substr($var_dump, $posStart, $posEnd - $posStart);
  316. switch ($type) {
  317. case 'string':
  318. $length = (int) $value;
  319. $value = '<pre>' . htmlspecialchars(substr($var_dump, $offset + 2, $length)) . '</pre>';
  320. $offset += $length + 3;
  321. break;
  322. case 'array':
  323. $elementTellTale = "\n" . str_pad('', ($nests + 1) * 2) . '['; // Not perfect but the best var_dump will allow
  324. $elementCount = (int) $value;
  325. $value = "\n$indent<table>\n";
  326. for ($i = 1; $i <= $elementCount; $i++) {
  327. $posStart = strpos($var_dump, $elementTellTale, $offset); if ($posStart === false) break;
  328. $posStart += ($nests + 1) * 2 + 2;
  329. $offset = $posStart;
  330. $posEnd = strpos($var_dump, ']', $offset); if ($posEnd === false) break;
  331. $offset = $posEnd + 4; // Read past the =>\n
  332. $key = substr($var_dump, $posStart, $posEnd - $posStart);
  333. if (!is_numeric($key)) $key = substr($key, 1, strlen($key) - 2); // Strip off the double quotes
  334. $search = ($i === $elementCount) ? "\n" . str_pad('', $nests * 2) . '}' : $elementTellTale;
  335. $posStart = strpos($var_dump, $search, $offset); if ($posStart === false) break;
  336. $next = substr($var_dump, $offset, $posStart - $offset);
  337. $offset = $posStart;
  338. $inner_value = self::var_dump_to_HTML($next);
  339. $value .= "$indent\t<tr>\n";
  340. $value .= "$indent\t\t<td>$key</td>\n";
  341. $value .= "$indent\t\t<td>$inner_value</td>\n";
  342. $value .= "$indent\t</tr>\n";
  343. }
  344. $value .= "$indent</table>\n";
  345. break;
  346. case 'object':
  347. if ($value === '__PHP_Incomplete_Class') {
  348. $posStart = strpos($var_dump, '(', $offset); if ($posStart === false) break;
  349. $offset = ++$posStart;
  350. echo "$indent Corrected \$offset = $offset\n"; // debug
  351. $posEnd = strpos($var_dump, ')', $offset); if ($posEnd === false) break;
  352. $offset = $posEnd + 1;
  353. echo "$indent Corrected \$offset = $offset\n"; // debug
  354. $value = substr($var_dump, $posStart, $posEnd - $posStart);
  355. }
  356. break;
  357. default:
  358. break;
  359. }
  360. }
  361. return $value;
  362. }
  363. /**
  364. * Return the contents of an array as HTML (like <var>var_dump()</var> on steroids), including object members
  365. *
  366. * @param mixed $source The array to export. If it's empty then $GLOBALS is exported.
  367. */
  368. public static /*.string.*/ function array_to_HTML(/*.array[]mixed.*/ $source = NULL) {
  369. // If no specific array is passed we will export $GLOBALS to HTML
  370. // Unfortunately, this means we have to use var_dump() because var_export() barfs on $GLOBALS
  371. // In fact var_dump is easier to walk than var_export anyway so this is no bad thing.
  372. ob_start();
  373. if (empty($source)) var_dump($GLOBALS); else var_dump($source);
  374. $var_dump = ob_get_clean();
  375. return self::var_dump_to_HTML($var_dump);
  376. }
  377. ///**
  378. // * Return the name of this package. By default this will be in lower case for use in Javascript tags etc.
  379. // *
  380. // * @param int $mode One of the <var>PACKAGE_CASE_XXX</var> predefined constants defined in this class
  381. // */
  382. // public static /*.string.*/ function getPackage($mode = self::PACKAGE_CASE_DEFAULT) {
  383. // switch ($mode) {
  384. // case self::PACKAGE_CASE_CAMEL:
  385. // $package = self::PACKAGE;
  386. // break;
  387. // case self::PACKAGE_CASE_UPPER:
  388. // $package = strtoupper(self::PACKAGE);
  389. // break;
  390. // default:
  391. // $package = strtolower(self::PACKAGE);
  392. // break;
  393. // }
  394. //
  395. // return $package;
  396. // }
  397. /**
  398. * Return all or part of the URL of the current script.
  399. *
  400. * @param int $mode One of the <var>URL_MODE_XXX</var> predefined constants defined in this class
  401. * @param string $filename If this is not empty then the returned script name is forced to be this filename.
  402. */
  403. public static /*.string.*/ function getURL($mode = self::URL_MODE_PATH, $filename = 'ajaxUnit') {
  404. // Version 1.14: PACKAGE & VERSION now hard-coded by build process.
  405. $portInteger = array_key_exists('SERVER_PORT', $_SERVER) ? (int) $_SERVER['SERVER_PORT'] : 0;
  406. if (array_key_exists('HTTPS', $_SERVER) && $_SERVER['HTTPS'] === 'on') {
  407. $protocolType = 'https';
  408. } else if (array_key_exists('SERVER_PROTOCOL', $_SERVER)) {
  409. $protocolType = strtolower(self::strleft($_SERVER['SERVER_PROTOCOL'], self::URL_SEPARATOR, self::STRLEFT_MODE_ALL));
  410. } else if ($portInteger === 443) {
  411. $protocolType = 'https';
  412. } else {
  413. $protocolType = 'http';
  414. }
  415. if ($portInteger === 0) $portInteger = ($protocolType === 'https') ? 443 : 80;
  416. // Protocol
  417. if ((boolean) ($mode & self::URL_MODE_PROTOCOL)) {
  418. $protocol = ($mode === self::URL_MODE_PROTOCOL) ? $protocolType : "$protocolType://";
  419. } else {
  420. $protocol = '';
  421. }
  422. // Host
  423. if ((boolean) ($mode & self::URL_MODE_HOST)) {
  424. $host = array_key_exists('HTTP_HOST', $_SERVER) ? self::strleft($_SERVER['HTTP_HOST'], ':', self::STRLEFT_MODE_ALL) : '';
  425. } else {
  426. $host = '';
  427. }
  428. // Port
  429. if ((boolean) ($mode & self::URL_MODE_PORT)) {
  430. $port = (string) $portInteger;
  431. if ($mode !== self::URL_MODE_PORT)
  432. $port = (($protocolType === 'http' && $portInteger === 80) || ($protocolType === 'https' && $portInteger === 443)) ? '' : ":$port";
  433. } else {
  434. $port = '';
  435. }
  436. // Path
  437. if ((boolean) ($mode & self::URL_MODE_PATH)) {
  438. $includePath = __FILE__;
  439. $scriptPath = realpath($_SERVER['SCRIPT_FILENAME']);
  440. if (DIRECTORY_SEPARATOR !== self::URL_SEPARATOR) {
  441. $includePath = (string) str_replace(DIRECTORY_SEPARATOR, self::URL_SEPARATOR , $includePath);
  442. $scriptPath = (string) str_replace(DIRECTORY_SEPARATOR, self::URL_SEPARATOR , $scriptPath);
  443. }
  444. /*
  445. echo "<pre>\n"; // debug
  446. echo "\$_SERVER['SCRIPT_FILENAME'] = " . $_SERVER['SCRIPT_FILENAME'] . "\n"; // debug
  447. echo "\$_SERVER['SCRIPT_NAME'] = " . $_SERVER['SCRIPT_NAME'] . "\n"; // debug
  448. echo "dirname(\$_SERVER['SCRIPT_NAME']) = " . dirname($_SERVER['SCRIPT_NAME']) . "\n"; // debug
  449. echo "\$includePath = $includePath\n"; // debug
  450. echo "\$scriptPath = $scriptPath\n"; // debug
  451. //echo self::array_to_HTML(); // debug
  452. echo "</pre>\n"; // debug
  453. */
  454. $start = strpos(strtolower($scriptPath), strtolower($_SERVER['SCRIPT_NAME']));
  455. $path = ($start === false) ? dirname($_SERVER['SCRIPT_NAME']) : dirname(substr($includePath, $start));
  456. $path .= self::URL_SEPARATOR . $filename;
  457. } else {
  458. $path = '';
  459. }
  460. return $protocol . $host . $port . $path;
  461. }
  462. /**
  463. * Convert a DocBlock to HTML (see http://java.sun.com/j2se/javadoc/writingdoccomments/index.html)
  464. *
  465. * @param string $docBlock Some PHP code containing a valid DocBlock.
  466. */
  467. public static /*.string.*/ function docBlock_to_HTML(/*.string.*/ $php) {
  468. // Updated in version 1.12 (bug fixes and formatting)
  469. // $package = self::getPackage(self::PACKAGE_CASE_CAMEL); // Version 1.14: PACKAGE & VERSION now hard-coded by build process.
  470. $eol = "\r\n";
  471. $tagStart = strpos($php, "/**$eol * ");
  472. if ($tagStart === false) return 'Development version';
  473. // Get summary and long description
  474. $tagStart += 8;
  475. $tagEnd = strpos($php, $eol, $tagStart);
  476. $summary = substr($php, $tagStart, $tagEnd - $tagStart);
  477. $tagStart = $tagEnd + 7;
  478. $tagPos = strpos($php, "$eol * @") + 2;
  479. $description = substr($php, $tagStart, $tagPos - $tagStart - 7);
  480. $description = (string) str_replace(' * ', '' , $description);
  481. // Get tags and values from DocBlock
  482. do {
  483. $tagStart = $tagPos + 4;
  484. $tagEnd = strpos($php, "\t", $tagStart);
  485. $tag = substr($php, $tagStart, $tagEnd - $tagStart);
  486. $offset = $tagEnd + 1;
  487. $tagPos = strpos($php, $eol, $offset);
  488. $value = htmlspecialchars(substr($php, $tagEnd + 1, $tagPos - $tagEnd - 1));
  489. $tagPos = strpos($php, " * @", $offset);
  490. // $$tag = htmlspecialchars($value); // The easy way. But PHPlint doesn't like it, so...
  491. // $package = '';
  492. // $summary = '';
  493. // $description = '';
  494. switch ($tag) {
  495. case 'license': $license = $value; break;
  496. case 'author': $author = $value; break;
  497. case 'link': $link = $value; break;
  498. case 'version': $version = $value; break;
  499. case 'copyright': $copyright = $value; break;
  500. default: $value = $value;
  501. }
  502. } while ((boolean) $tagPos);
  503. // Add some links
  504. // 1. License
  505. if (isset($license) && (boolean) strpos($license, '://')) {
  506. $tagPos = strpos($license, ' ');
  507. $license = '<a href="' . substr($license, 0, $tagPos) . '">' . substr($license, $tagPos + 1) . '</a>';
  508. }
  509. // 2. Author
  510. if (isset($author) && preg_match('/&lt;.+@.+&gt;/', $author) > 0) {
  511. $tagStart = strpos($author, '&lt;') + 4;
  512. $tagEnd = strpos($author, '&gt;', $tagStart);
  513. $author = '<a href="mailto:' . substr($author, $tagStart, $tagEnd - $tagStart) . '">' . substr($author, 0, $tagStart - 5) . '</a>';
  514. }
  515. // 3. Link
  516. if (isset($link) && (boolean) strpos($link, '://')) {
  517. $link = '<a href="' . $link . '">' . $link . '</a>';
  518. }
  519. // Build the HTML
  520. $html = <<<HTML
  521. <h1>ajaxUnit</h1>
  522. <h2>$summary</h2>
  523. <pre>$description</pre>
  524. <hr />
  525. <table>
  526. HTML;
  527. // Version 1.14: PACKAGE & VERSION now hard-coded by build process.
  528. if (isset($version)) $html .= "\t\t<tr><td>Version</td><td>$version</td></tr>\n";
  529. if (isset($copyright)) $html .= "\t\t<tr><td>Copyright</td><td>$copyright</td></tr>\n";
  530. if (isset($license)) $html .= "\t\t<tr><td>License</td><td>$license</td></tr>\n";
  531. if (isset($author)) $html .= "\t\t<tr><td>Author</td><td>$author</td></tr>\n";
  532. if (isset($link)) $html .= "\t\t<tr><td>Link</td><td>$link</td></tr>\n";
  533. $html .= "\t</table>";
  534. return $html;
  535. }
  536. /**
  537. * glob() replacement (in case glob() is disabled).
  538. *
  539. * Function glob() is prohibited on some server (probably in safe mode)
  540. * (Message "Warning: glob() has been disabled for security reasons in
  541. * (script) on line (line)") for security reasons as stated on:
  542. * http://seclists.org/fulldisclosure/2005/Sep/0001.html
  543. *
  544. * safe_glob() intends to replace glob() using readdir() & fnmatch() instead.
  545. * Supported flags: GLOB_MARK, GLOB_NOSORT, GLOB_ONLYDIR
  546. * Additional flags: GLOB_NODIR, GLOB_PATH, GLOB_NODOTS, GLOB_RECURSE
  547. * (these were not original glob() flags)
  548. * @author BigueNique AT yahoo DOT ca
  549. */
  550. public static /*.mixed.*/ function safe_glob(/*.string.*/ $pattern, /*.int.*/ $flags = 0) {
  551. $split = explode('/', (string) str_replace('\\', '/', $pattern));
  552. $mask = (string) array_pop($split);
  553. $path = (count($split) === 0) ? '.' : implode('/', $split);
  554. $dir = @opendir($path);
  555. if ($dir === false) return false;
  556. $glob = /*.(array[int]).*/ array();
  557. do {
  558. $filename = readdir($dir);
  559. if ($filename === false) break;
  560. $is_dir = is_dir("$path/$filename");
  561. $is_dot = in_array($filename, array('.', '..'));
  562. // Recurse subdirectories (if GLOB_RECURSE is supplied)
  563. if ($is_dir && !$is_dot && (($flags & self::GLOB_RECURSE) !== 0)) {
  564. $sub_glob = /*.(array[int]).*/ self::safe_glob($path.'/'.$filename.'/'.$mask, $flags);
  565. // array_prepend($sub_glob, ((boolean) ($flags & self::GLOB_PATH) ? '' : $filename.'/'));
  566. $glob = /*.(array[int]).*/ array_merge($glob, $sub_glob);
  567. }
  568. // Match file mask
  569. if (fnmatch($mask, $filename)) {
  570. if ( ((($flags & GLOB_ONLYDIR) === 0) || $is_dir)
  571. && ((($flags & self::GLOB_NODIR) === 0) || !$is_dir)
  572. && ((($flags & self::GLOB_NODOTS) === 0) || !$is_dot)
  573. )
  574. $glob[] = (($flags & self::GLOB_PATH) !== 0 ? $path.'/' : '') . $filename . (($flags & GLOB_MARK) !== 0 ? '/' : '');
  575. }
  576. } while(true);
  577. closedir($dir);
  578. if (($flags & GLOB_NOSORT) === 0) sort($glob);
  579. return $glob;
  580. }
  581. /**
  582. * Return file contents as a string. Fail silently if the file can't be opened.
  583. *
  584. * The parameters are the same as the built-in PHP function {@link http://www.php.net/file_get_contents file_get_contents}
  585. */
  586. public static /*.string.*/ function getFileContents(/*.string.*/ $filename, /*.int.*/ $flags = 0, /*.object.*/ $context = NULL, /*.int.*/ $offset = -1, /*.int.*/ $maxlen = -1) {
  587. // From the documentation of file_get_contents:
  588. // Note: The default value of maxlen is not actually -1; rather, it is an internal PHP value which means to copy the entire stream until end-of-file is reached. The only way to specify this default value is to leave it out of the parameter list.
  589. if ($maxlen === -1) {
  590. $contents = @file_get_contents($filename, $flags, $context, $offset);
  591. } else {
  592. $contents = @file_get_contents($filename, $flags, $context, $offset, $maxlen);
  593. // version 1.9 - remembered the @s
  594. }
  595. if ($contents === false) $contents = '';
  596. return $contents;
  597. }
  598. /**
  599. * Return the name of the index file (e.g. <var>index.php</var>) from a folder
  600. *
  601. * @param string $folder The folder to look for the index file. If not a folder or no index file can be found then an empty string is returned.
  602. */
  603. public static /*.string.*/ function findIndexFile(/*.string.*/ $folder) {
  604. if (!is_dir($folder)) return '';
  605. $filelist = array('index.php', 'index.pl', 'index.cgi', 'index.asp', 'index.shtml', 'index.html', 'index.htm', 'default.php', 'default.pl', 'default.cgi', 'default.asp', 'default.shtml', 'default.html', 'default.htm', 'home.php', 'home.pl', 'home.cgi', 'home.asp', 'home.shtml', 'home.html', 'home.htm');
  606. foreach ($filelist as $filename) {
  607. $target = $folder . DIRECTORY_SEPARATOR . $filename;
  608. if (is_file($target)) return $target;
  609. }
  610. return '';
  611. }
  612. /**
  613. * Return the name of the target file from a string that might be a directory or just a basename without a suffix. If it's a directory then look for an index file in the directory.
  614. *
  615. * @param string $target The file to look for or folder to look in. If no file can be found then an empty string is returned.
  616. */
  617. public static /*.string.*/ function findTarget(/*.string.*/ $target) {
  618. // Is it actually a file? If so, look no further
  619. if (is_file($target)) return $target;
  620. // Added in version 1.7
  621. // Is it a basename? i.e. can we find $target.html or something?
  622. $suffixes = array('shtml', 'html', 'php', 'pl', 'cgi', 'asp', 'htm');
  623. foreach ($suffixes as $suffix) {
  624. $filename = "$target.$suffix";
  625. if (is_file($filename)) return $filename;
  626. }
  627. // Otherwise, let's assume it's a directory and try to find an index file in that directory
  628. return self::findIndexFile($target);
  629. }
  630. /**
  631. * Make a unique ID based on the current date and time
  632. */
  633. public static /*.string.*/ function makeId() {
  634. // Note could also try this: return md5(uniqid(mt_rand(), true));
  635. list($usec, $sec) = explode(" ", (string) microtime());
  636. return base_convert($sec, 10, 36) . base_convert((string) mt_rand(0, 35), 10, 36) . str_pad(base_convert(($usec * 1000000), 10, 36), 4, '_', STR_PAD_LEFT);
  637. }
  638. /**
  639. * Make a unique hash key from a string (usually an ID)
  640. */
  641. public static /*.string.*/ function makeUniqueKey(/*.string.*/ $id) {
  642. return hash(self::HASH_FUNCTION, $_SERVER['REQUEST_TIME'] . $id);
  643. }
  644. // Added in version 1.10
  645. /**
  646. * Shuffle a string using the Mersenne Twist PRNG (can be deterministically seeded)
  647. *
  648. * @param string $str The string to be shuffled
  649. * @param int $seed The seed for the PRNG means this can be used to shuffle the string in the same order every time
  650. */
  651. public static /*.string.*/ function mt_shuffle(/*.string.*/ $str, /*.int.*/ $seed = 0) {
  652. $count = strlen($str);
  653. $result = $str;
  654. // Seed the RNG with a deterministic seed
  655. mt_srand($seed);
  656. // Shuffle the digits
  657. for ($element = $count - 1; $element >= 0; $element--) {
  658. $shuffle = mt_rand(0, $element);
  659. $value = $result[$shuffle];
  660. // $result[$shuffle] = $result[$element];
  661. // $result[$element] = $value; // PHPLint doesn't like this syntax, so...
  662. substr_replace($result, $result[$element], $shuffle, 1);
  663. substr_replace($result, $value, $element, 1);
  664. }
  665. return $result;
  666. }
  667. // Added in version 1.10
  668. /**
  669. * Shuffle an array using the Mersenne Twist PRNG (can be deterministically seeded)
  670. *
  671. */
  672. public static /*.void.*/ function mt_shuffle_array(/*.array.*/ &$arr, /*.int.*/ $seed = 0) {
  673. $count = count($arr);
  674. $keys = array_keys($arr);
  675. // Seed the RNG with a deterministic seed
  676. mt_srand($seed);
  677. // Shuffle the digits
  678. for ($element = $count - 1; $element >= 0; $element--) {
  679. $shuffle = mt_rand(0, $element);
  680. $key_shuffle = $keys[$shuffle];
  681. $key_element = $keys[$element];
  682. $value = $arr[$key_shuffle];
  683. $arr[$key_shuffle] = $arr[$key_element];
  684. $arr[$key_element] = $value;
  685. }
  686. }
  687. // Added in version 1.10
  688. /**
  689. * The Pseudo-Random Key Generator returns an apparently random key of
  690. * length $length and comprising digits specified by $base. However, for
  691. * a given seed this key depends only on $index.
  692. *
  693. * In other words, if you keep the $seed constant then you'll get a
  694. * non-repeating series of keys as you increment $index but these keys
  695. * will be returned in a pseudo-random order.
  696. *
  697. * The $seed parameter is available in case you want your series of keys
  698. * to come out in a different order to mine.
  699. *
  700. * Comparison of bases:
  701. * <pre>
  702. * +------+----------------+---------------------------------------------+
  703. * | | Max keys | |
  704. * | | (based on | |
  705. * | Base | $length = 6) | Notes |
  706. * +------+----------------+---------------------------------------------+
  707. * | 2 | 64 | Uses digits 0 and 1 only |
  708. * | 8 | 262,144 | Uses digits 0-7 only |
  709. * | 10 | 1,000,000 | Good choice if you need integer keys |
  710. * | 16 | 16,777,216 | Good choice if you need hex keys |
  711. * | 26 | 308,915,776 | Good choice if you need purely alphabetic |
  712. * | | | keys (case-insensitive) |
  713. * | 32 | 1,073,741,824 | Smallest base that gives you a billion keys |
  714. * | | | in 6 digits |
  715. * | 34 | 1,544,804,416 | (default) Good choice if you want to |
  716. * | | | maximise your keyset size but still |
  717. * | | | generate keys that are unambiguous and |
  718. * | | | case-insensitive (no confusion between 1, I |
  719. * | | | and l for instance) |
  720. * | 36 | 2,176,782,336 | Same digits as base-34 but includes 'O' and |
  721. * | | | 'I' (may be confused with '0' and '1' in |
  722. * | | | some fonts) |
  723. * | 52 | 19,770,609,664 | Good choice if you need purely alphabetic |
  724. * | | | keys (case-sensitive) |
  725. * | 62 | 56,800,235,584 | Same digits as other URL shorteners |
  726. * | | | (e.g bit.ly) |
  727. * | 66 | 82,653,950,016 | Includes all legal URI characters |
  728. * | | | (http://tools.ietf.org/html/rfc3986) |
  729. * | | | This is the maximum size of keyset that |
  730. * | | | results in a legal URL for a given length |
  731. * | | | of key. |
  732. * +------+----------------+---------------------------------------------+
  733. * </pre>
  734. * @param int $index The number to be converted into a key
  735. * @param int $length The length of key to be returned. Along with the $base this determines the size of the keyset
  736. * @param int $base The number of distinct characters that can be included in the key to be returned. Along with the $length this determines the size of the keyset
  737. * @param int $seed The seed for the PRNG means this can be used to generate keys in the same sequence every time
  738. */
  739. public static /*.string.*/ function prkg($index, $length = 6, $base = 34, $seed = 0) {
  740. /*
  741. To return a pseudo-random key, we will take $index, convert it
  742. to base $base, then randomize the order of the digits. In
  743. addition we will give each digit a random offset.
  744. All the randomization operations are deterministic (based on
  745. $seed) so each time the function is called we will get the
  746. same shuffling of digits and the same offset for each digit.
  747. */
  748. $digits = '0123456789ABCDEFGHJKLMNPQRSTUVWXYZIOabcdefghijklmnopqrstuvwxyz-._~';
  749. // ^ base 34 recommended
  750. // Is $base in range?
  751. if ($base < 2) {die('Base must be greater than or equal to 2');}
  752. if ($base > 66) {die('Base must be less than or equal to 66');}
  753. // Is $length in range?
  754. if ($length < 1) {die('Length must be greater than or equal to 1');}
  755. // Max length depends on arithmetic functions of PHP
  756. // Is $index in range?
  757. $max_index = (int) pow($base, $length);
  758. if ($index < 0) {die('Index must be greater than or equal to 0');}
  759. if ($index > $max_index) {die('Index must be less than or equal to ' . $max_index);}
  760. // Seed the RNG with a deterministic seed
  761. mt_srand($seed);
  762. // Convert to $base
  763. $remainder = $index;
  764. $digit = 0;
  765. $result = '';
  766. while ($digit < $length) {
  767. $unit = (int) pow($base, $length - $digit++ - 1);
  768. $value = (int) floor($remainder / $unit);
  769. $remainder = $remainder - ($value * $unit);
  770. // Shift the digit
  771. $value = ($value + mt_rand(0, $base - 1)) % $base;
  772. $result .= $digits[$value];
  773. }
  774. // Shuffle the digits
  775. $result = self::mt_shuffle($result, $seed);
  776. // We're done
  777. return $result;
  778. }
  779. // Updated in version 1.8
  780. /**
  781. * Check that an email address conforms to RFC5322 and other RFCs
  782. *
  783. * @param boolean $checkDNS If true then a DNS check for A and MX records will be made
  784. * @param boolean $diagnose If true then return an integer error number rather than true or false
  785. */
  786. public static /*.mixed.*/ function is_email (/*.string.*/ $email, $checkDNS = false, $diagnose = false) {
  787. // Check that $email is a valid address. Read the following RFCs to understand the constraints:
  788. // (http://tools.ietf.org/html/rfc5322)
  789. // (http://tools.ietf.org/html/rfc3696)
  790. // (http://tools.ietf.org/html/rfc5321)
  791. // (http://tools.ietf.org/html/rfc4291#section-2.2)
  792. // (http://tools.ietf.org/html/rfc1123#section-2.1)
  793. // the upper limit on address lengths should normally be considered to be 256
  794. // (http://www.rfc-editor.org/errata_search.php?rfc=3696)
  795. // NB I think John Klensin is misreading RFC 5321 and the the limit should actually be 254
  796. // However, I will stick to the published number until it is changed.
  797. //
  798. // The maximum total length of a reverse-path or forward-path is 256
  799. // characters (including the punctuation and element separators)
  800. // (http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3)
  801. $emailLength = strlen($email);
  802. if ($emailLength > 256) if ($diagnose) return self::ISEMAIL_TOOLONG; else return false; // Too long
  803. // Contemporary email addresses consist of a "local part" separated from
  804. // a "domain part" (a fully-qualified domain name) by an at-sign ("@").
  805. // (http://tools.ietf.org/html/rfc3696#section-3)
  806. $atIndex = strrpos($email,'@');
  807. if ($atIndex === false) if ($diagnose) return self::ISEMAIL_NOAT; else return false; // No at-sign
  808. if ($atIndex === 0) if ($diagnose) return self::ISEMAIL_NOLOCALPART; else return false; // No local part
  809. if ($atIndex === $emailLength - 1) if ($diagnose) return self::ISEMAIL_NODOMAIN; else return false; // No domain part
  810. // revision 1.14: Length test bug suggested by Andrew Campbell of Gloucester, MA
  811. // Sanitize comments
  812. // - remove nested comments, quotes and dots in comments
  813. // - remove parentheses and dots from quoted strings
  814. $braceDepth = 0;
  815. $inQuote = false;
  816. $escapeThisChar = false;
  817. for ($i = 0; $i < $emailLength; ++$i) {
  818. $char = $email[$i];
  819. $replaceChar = false;
  820. if ($char === '\\') {
  821. $escapeThisChar = !$escapeThisChar; // Escape the next character?
  822. } else {
  823. switch ($char) {
  824. case '(':
  825. if ($escapeThisChar) {
  826. $replaceChar = true;
  827. } else {
  828. if ($inQuote) {
  829. $replaceChar = true;
  830. } else {
  831. if ($braceDepth++ > 0) $replaceChar = true; // Increment brace depth
  832. }
  833. }
  834. break;
  835. case ')':
  836. if ($escapeThisChar) {
  837. $replaceChar = true;
  838. } else {
  839. if ($inQuote) {
  840. $replaceChar = true;
  841. } else {
  842. if (--$braceDepth > 0) $replaceChar = true; // Decrement brace depth
  843. if ($braceDepth < 0) $braceDepth = 0;
  844. }
  845. }
  846. break;
  847. case '"':
  848. if ($escapeThisChar) {
  849. $replaceChar = true;
  850. } else {
  851. if ($braceDepth === 0) {
  852. $inQuote = !$inQuote; // Are we inside a quoted string?
  853. } else {
  854. $replaceChar = true;
  855. }
  856. }
  857. break;
  858. case '.': // Dots don't help us either
  859. if ($escapeThisChar) {
  860. $replaceChar = true;
  861. } else {
  862. if ($braceDepth > 0) $replaceChar = true;
  863. }
  864. break;
  865. default:
  866. }
  867. $escapeThisChar = false;
  868. // if ($replaceChar) $email[$i] = 'x'; // Replace the offending character with something harmless
  869. // revision 1.12: Line above replaced because PHPLint doesn't like that syntax
  870. if ($replaceChar) $email = (string) substr_replace($email, 'x', $i, 1); // Replace the offending character with something harmless
  871. }
  872. }
  873. $localPart = substr($email, 0, $atIndex);
  874. $domain = substr($email, $atIndex + 1);
  875. $FWS = "(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t]+)|(?:[ \\t]+(?:(?:\\r\\n)[ \\t]+)*))"; // Folding white space
  876. // Let's check the local part for RFC compliance...
  877. //
  878. // local-part = dot-atom / quoted-string / obs-local-part
  879. // obs-local-part = word *("." word)
  880. // (http://tools.ietf.org/html/rfc5322#section-3.4.1)
  881. //
  882. // Problem: need to distinguish between "first.last" and "first"."last"
  883. // (i.e. one element or two). And I suck at regexes.
  884. $dotArray = /*. (array[int]string) .*/ preg_split('/\\.(?=(?:[^\\"]*\\"[^\\"]*\\")*(?![^\\"]*\\"))/m', $localPart);
  885. $partLength = 0;
  886. foreach ($dotArray as $element) {
  887. // Remove any leading or trailing FWS
  888. $element = preg_replace("/^$FWS|$FWS\$/", '', $element);
  889. $elementLength = strlen($element);
  890. if ($elementLength === 0) if ($diagnose) return self::ISEMAIL_ZEROLENGTHELEMENT; else return false; // Can't have empty element (consecutive dots or dots at the start or end)
  891. // revision 1.15: Speed up the test and get rid of "unitialized string offset" notices from PHP
  892. // We need to remove any valid comments (i.e. those at the start or end of the element)
  893. if ($element[0] === '(') {
  894. $indexBrace = strpos($element, ')');
  895. if ($indexBrace !== false) {
  896. if (preg_match('/(?<!\\\\)[\\(\\)]/', substr($element, 1, $indexBrace - 1)) > 0) {
  897. if ($diagnose) return self::ISEMAIL_BADCOMMENT_START; else return false; // Illegal characters in comment
  898. }
  899. $element = substr($element, $indexBrace + 1, $elementLength - $indexBrace - 1);
  900. $elementLength = strlen($element);
  901. }
  902. }
  903. if ($element[$elementLength - 1] === ')') {
  904. $indexBrace = strrpos($element, '(');
  905. if ($indexBrace !== false) {
  906. if (preg_match('/(?<!\\\\)(?:[\\(\\)])/', substr($element, $indexBrace + 1, $elementLength - $indexBrace - 2)) > 0) {
  907. if ($diagnose) return self::ISEMAIL_BADCOMMENT_END; else return false; // Illegal characters in comment
  908. }
  909. $element = substr($element, 0, $indexBrace);
  910. $elementLength = strlen($element);
  911. }
  912. }
  913. // Remove any leading or trailing FWS around the element (inside any comments)
  914. $element = preg_replace("/^$FWS|$FWS\$/", '', $element);
  915. // What's left counts towards the maximum length for this part
  916. if ($partLength > 0) $partLength++; // for the dot
  917. $partLength += strlen($element);
  918. // Each dot-delimited component can be an atom or a quoted string
  919. // (because of the obs-local-part provision)
  920. if (preg_match('/^"(?:.)*"$/s', $element) > 0) {
  921. // Quoted-string tests:
  922. //
  923. // Remove any FWS
  924. $element = preg_replace("/(?<!\\\\)$FWS/", '', $element);
  925. // My regex skillz aren't up to distinguishing between \" \\" \\\" \\\\" etc.
  926. // So remove all \\ from the string first...
  927. $element = preg_replace('/\\\\\\\\/', ' ', $element);
  928. if (preg_match('/(?<!\\\\|^)["\\r\\n\\x00](?!$)|\\\\"$|""/', $element) > 0) if ($diagnose) return self::ISEMAIL_UNESCAPEDDELIM; else return false; // ", CR, LF and NUL must be escaped, "" is too short
  929. } else {
  930. // Unquoted string tests:
  931. //
  932. // Period (".") may...appear, but may not be used to start or end the
  933. // local part, nor may two or more consecutive periods appear.
  934. // (http://tools.ietf.org/html/rfc3696#section-3)
  935. //
  936. // A zero-length element implies a period at the beginning or end of the
  937. // local part, or two periods together. Either way it's not allowed.
  938. if ($element === '') if ($diagnose) return self::ISEMAIL_EMPTYELEMENT; else return false; // Dots in wrong place
  939. // Any ASCII graphic (printing) character other than the
  940. // at-sign ("@"), backslash, double quote, comma, or square brackets may
  941. // appear without quoting. If any of that list of excluded characters
  942. // are to appear, they must be quoted
  943. // (http://tools.ietf.org/html/rfc3696#section-3)
  944. //
  945. // Any excluded characters? i.e. 0x00-0x20, (, ), <, >, [, ], :, ;, @, \, comma, period, "
  946. if (preg_match('/[\\x00-\\x20\\(\\)<>\\[\\]:;@\\\\,\\."]/', $element) > 0) if ($diagnose) return self::ISEMAIL_UNESCAPEDSPECIAL; else return false; // These characters must be in a quoted string
  947. }
  948. }
  949. if ($partLength > 64) if ($diagnose) return self::ISEMAIL_LOCALTOOLONG; else return false; // Local part must be 64 characters or less
  950. // Now let's check the domain part...
  951. // The domain name can also be replaced by an IP address in square brackets
  952. // (http://tools.ietf.org/html/rfc3696#section-3)
  953. // (http://tools.ietf.org/html/rfc5321#section-4.1.3)
  954. // (http://tools.ietf.org/html/rfc4291#section-2.2)
  955. if (preg_match('/^\\[(.)+]$/', $domain) === 1) {
  956. // It's an address-literal
  957. $addressLiteral = substr($domain, 1, strlen($domain) - 2);
  958. $matchesIP = array();
  959. // Extract IPv4 part from the end of the address-literal (if there is one)
  960. if (preg_match('/\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/', $addressLiteral, $matchesIP) > 0) {
  961. $index = strrpos($addressLiteral, $matchesIP[0]);
  962. if ($index === 0) {
  963. // Nothing there except a valid IPv4 address, so...
  964. if ($diagnose) return self::ISEMAIL_VALID; else return true;
  965. } else {
  966. // Assume it's an attempt at a mixed address (IPv6 + IPv4)
  967. if ($addressLiteral[$index - 1] !== ':') if ($diagnose) return self::ISEMAIL_IPV4BADPREFIX; else return false; // Character preceding IPv4 address must be ':'
  968. if (substr($addressLiteral, 0, 5) !== 'IPv6:') if ($diagnose) return self::ISEMAIL_IPV6BADPREFIXMIXED; else return false; // RFC5321 section 4.1.3
  969. $IPv6 = substr($addressLiteral, 5, ($index ===7) ? 2 : $index - 6);
  970. $groupMax = 6;
  971. }
  972. } else {
  973. // It must be an attempt at pure IPv6
  974. if (substr($addressLiteral, 0, 5) !== 'IPv6:') if ($diagnose) return self::ISEMAIL_IPV6BADPREFIX; else return false; // RFC5321 section 4.1.3
  975. $IPv6 = substr($addressLiteral, 5);
  976. $groupMax = 8;
  977. }
  978. $groupCount = preg_match_all('/^[0-9a-fA-F]{0,4}|\\:[0-9a-fA-F]{0,4}|(.)/', $IPv6, $matchesIP);
  979. $index = strpos($IPv6,'::');
  980. if ($index === false) {
  981. // We need exactly the right number of groups
  982. if ($groupCount !== $groupMax) if ($diagnose) return self::ISEMAIL_IPV6GROUPCOUNT; else return false; // RFC5321 section 4.1.3
  983. } else {
  984. if ($index !== strrpos($IPv6,'::')) if ($diagnose) return self::ISEMAIL_IPV6DOUBLEDOUBLECOLON; else return false; // More than one '::'
  985. $groupMax = ($index === 0 || $index === (strlen($IPv6) - 2)) ? $groupMax : $groupMax - 1;
  986. if ($groupCount > $groupMax) if ($diagnose) return self::ISEMAIL_IPV6TOOMANYGROUPS; else return false; // Too many IPv6 groups in address
  987. }
  988. // Check for unmatched characters
  989. array_multisort($matchesIP[1], SORT_DESC);
  990. if ($matchesIP[1][0] !== '') if ($diagnose) return self::ISEMAIL_IPV6BADCHAR; else return false; // Illegal characters in address
  991. // It's a valid IPv6 address, so...
  992. if ($diagnose) return self::ISEMAIL_VALID; else return true;
  993. } else {
  994. // It's a domain name...
  995. // The syntax of a legal Internet host name was specified in RFC-952
  996. // One aspect of host name syntax is hereby changed: the
  997. // restriction on the first character is relaxed to allow either a
  998. // letter or a digit.
  999. // (http://tools.ietf.org/html/rfc1123#section-2.1)
  1000. //
  1001. // NB RFC 1123 updates RFC 1035, but this is not currently apparent from reading RFC 1035.
  1002. //
  1003. // Most common applications, including email and the Web, will generally not
  1004. // permit...escaped strings
  1005. // (http://tools.ietf.org/html/rfc3696#section-2)
  1006. //
  1007. // the better strategy has now become to make the "at least one period" test,
  1008. // to verify LDH conformance (including verification that the apparent TLD name
  1009. // is not all-numeric)
  1010. // (http://tools.ietf.org/html/rfc3696#section-2)
  1011. //
  1012. // Characters outside the set of alphabetic characters, digits, and hyphen MUST NOT appear in domain name
  1013. // labels for SMTP clients or servers
  1014. // (http://tools.ietf.org/html/rfc5321#section-4.1.2)
  1015. //
  1016. // RFC5321 precludes the use of a trailing dot in a domain name for SMTP purposes
  1017. // (http://tools.ietf.org/html/rfc5321#section-4.1.2)
  1018. $dotArray = /*. (array[int]string) .*/ preg_split('/\\.(?=(?:[^\\"]*\\"[^\\"]*\\")*(?![^\\"]*\\"))/m', $domain);
  1019. $partLength = 0;
  1020. $element = ''; // Since we use $element after the foreach loop let's make sure it has a value
  1021. // revision 1.13: Line above added because PHPLint now checks for Definitely Assigned Variables
  1022. if (count($dotArray) === 1) if ($diagnose) return self::ISEMAIL_TLD; else return false; // Mail host can't be a TLD (cite? What about localhost?)
  1023. foreach ($dotArray as $element) {
  1024. // Remove any leading or trailing FWS
  1025. $element = preg_replace("/^$FWS|$FWS\$/", '', $element);
  1026. $elementLength = strlen($element);
  1027. // Each dot-delimited component must be of type atext
  1028. // A zero-length element implies a period at the beginning or end of the
  1029. // local part, or two periods together. Either way it's not allowed.
  1030. if ($elementLength === 0) if ($diagnose) return self::ISEMAIL_DOMAINEMPTYELEMENT; else return false; // Dots in wrong place
  1031. // revision 1.15: Speed up the test and get rid of "unitialized string offset" notices from PHP
  1032. // Then we need to remove all valid comments (i.e. those at the start or end of the element
  1033. if ($element[0] === '(') {
  1034. $indexBrace = strpos($element, ')');
  1035. if ($indexBrace !== false) {
  1036. if (preg_match('/(?<!\\\\)[\\(\\)]/', substr($element, 1, $indexBrace - 1)) > 0) {
  1037. if ($diagnose) return self::ISEMAIL_BADCOMMENT_START; else return false; // Illegal characters in comment
  1038. }
  1039. $element = substr($element, $indexBrace + 1, $elementLength - $indexBrace - 1);
  1040. $elementLength = strlen($element);
  1041. }
  1042. }
  1043. if ($element[$elemen

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