PageRenderTime 65ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 1ms

/ezUser.php

https://bitbucket.org/dominicsayers/ezuser
PHP | 4848 lines | 3186 code | 596 blank | 1066 comment | 529 complexity | c1fa27ac6a012155e52c1ac5d417d03a MD5 | raw file

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

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

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