PageRenderTime 91ms CodeModel.GetById 24ms 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
  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[$elementLength - 1] === ')') {
  1044. $indexBrace = strrpos($element, '(');
  1045. if ($indexBrace !== false) {
  1046. if (preg_match('/(?<!\\\\)(?:[\\(\\)])/', substr($element, $indexBrace + 1, $elementLength - $indexBrace - 2)) > 0)
  1047. if ($diagnose) return self::ISEMAIL_BADCOMMENT_END; else return false; // Illegal characters in comment
  1048. $element = substr($element, 0, $indexBrace);
  1049. $elementLength = strlen($element);
  1050. }
  1051. }
  1052. // Remove any leading or trailing FWS around the element (inside any comments)
  1053. $element = preg_replace("/^$FWS|$FWS\$/", '', $element);
  1054. // What's left counts towards the maximum length for this part
  1055. if ($partLength > 0) $partLength++; // for the dot
  1056. $partLength += strlen($element);
  1057. // The DNS defines domain name syntax very generally -- a
  1058. // string of labels each containing up to 63 8-bit octets,
  1059. // separated by dots, and with a maximum total of 255
  1060. // octets.
  1061. // (http://tools.ietf.org/html/rfc1123#section-6.1.3.5)
  1062. if ($elementLength > 63) if ($diagnose) return self::ISEMAIL_DOMAINELEMENTTOOLONG; else return false; // Label must be 63 characters or less
  1063. // Any ASCII graphic (printing) character other than the
  1064. // at-sign ("@"), backslash, double quote, comma, or square brackets may
  1065. // appear without quoting. If any of that list of excluded characters
  1066. // are to appear, they must be quoted
  1067. // (http://tools.ietf.org/html/rfc3696#section-3)
  1068. //
  1069. // If the hyphen is used, it is not permitted to appear at
  1070. // either the beginning or end of a label.
  1071. // (http://tools.ietf.org/html/rfc3696#section-2)
  1072. //
  1073. // Any excluded characters? i.e. 0x00-0x20, (, ), <, >, [, ], :, ;, @, \, comma, period, "
  1074. if (preg_match('/[\\x00-\\x20\\(\\)<>\\[\\]:;@\\\\,\\."]|^-|-$/', $element) > 0) {
  1075. if ($diagnose) return self::ISEMAIL_DOMAINBADCHAR; else return false;
  1076. }
  1077. }
  1078. if ($partLength > 255) if ($diagnose) return self::ISEMAIL_DOMAINTOOLONG; else return false; // Domain part must be 255 characters or less (http://tools.ietf.org/html/rfc1123#section-6.1.3.5)
  1079. if (preg_match('/^[0-9]+$/', $element) > 0) if ($diagnose) return self::ISEMAIL_TLDNUMERIC; else return false; // TLD can't be all-numeric (http://www.apps.ietf.org/rfc/rfc3696.html#sec-2)
  1080. // Check DNS?
  1081. if ($checkDNS && function_exists('checkdnsrr')) {
  1082. if (!(checkdnsrr($domain, 'A') || checkdnsrr($domain, 'MX'))) {
  1083. if ($diagnose) return self::ISEMAIL_DOMAINNOTFOUND; else return false; // Domain doesn't actually exist
  1084. }
  1085. }
  1086. }
  1087. // Eliminate all other factors, and the one which remains must be the truth.
  1088. // (Sherlock Holmes, The Sign of Four)
  1089. if ($diagnose) return self::ISEMAIL_VALID; else return true;
  1090. }
  1091. }
  1092. // End of class ajaxUnit_common
  1093. /**
  1094. * Browser detection class for ajaxUnit
  1095. *
  1096. * @package ajaxUnit
  1097. */
  1098. class ajaxUnit_browser {
  1099. // Can be public if required
  1100. private /*.string.*/ $Agent;
  1101. public /*.string.*/ $Name;
  1102. public /*.string.*/ $Version;
  1103. public /*.void.*/ function __Construct() {
  1104. $browsers = array("firefox", "msie", "opera", "chrome", "safari",
  1105. "mozilla", "seamonkey", "konqueror", "netscape",
  1106. "gecko", "navigator", "mosaic", "lynx", "amaya",
  1107. "omniweb", "avant", "camino", "flock", "aol");
  1108. $this->Agent = $_SERVER['HTTP_USER_AGENT'];
  1109. $match = array();
  1110. foreach ($browsers as $browser) {
  1111. if (preg_match("#($browser)[/ ]?([0-9.]*)#i", $this->Agent, $match) !== 0) {
  1112. $this->Name = $match[1];
  1113. $this->Version = $match[2];
  1114. break;
  1115. }
  1116. }
  1117. }
  1118. }
  1119. // End of class ajaxUnit_browser
  1120. class ajaxUnit_cookies {
  1121. // Can be public if required
  1122. private static /*.string.*/ function get(/*.string.*/ $name) {
  1123. return (isset($_COOKIE[$name])) ? (string) $_COOKIE[$name] : "Cookie $name is not set";
  1124. }
  1125. public static /*.string.*/ function set(/*.string.*/ $name, /*.string.*/ $value, $days = '0', $path = '/', $domain = '') {
  1126. $expiry = ($days === '0') ? 0 : time() + 60 * 60 * 24 * (int) $days;
  1127. if ($domain === '')
  1128. setcookie($name, $value, $expiry, $path);
  1129. else
  1130. setcookie($name, $value, $expiry, $path, $domain);
  1131. return "Setting cookie '$name' to [$value] until " . date(DateTime::COOKIE, $expiry);
  1132. }
  1133. public static /*.string.*/ function remove(/*.string.*/ $name) {
  1134. if (!array_key_exists($name, $_COOKIE)) return "Cookie $name doesn't exist";
  1135. return self::set($name, '0', '-1');
  1136. }
  1137. public static /*.string.*/ function toTable($name = '', $tableTop = true, $tableBottom = true) {
  1138. $html = '';
  1139. if ($tableTop) $html .= "<table>\n";
  1140. if ($name === '') {
  1141. $cookieCount = count($_COOKIE);
  1142. $keys = array_keys($_COOKIE);
  1143. $html .= "<tr><td>Cookie ($cookieCount)</td><td>Value</td></tr>\n";
  1144. for ($i = 0; $i < $cookieCount; $i++) {
  1145. $name = $keys[$i];
  1146. $html .= self::toTable($name, false, false);
  1147. }
  1148. } else {
  1149. $value = self::get($name);
  1150. $html .= "<tr><td>$name</td><td>$value</td></tr>\n";
  1151. }
  1152. if ($tableBottom) $html .= "</table>\n";
  1153. return $html;
  1154. }
  1155. }
  1156. // End of class ajaxUnit_cookies
  1157. class ajaxUnit_log {
  1158. public static /*.string.*/ function format(/*.string.*/ $text, $textIndent = 4, $tag = 'p', $htmlIndent = 4) {
  1159. $marginLeft = ($textIndent === 0) ? '' : ' style="margin-left:' . $textIndent . 'em"';
  1160. $timestamp = (string) microtime(true);
  1161. if ($tag === 'p') {
  1162. $openTag = "<p$marginLeft class=\"ajaxunit-testlog\" timestamp =\"$timestamp\">";
  1163. $closeTag = '</p>';
  1164. } else {
  1165. $openTag = '';
  1166. $closeTag = '';
  1167. }
  1168. return str_pad('', $htmlIndent, "\t") . "$openTag$text$closeTag\n";
  1169. }
  1170. }
  1171. /**
  1172. * All the methods needed to interact with the file system etc.
  1173. *
  1174. * @package ajaxUnit
  1175. */
  1176. interface I_ajaxUnit_environment extends I_ajaxUnit_common {
  1177. const ACTION_ABOUT = 'about',
  1178. ACTION_CONTROL = 'control',
  1179. ACTION_CSS = 'css',
  1180. ACTION_CUSTOMURL = 'customURL',
  1181. ACTION_DUMMY = 'dummyrun',
  1182. ACTION_ICON = 'icon',
  1183. ACTION_JAVASCRIPT = 'js',
  1184. ACTION_PARSE = 'parse',
  1185. ACTION_SUITE = 'suite',
  1186. ACTION_END = 'end',
  1187. ACTION_SOURCECODE = 'source',
  1188. ACTION_LOGTIDY = 'logtidy',
  1189. TAGNAME_ADD = 'add',
  1190. TAGNAME_BROWSER = 'browser',
  1191. TAGNAME_BROWSERVERSION = 'browserversion',
  1192. TAGNAME_CHECKBOX = 'checkbox',
  1193. TAGNAME_CHECKSIZE = 'check-size',
  1194. TAGNAME_CLICK = 'click',
  1195. TAGNAME_COOKIES = 'cookies',
  1196. TAGNAME_COPY = 'copy',
  1197. TAGNAME_COUNT = 'count',
  1198. TAGNAME_CUSTOMURL = 'customurl',
  1199. TAGNAME_DELETE = 'delete',
  1200. TAGNAME_EXPECTINGCOUNT = 'expectingCount',
  1201. TAGNAME_FILE = 'file',
  1202. TAGNAME_FOLDER_BASE = 'baseFolder',
  1203. TAGNAME_FOLDER_LOGS = 'logsFolder',
  1204. //- TAGNAME_FOLDER_PACKAGE = 'packageFolder',
  1205. TAGNAME_FOLDER_TESTS = 'testsFolder',
  1206. TAGNAME_FORMFILL = 'formfill',
  1207. TAGNAME_HEADERS = 'headers',
  1208. TAGNAME_IGNORE = 'ignore',
  1209. TAGNAME_INCLUDEPATH = 'include_path',
  1210. TAGNAME_INDEX = 'index',
  1211. TAGNAME_LOCATION = 'location',
  1212. TAGNAME_LOGAPPEND = 'logAppend',
  1213. TAGNAME_OPEN = 'open',
  1214. TAGNAME_PARAMETERS = 'parameters',
  1215. TAGNAME_PARSING = 'parsing',
  1216. TAGNAME_PROJECT = 'project',
  1217. TAGNAME_POST = 'post',
  1218. TAGNAME_RADIO = 'radio',
  1219. TAGNAME_RUBRIC = 'rubric',
  1220. TAGNAME_RESET = 'reset',
  1221. TAGNAME_RESPONSECOUNT = 'responseCount',
  1222. TAGNAME_RESPONSELIST = 'responseList',
  1223. TAGNAME_RESULT = 'result',
  1224. TAGNAME_RESULTS = 'results',
  1225. TAGNAME_RESULTSNODENAME = 'resultsnodename',
  1226. TAGNAME_SESSION = 'session',
  1227. TAGNAME_SET = 'set',
  1228. TAGNAME_STATUS = 'status',
  1229. TAGNAME_STOP = 'stop',
  1230. TAGNAME_SUITE = 'suite',
  1231. TAGNAME_TEST = 'test',
  1232. TAGNAME_UID = 'uid',
  1233. TAGNAME_URL_BASE = 'baseURL',
  1234. TAGNAME_URL_LOGS = 'logsURL',
  1235. TAGNAME_URL_TESTS = 'testsURL',
  1236. ATTRNAME_DAYS = 'days',
  1237. ATTRNAME_DESTINATION = 'dest',
  1238. ATTRNAME_ID = 'id',
  1239. ATTRNAME_NAME = 'name',
  1240. ATTRNAME_SOURCE = 'src',
  1241. ATTRNAME_UPDATE = 'update',
  1242. ATTRNAME_URL = 'url',
  1243. ATTRNAME_VALUE = 'value',
  1244. STATUS_INPROGRESS = 'in progress',
  1245. STATUS_FAIL = '<span style="color:red;font-weight:bold;">FAIL!</span>',
  1246. STATUS_SUCCESS = 'success',
  1247. STATUS_FINISHED = 'finished',
  1248. STATUS_FATALERROR = '<span style="color:red;font-weight:bold;">fatal error :-(</span>',
  1249. STATUS_UNKNOWN = 'unknown',
  1250. TESTS_FOLDER = 'tests',
  1251. TESTS_EXTENSION = 'xml',
  1252. LOG_FOLDER = 'logs',
  1253. LOG_EXTENSION = 'html',
  1254. CONTEXT_FOLDER = '.context',
  1255. LOG_MAXHOURS = 12,
  1256. TEST_WAITUSECS = 50000, // uSecs to wait for response
  1257. TEST_MAXWAIT = 10000000; // Maximum waiting time in uSecs
  1258. public static /*.void.*/ function tidyLogFiles();
  1259. }
  1260. /**
  1261. * All the methods needed to interact with the file system etc.
  1262. *
  1263. * @package ajaxUnit
  1264. */
  1265. abstract class ajaxUnit_environment extends ajaxUnit_common implements I_ajaxUnit_environment {
  1266. public static /*.string.*/ function thisURL() {
  1267. return self::getURL(self::URL_MODE_PATH, 'ajaxunit.php');
  1268. }
  1269. public static /*.void.*/ function sendContent(/*.string.*/ $content, $component = '', $contentType = '') {
  1270. // Send headers first
  1271. if (headers_sent()) {
  1272. echo "<!-- headers already sent -->\n";
  1273. } else {
  1274. // $defaultType = ($component === 'container') ? "text/html" : "application/ajaxunit"; // Webkit oddity
  1275. $defaultType = "text/html";
  1276. $contentType = ($contentType === '') ? $defaultType : $contentType;
  1277. $component = ($component === '') ? 'ajaxunit' : "ajaxunit-$component";
  1278. header("Cache-Control: no-cache"); // Damn fool Internet Explorer caching feature
  1279. header("Expires: -1"); // Ditto
  1280. header("Pragma: no-cache"); // Ditto
  1281. header("Content-type: $contentType");
  1282. header("Package: ajaxUnit");
  1283. header("ajaxUnit-component: $component");
  1284. }
  1285. // Send content
  1286. echo $content;
  1287. }
  1288. protected static /*.string.*/ function htmlPageTop() {
  1289. $actionCSS = self::ACTION_CSS;
  1290. $URL = self::thisURL();
  1291. return <<<HTML
  1292. <!doctype html>
  1293. <html>
  1294. <head>
  1295. <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
  1296. <title>ajaxUnit</title>
  1297. <link type="text/css" rel="stylesheet" href="$URL?$actionCSS" title="ajaxUnit"/>
  1298. </head>
  1299. <body>
  1300. <div id="ajaxunit">
  1301. <a id="top" href="#bottom">bottom &raquo;</a>
  1302. <h1>ajaxUnit test log</h1>
  1303. HTML;
  1304. }
  1305. private static /*.string.*/ function htmlPageBottom() {
  1306. return <<<HTML
  1307. <p><a id="bottom" href="#top">&laquo; top</a></p>
  1308. <p>ajaxUnit version 0.17.30</p>
  1309. </div>
  1310. <script type="text/javascript">
  1311. function ajaxUnit_toggle_log(control, id) {
  1312. if (control.innerHTML === '+') {
  1313. control.innerHTML = '&ndash;';
  1314. document.getElementById(id).style.display = 'block';
  1315. } else {
  1316. control.innerHTML = '+';
  1317. document.getElementById(id).style.display = 'none';
  1318. }
  1319. }
  1320. document.getElementById('bottom').scrollIntoView(true);
  1321. </script>
  1322. </body>
  1323. </html>
  1324. HTML;
  1325. }
  1326. private static /*.void.*/ function makeFolder(/*.string.*/ $folder) {
  1327. if (!is_dir($folder)) if (!@mkdir($folder, 0600)) exit("Failed to create folder $folder");
  1328. }
  1329. private static /*.string.*/ function getContextFilename() {
  1330. self::makeFolder(self::CONTEXT_FOLDER);
  1331. return self::CONTEXT_FOLDER . DIRECTORY_SEPARATOR . 'ajaxUnit_' . (string) str_replace(':', '_' , $_SERVER['REMOTE_ADDR']); // str_replace is because of IPv6
  1332. }
  1333. protected static /*.array[string]string.*/ function setTestContext(/*.mixed.*/ $newContext, $value = '') {
  1334. $filename = self::getContextFilename();
  1335. $context = /*.(array[string]string).*/ array();
  1336. if (is_file($filename)) {
  1337. $handle = @fopen($filename, 'r+b'); // Lock the file
  1338. $serial = @file_get_contents($filename);
  1339. $context = /*.(array[string]string).*/ unserialize($serial);
  1340. rewind($handle);
  1341. ftruncate($handle, 0);
  1342. } else {
  1343. $handle = @fopen($filename, 'wb');
  1344. }
  1345. if (is_array($newContext)) {
  1346. $context = /*.(array[string]string).*/ array_merge($context, /*.(array[string]string).*/ $newContext);
  1347. } else {
  1348. $context[(string) $newContext] = $value;
  1349. }
  1350. @fwrite($handle, serialize($context));
  1351. fclose($handle);
  1352. return $context;
  1353. }
  1354. protected static /*.mixed.*/ function getTestContext($key = '') {
  1355. $filename = self::getContextFilename();
  1356. if (!is_file($filename)) {
  1357. $context = /*.(array[]string).*/ array();
  1358. } else {
  1359. $serial = @file_get_contents($filename);
  1360. $context = /*.(array[]string).*/ unserialize($serial);
  1361. }
  1362. if ($key === '') {
  1363. return $context;
  1364. } else if (array_key_exists($key, $context)) {
  1365. return $context[$key];
  1366. } else {
  1367. return '';
  1368. }
  1369. }
  1370. protected static /*.array[string]string.*/ function setInitialContext(DOMElement $test, /*.int.*/ $testIndex) {
  1371. // Any browser-specific results in the test data?
  1372. $browser = new ajaxUnit_browser();
  1373. $resultsNodeName = self::TAGNAME_RESULTS . '-' . $browser->Name . '-' . $browser->Version;
  1374. $resultsNodeList = $test->getElementsByTagName($resultsNodeName); // Browser and version-specific results
  1375. if ($resultsNodeList->length === 0) {
  1376. $resultsNodeName = self::TAGNAME_RESULTS . '-' . $browser->Name;
  1377. $resultsNodeList = $test->getElementsByTagName($resultsNodeName); // Just browser-specific results
  1378. if ($resultsNodeList->length === 0) {
  1379. // General (not browser-specific) results
  1380. $resultsNodeName = self::TAGNAME_RESULTS;
  1381. $resultsNodeList = $test->getElementsByTagName($resultsNodeName);
  1382. if ($resultsNodeList->length === 0) {
  1383. // No results defined for this test
  1384. $resultsElement = new DOMElement($resultsNodeName);
  1385. $resultsNode = $test->appendChild($resultsElement);
  1386. $resultsNodeName = '';
  1387. } else {
  1388. $resultsNode = $resultsNodeList->item(0);
  1389. }
  1390. } else {
  1391. $resultsNode = $resultsNodeList->item(0);
  1392. }
  1393. } else {
  1394. $resultsNode = $resultsNodeList->item(0);
  1395. }
  1396. // Build a string containing a list of expected responses (1234...n) according to <results> child nodes
  1397. $responseList = '';
  1398. if ($resultsNodeName === '') {
  1399. $expected = 0;
  1400. } else {
  1401. /*.object.*/ $resultsObject = $resultsNode; // PHPLint-compliant typecasting
  1402. $resultsElement = /*.(DOMElement).*/ $resultsObject; // PHPLint-compliant typecasting
  1403. $resultsList = $resultsElement->getElementsByTagName(self::TAGNAME_RESULT); // Get the expected results
  1404. $expected = $resultsList->length;
  1405. for ($i = 0; $i < $expected; $i++) $responseList .= (string) $i;
  1406. }
  1407. // Set context data
  1408. $context = /*.(array[string]string).*/ array();
  1409. $context[self::TAGNAME_INDEX] = (string) $testIndex;
  1410. $context[self::TAGNAME_RESPONSECOUNT] = '0';
  1411. $context[self::TAGNAME_PARSING] = '0';
  1412. $context[self::TAGNAME_RESPONSELIST] = $responseList;
  1413. $context[self::TAGNAME_RESULTSNODENAME] = $resultsNodeName;
  1414. $context[self::TAGNAME_EXPECTINGCOUNT] = (string) $expected;
  1415. self::setTestContext($context);
  1416. return $context;
  1417. }
  1418. private static /*.string.*/ function getSuiteFilename(/*.string.*/ $suite) {
  1419. $folder = (string) self::getTestContext(self::TAGNAME_FOLDER_TESTS);
  1420. self::makeFolder($folder);
  1421. return $folder.DIRECTORY_SEPARATOR.$suite.'.'.self::TESTS_EXTENSION;
  1422. }
  1423. protected static /*.DOMDocument.*/ function getDOMDocument(/*.string.*/ $suite) {
  1424. $filename = self::getSuiteFilename($suite);
  1425. $document = new DOMDocument();
  1426. $document->documentURI = $filename;
  1427. @$document->load($filename);
  1428. $document->xinclude();
  1429. return $document;
  1430. }
  1431. protected static /*.void.*/ function updateTestSuite(/*.string.*/ $suite, DOMDocument $document) {
  1432. $document->save(self::getSuiteFilename($suite));
  1433. }
  1434. protected static /*.DOMNodeList.*/ function getTestList(DOMDocument $document) {
  1435. return $document->getElementsByTagName(self::TAGNAME_TEST);
  1436. }
  1437. private static /*.string.*/ function getLogFilename($basename = false) {
  1438. $filename = 'ajaxUnit_' . (string) self::getTestContext(self::TAGNAME_UID) .'.'. self::LOG_EXTENSION;
  1439. if ($basename) {
  1440. return $filename;
  1441. } else {
  1442. $folder = (string) self::getTestContext(self::TAGNAME_FOLDER_LOGS);
  1443. self::makeFolder($folder);
  1444. return $folder.DIRECTORY_SEPARATOR.$filename;
  1445. }
  1446. }
  1447. private static /*.void.*/ function appendLogEntry(/*.string.*/ $html, $dummyRun = false) {
  1448. if ($dummyRun) {
  1449. echo $html;
  1450. } else {
  1451. $handle = @fopen(self::getLogFilename(), 'ab');
  1452. fwrite($handle, $html);
  1453. fclose($handle);
  1454. }
  1455. }
  1456. protected static /*.void.*/ function appendLog(/*.string.*/ $text, $dummyRun = false, $textIndent = 4, $tag = 'p', $htmlIndent = 4) {
  1457. self::appendLogEntry(ajaxUnit_log::format($text, $textIndent, $tag, $htmlIndent), $dummyRun);
  1458. }
  1459. protected static /*.void.*/ function logTestContext($dummyRun = false) {
  1460. $context = /*.(array[string]string).*/ self::getTestContext();
  1461. self::appendLog("<span class=\"ajaxunit-testlog\" onclick=\"ajaxUnit_toggle_log(this, 'ajaxunit-parameters')\">+</span> Global test parameters", $dummyRun, 0, 'p', 3);
  1462. self::appendLog("<div class=\"ajaxunit-testlog\" id=\"ajaxunit-parameters\">", $dummyRun, 0, '', 3);
  1463. foreach ($context as $key => $value) self::appendLog("$key = " . htmlspecialchars(substr($value, 0, 64)), $dummyRun);
  1464. self::appendLog('</div>', $dummyRun, 0, '', 3);
  1465. self::appendLog('<hr />', $dummyRun, 0, '', 3);
  1466. }
  1467. // private static /*.void.*/ function logTestScript(DOMDocument $document, $dummyRun = false) {
  1468. // self::appendLog("<span class=\"ajaxunit-testlog\" onclick=\"ajaxUnit_toggle_log(this, 'ajaxunit-script')\">+</span> Test script", $dummyRun, 0, 'p', 3);
  1469. // self::appendLog("<div class=\"ajaxunit-testlog\" id=\"ajaxunit-script\">", $dummyRun, 0, '', 3);
  1470. // self::appendLog('<pre>' . htmlspecialchars($document->saveXML()) . '</pre>', $dummyRun);
  1471. // self::appendLog('</div>', $dummyRun, 0, '', 3);
  1472. // self::appendLog('<hr />', $dummyRun, 0, '', 3);
  1473. // }
  1474. protected static /*.void.*/ function logResult(/*.boolean.*/ $success, $dummyRun = false) {
  1475. self::appendLog('</div>', $dummyRun, 0, '', 3);
  1476. if ($success) {
  1477. $status = self::STATUS_SUCCESS;
  1478. // Increment successful test counter
  1479. $count = (int) self::getTestContext(self::TAGNAME_COUNT);
  1480. $count++;
  1481. self::setTestContext(self::TAGNAME_COUNT, (string) $count);
  1482. self::setTestContext(self::TAGNAME_STATUS, $status);
  1483. } else {
  1484. $status = self::STATUS_FAIL;
  1485. }
  1486. self::appendLog("Result: $status", $dummyRun, 0, 'p', 3);
  1487. self::appendLog('<hr />', $dummyRun, 0, '', 3);
  1488. }
  1489. private static /*.string.*/ function getLogLink() {
  1490. return (string) self::getTestContext(self::TAGNAME_URL_LOGS) . '/' . self::getLogFilename(true);
  1491. }
  1492. private static /*.void.*/ function sendLogLink($dummyRun = false) {
  1493. if ($dummyRun) return;
  1494. $attrName = self::ATTRNAME_URL;
  1495. $URL = self::getLogLink();
  1496. $xml = "<test><open $attrName=\"$URL\" /></test>";
  1497. self::sendContent($xml, self::ACTION_PARSE, 'text/xml');
  1498. }
  1499. protected static /*.void.*/ function tidyUp($dummyRun = false) {
  1500. $count = ltrim((string) self::getTestContext(self::TAGNAME_COUNT));
  1501. self::appendLog("$count tests successfully completed", $dummyRun, 0, 'p', 3);
  1502. self::appendLog(self::htmlPageBottom(), $dummyRun, 0, '', 0);
  1503. self::sendLogLink($dummyRun);
  1504. }
  1505. protected static /*.string.*/ function substituteParameters(/*.string.*/ $text) {
  1506. $variables = /*.(array[string]string).*/ self::getTestContext();
  1507. $variables['URL'] = self::thisURL();
  1508. extract($variables);
  1509. return (string) eval('return "' . (string) str_replace('"', '\\"', $text) . '";');
  1510. }
  1511. /**
  1512. * Delete all old log files. "Old" meaning older than LOG_MAXHOURS
  1513. */
  1514. public static /*.void.*/ function tidyLogFiles() {
  1515. $folder = (string) self::getTestContext(self::TAGNAME_FOLDER_LOGS);
  1516. $extension = self::LOG_EXTENSION;
  1517. foreach (glob($folder.DIRECTORY_SEPARATOR."*.$extension") as $filename) {
  1518. if (is_file($filename)) {
  1519. $ageInHours = (int) floor((time() - filemtime($filename)) / (60 * 60));
  1520. if ($ageInHours > self::LOG_MAXHOURS) @unlink($filename);
  1521. }
  1522. }
  1523. }
  1524. protected static /*.boolean.*/ function terminate($status = self::STATUS_UNKNOWN, $message = '', $success = false, $dummyRun = false) {
  1525. if ($message !== '') self::appendLog($message, $dummyRun);
  1526. if (!$success) self::logResult($success, $dummyRun);
  1527. self::setTestContext(self::TAGNAME_STATUS, $status);
  1528. self::setTestContext(self::TAGNAME_PARSING, '0');
  1529. self::tidyUp($dummyRun);
  1530. return $success;
  1531. }
  1532. }
  1533. // End of class ajaxUnit_environment
  1534. /**
  1535. * @package Text_Diff
  1536. * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
  1537. */
  1538. abstract class ajaxUnit_Text_Diff_Op {
  1539. public /*.array[int]string.*/ $originalArray;
  1540. public /*.array[int]string.*/ $finalArray;
  1541. public /*.int.*/ function norig()
  1542. {
  1543. return (isset($this->originalArray)) ? count($this->originalArray) : 0;
  1544. }
  1545. public /*.int.*/ function nfinal()
  1546. {
  1547. return (isset($this->finalArray)) ? count($this->finalArray) : 0;
  1548. }
  1549. public /*.object.*/ function reverse()
  1550. {
  1551. return $this;
  1552. }
  1553. }
  1554. /**
  1555. * @package Text_Diff
  1556. * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
  1557. */
  1558. class ajaxUnit_Text_Diff_Op_copy extends ajaxUnit_Text_Diff_Op {
  1559. public /*.void.*/ function __construct(/*.array[int]string.*/ $originalArray)
  1560. {
  1561. $this->originalArray = $originalArray;
  1562. $this->finalArray = $originalArray;
  1563. }
  1564. public /*.ajaxUnit_Text_Diff_Op_copy.*/ function reverse()
  1565. {
  1566. $reverse = new ajaxUnit_Text_Diff_Op_copy($this->finalArray);
  1567. return $reverse;
  1568. }
  1569. }
  1570. /**
  1571. * @package Text_Diff
  1572. * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
  1573. */
  1574. /*.
  1575. forward class ajaxUnit_Text_Diff_Op_delete {
  1576. public void function __construct(array[int]string $lines);
  1577. }
  1578. .*/
  1579. class ajaxUnit_Text_Diff_Op_add extends ajaxUnit_Text_Diff_Op {
  1580. public /*.void.*/ function __construct(/*.array[int]string.*/ $lines)
  1581. {
  1582. $this->finalArray = $lines;
  1583. unset($this->originalArray);
  1584. }
  1585. public /*.ajaxUnit_Text_Diff_Op_delete.*/ function reverse()
  1586. {
  1587. $reverse = new ajaxUnit_Text_Diff_Op_delete($this->finalArray);
  1588. return $reverse;
  1589. }
  1590. }
  1591. /**
  1592. * @package Text_Diff
  1593. * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
  1594. */
  1595. class ajaxUnit_Text_Diff_Op_delete extends ajaxUnit_Text_Diff_Op {
  1596. public /*.void.*/ function __construct(/*.array[int]string.*/ $lines)
  1597. {
  1598. $this->originalArray = $lines;
  1599. unset($this->finalArray);
  1600. }
  1601. public /*.ajaxUnit_Text_Diff_Op_add.*/ function reverse()
  1602. {
  1603. $reverse = new ajaxUnit_Text_Diff_Op_add($this->originalArray);
  1604. return $reverse;
  1605. }
  1606. }
  1607. /**
  1608. * @package Text_Diff
  1609. * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
  1610. */
  1611. class ajaxUnit_Text_Diff_Op_change extends ajaxUnit_Text_Diff_Op {
  1612. public /*.void.*/ function __construct(/*.array[int]string.*/ $originalArray, /*.array[int]string.*/ $finalArray)
  1613. {
  1614. $this->originalArray = $originalArray;
  1615. $this->finalArray = $finalArray;
  1616. }
  1617. public /*.ajaxUnit_Text_Diff_Op_change.*/ function reverse()
  1618. {
  1619. $reverse = new ajaxUnit_Text_Diff_Op_change($this->finalArray, $this->originalArray);
  1620. return $reverse;
  1621. }
  1622. }
  1623. /**
  1624. * Class used internally by ajaxUnit_Text_Diff to actually compute the diffs.
  1625. *
  1626. * This class is implemented using native PHP code.
  1627. *
  1628. * The algorithm used here is mostly lifted from the perl module
  1629. * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
  1630. * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
  1631. *
  1632. * More ideas are taken from: http://www.ics.uci.edu/~eppstein/161/960229.html
  1633. *
  1634. * Some ideas (and a bit of code) are taken from analyze.c, of GNU
  1635. * diffutils-2.7, which can be found at:
  1636. * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
  1637. *
  1638. * Some ideas (subdivision by NCHUNKS > 2, and some optimizations) are from
  1639. * Geoffrey T. Dairiki <dairiki@dairiki.org>. The original PHP version of this
  1640. * code was written by him, and is used/adapted with his permission.
  1641. *
  1642. * $Horde: framework/Text_Diff/Diff/Engine/native.php,v 1.7.2.5 2009/01/06 15:23:41 jan Exp $
  1643. *
  1644. * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
  1645. *
  1646. * See the enclosed file COPYING for license information (LGPL). If you did
  1647. * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
  1648. *
  1649. * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
  1650. * @package Text_Diff
  1651. */
  1652. class ajaxUnit_Text_Diff_Engine_native {
  1653. private /*.array[int]boolean.*/ $xchanged;
  1654. private /*.array[int]boolean.*/ $ychanged;
  1655. private /*.array[int]string.*/ $xv;
  1656. private /*.array[int]string.*/ $yv;
  1657. private /*.array[int]int.*/ $xind;
  1658. private /*.array[int]int.*/ $yind;
  1659. private /*.array[int]int.*/ $seq;
  1660. private /*.array[int]int.*/ $in_seq;
  1661. private /*.int.*/ $lcs = 0;
  1662. private /*.int.*/ function _lcsPos(/*.int.*/ $ypos)
  1663. {
  1664. $end = $this->lcs;
  1665. if ($end === 0 || $ypos > $this->seq[$end]) {
  1666. $this->seq[++$this->lcs] = $ypos;
  1667. $this->in_seq[$ypos] = 1;
  1668. return $this->lcs;
  1669. }
  1670. $beg = 1;
  1671. while ($beg < $end) {
  1672. $mid = (int)(($beg + $end) / 2);
  1673. if ($ypos > $this->seq[$mid]) {
  1674. $beg = $mid + 1;
  1675. } else {
  1676. $end = $mid;
  1677. }
  1678. }
  1679. assert($ypos != $this->seq[$end]);
  1680. $this->in_seq[$this->seq[$end]] = 0;
  1681. $this->seq[$end] = $ypos;
  1682. $this->in_seq[$ypos] = 1;
  1683. return $end;
  1684. }
  1685. /**
  1686. * Divides the Largest Common Subsequence (LCS) of the sequences (XOFF,
  1687. * XLIM) and (YOFF, YLIM) into NCHUNKS approximately equally sized
  1688. * segments.
  1689. *
  1690. * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an array of
  1691. * NCHUNKS+1 (X, Y) indexes giving the diving points between sub
  1692. * sequences. The first sub-sequence is contained in (X0, X1), (Y0, Y1),
  1693. * the second in (X1, X2), (Y1, Y2) and so on. Note that (X0, Y0) ==
  1694. * (XOFF, YOFF) and (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
  1695. *
  1696. * This function assumes that the first lines of the specified portions of
  1697. * the two files do not match, and likewise that the last lines do not
  1698. * match. The caller must trim matching lines from the beginning and end
  1699. * of the portions it is going to specify.
  1700. */
  1701. private /*.array.*/ function _diag (/*.int.*/ $xoff, /*.int.*/ $xlim, /*.int.*/ $yoff, /*.int.*/ $ylim, /*.int.*/ $nchunks)
  1702. {
  1703. $flip = false;
  1704. $ymatches = /*.(array[string][int]int).*/ array();
  1705. if ($xlim - $xoff > $ylim - $yoff) {
  1706. /* Things seems faster (I'm not sure I understand why) when the
  1707. * shortest sequence is in X. */
  1708. $flip = true;
  1709. list ($xoff, $xlim, $yoff, $ylim)
  1710. = array($yoff, $ylim, $xoff, $xlim);
  1711. }
  1712. if ($flip) {
  1713. for ($i = $ylim - 1; $i >= $yoff; $i--) {
  1714. $ymatches[$this->xv[$i]][] = $i;
  1715. }
  1716. } else {
  1717. for ($i = $ylim - 1; $i >= $yoff; $i--) {
  1718. $ymatches[$this->yv[$i]][] = $i;
  1719. }
  1720. }
  1721. $this->lcs = 0;
  1722. $this->seq[0] = $yoff - 1;
  1723. $this->in_seq = /*.(array[int]int).*/ array();
  1724. $ymids[0] = array();
  1725. $numer = $xlim - $xoff + $nchunks - 1;
  1726. $x = $xoff;
  1727. for ($chunk = 0; $chunk < $nchunks; $chunk++) {
  1728. if ($chunk > 0) {
  1729. for ($i = 0; $i <= $this->lcs; $i++) {
  1730. $ymids[$i][$chunk - 1] = $this->seq[$i];
  1731. }
  1732. }
  1733. $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $chunk) / $nchunks);
  1734. for (; $x < $x1; $x++) {
  1735. $line = $flip ? $this->yv[$x] : $this->xv[$x];
  1736. if (empty($ymatches[$line])) continue;
  1737. $matches = $ymatches[$line];
  1738. reset($matches);
  1739. $k = 0;
  1740. do {
  1741. $next = each($matches);
  1742. if (is_bool($next)) break;
  1743. $y = 0;
  1744. list(, $y) = $next;
  1745. if (empty($this->in_seq[$y])) {
  1746. $k = $this->_lcsPos($y);
  1747. assert($k > 0);
  1748. $ymids[$k] = $ymids[$k - 1];
  1749. break;
  1750. }
  1751. } while (true);
  1752. do {
  1753. $next = each($matches);
  1754. if (is_bool($next)) break;
  1755. $y = 0;
  1756. list(, $y) = $next;
  1757. if ($y > $this->seq[$k - 1]) {
  1758. assert($y <= $this->seq[$k]);
  1759. /* Optimization: this is a common case: next match is
  1760. * just replacing previous match. */
  1761. $this->in_seq[$this->seq[$k]] = 0;
  1762. $this->seq[$k] = $y;
  1763. $this->in_seq[$y] = 1;
  1764. } elseif (empty($this->in_seq[$y])) {
  1765. $k = $this->_lcsPos($y);
  1766. assert($k > 0);
  1767. $ymids[$k] = $ymids[$k - 1];
  1768. }
  1769. } while (true);
  1770. }
  1771. }
  1772. $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
  1773. $ymid = $ymids[$this->lcs];
  1774. for ($n = 0; $n < $nchunks - 1; $n++) {
  1775. $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
  1776. $y1 = $ymid[$n] + 1;
  1777. $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
  1778. }
  1779. $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
  1780. return array($this->lcs, $seps);
  1781. }
  1782. /**
  1783. * Finds LCS of two sequences.
  1784. *
  1785. * The results are recorded in the vectors $this->{x,y}changed[], by
  1786. * storing a 1 in the element for each line that is an insertion or
  1787. * deletion (ie. is not in the LCS).
  1788. *
  1789. * The subsequence of file 0 is (XOFF, XLIM) and likewise for file 1.
  1790. *
  1791. * Note that XLIM, YLIM are exclusive bounds. All line numbers are
  1792. * origin-0 and discarded lines are not counted.
  1793. */
  1794. private /*.void.*/ function _compareseq (/*.int.*/ $xoff, /*.int.*/ $xlim, /*.int.*/ $yoff, /*.int.*/ $ylim)
  1795. {
  1796. /* Slide down the bottom initial diagonal. */
  1797. while ($xoff < $xlim && $yoff < $ylim
  1798. && $this->xv[$xoff] === $this->yv[$yoff]) {
  1799. ++$xoff;
  1800. ++$yoff;
  1801. }
  1802. /* Slide up the top initial diagonal. */
  1803. while ($xlim > $xoff && $ylim > $yoff
  1804. && $this->xv[$xlim - 1] === $this->yv[$ylim - 1]) {
  1805. --$xlim;
  1806. --$ylim;
  1807. }
  1808. if ($xoff == $xlim || $yoff == $ylim) {
  1809. $lcs = 0;
  1810. $seps = /*.(array[int][int]int).*/ array();
  1811. } else {
  1812. /* This is ad hoc but seems to work well. $nchunks =
  1813. * sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); $nchunks =
  1814. * max(2,min(8,(int)$nchunks)); */
  1815. $nchunks = (int) min(7, $xlim - $xoff, $ylim - $yoff) + 1;
  1816. list($lcs, $seps) = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
  1817. }
  1818. if ($lcs == 0) {
  1819. /* X and Y sequences have no common subsequence: mark all
  1820. * changed. */
  1821. while ($yoff < $ylim) {
  1822. $this->ychanged[$this->yind[$yoff++]] = true;
  1823. }
  1824. while ($xoff < $xlim) {
  1825. $this->xchanged[$this->xind[$xoff++]] = true;
  1826. }
  1827. } else {
  1828. /* Use the partitions to split this problem into subproblems. */
  1829. reset($seps);
  1830. $pt1 = $seps[0];
  1831. do {
  1832. $next = next($seps);
  1833. if (is_bool($next)) break;
  1834. $pt2 = /*.(array[int]int).*/ $next;
  1835. $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
  1836. $pt1 = $pt2;
  1837. } while (true);
  1838. }
  1839. }
  1840. /**
  1841. * Adjusts inserts/deletes of identical lines to join changes as much as
  1842. * possible.
  1843. *
  1844. * We do something when a run of changed lines include a line at one end
  1845. * and has an excluded, identical line at the other. We are free to
  1846. * choose which identical line is included. `compareseq' usually chooses
  1847. * the one at the beginning, but usually it is cleaner to consider the
  1848. * following identical line to be the "change".
  1849. *
  1850. * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
  1851. */
  1852. private /*.void.*/ function _shiftBoundaries(/*.array[int]string.*/ $lines, /*.array[int]boolean.*/ &$changed, /*.array[int]boolean.*/ $other_changed)
  1853. {
  1854. $i = 0;
  1855. $j = 0;
  1856. assert('count($lines) == count($changed)');
  1857. $len = count($lines);
  1858. $other_len = count($other_changed);
  1859. while (true) {
  1860. /* Scan forward to find the beginning of another run of
  1861. * changes. Also keep track of the corresponding point in the
  1862. * other file.
  1863. *
  1864. * Throughout this code, $i and $j are adjusted together so that
  1865. * the first $i elements of $changed and the first $j elements of
  1866. * $other_changed both contain the same number of zeros (unchanged
  1867. * lines).
  1868. *
  1869. * Furthermore, $j is always kept so that $j == $other_len or
  1870. * $other_changed[$j] == false. */
  1871. while ($j < $other_len && $other_changed[$j]) {
  1872. $j++;
  1873. }
  1874. while ($i < $len && ! $changed[$i]) {
  1875. assert('$j < $other_len && ! $other_changed[$j]');
  1876. $i++; $j++;
  1877. while ($j < $other_len && $other_changed[$j]) {
  1878. $j++;
  1879. }
  1880. }
  1881. if ($i == $len) {
  1882. break;
  1883. }
  1884. $start = $i;
  1885. /* Find the end of this run of changes. */
  1886. while (++$i < $len && $changed[$i]) {
  1887. continue;
  1888. }
  1889. do {
  1890. /* Record the length of this run of changes, so that we can
  1891. * later determine whether the run has grown. */
  1892. $runlength = $i - $start;
  1893. /* Move the changed region back, so long as the previous
  1894. * unchanged line matches the last changed one. This merges
  1895. * with previous changed regions. */
  1896. while ($start > 0 && $lines[$start - 1] === $lines[$i - 1]) {
  1897. $changed[--$start] = true;
  1898. $changed[--$i] = false;
  1899. while ($start > 0 && $changed[$start - 1]) {
  1900. $start--;
  1901. }
  1902. assert('$j > 0');
  1903. while ($other_changed[--$j]) {
  1904. continue;
  1905. }
  1906. assert('$j >= 0 && !$other_changed[$j]');
  1907. }
  1908. /* Set CORRESPONDING to the end of the changed run, at the
  1909. * last point where it corresponds to a changed run in the
  1910. * other file. CORRESPONDING == LEN means no such point has
  1911. * been found. */
  1912. $corresponding = $j < $other_len ? $i : $len;
  1913. /* Move the changed region forward, so long as the first
  1914. * changed line matches the following unchanged one. This
  1915. * merges with following changed regions. Do this second, so
  1916. * that if there are no merges, the changed region is moved
  1917. * forward as far as possible. */
  1918. while ($i < $len && $lines[$start] === $lines[$i]) {
  1919. $changed[$start++] = false;
  1920. $changed[$i++] = true;
  1921. while ($i < $len && $changed[$i]) {
  1922. $i++;
  1923. }
  1924. assert('$j < $other_len && ! $other_changed[$j]');
  1925. $j++;
  1926. if ($j < $other_len && $other_changed[$j]) {
  1927. $corresponding = $i;
  1928. while ($j < $other_len && $other_changed[$j]) {
  1929. $j++;
  1930. }
  1931. }
  1932. }
  1933. } while ($runlength != $i - $start);
  1934. /* If possible, move the fully-merged run of changes back to a
  1935. * corresponding run in the other file. */
  1936. while ($corresponding < $i) {
  1937. $changed[--$start] = true;
  1938. $changed[--$i] = false;
  1939. assert('$j > 0');
  1940. while ($other_changed[--$j]) {
  1941. continue;
  1942. }
  1943. assert('$j >= 0 && !$other_changed[$j]');
  1944. }
  1945. }
  1946. }
  1947. public /*.array.*/ function diff(/*.array[int]string.*/ $from_lines, /*.array[int]string.*/ $to_lines)
  1948. {
  1949. array_walk($from_lines, array('ajaxUnit_Text_Diff', 'trimNewlines'));
  1950. array_walk($to_lines, array('ajaxUnit_Text_Diff', 'trimNewlines'));
  1951. $n_from = count($from_lines);
  1952. $n_to = count($to_lines);
  1953. $this->xchanged = $this->ychanged = /*.(array[int]boolean).*/ array();
  1954. $this->xv = $this->yv = /*.(array[int]string).*/ array();
  1955. $this->xind = $this->yind = /*.(array[int]int).*/ array();
  1956. $xhash = $yhash = /*.(array[string]int).*/ array();
  1957. unset($this->seq);
  1958. unset($this->in_seq);
  1959. unset($this->lcs);
  1960. // Skip leading common lines.
  1961. for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
  1962. if ($from_lines[$skip] !== $to_lines[$skip]) {
  1963. break;
  1964. }
  1965. $this->xchanged[$skip] = $this->ychanged[$skip] = false;
  1966. }
  1967. // Skip trailing common lines.
  1968. $xi = $n_from; $yi = $n_to;
  1969. for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
  1970. if ($from_lines[$xi] !== $to_lines[$yi]) {
  1971. break;
  1972. }
  1973. $this->xchanged[$xi] = $this->ychanged[$yi] = false;
  1974. }
  1975. // Ignore lines which do not exist in both files.
  1976. for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
  1977. $xhash[$from_lines[$xi]] = 1;
  1978. }
  1979. for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
  1980. $line = $to_lines[$yi];
  1981. if (($this->ychanged[$yi] = empty($xhash[$line]))) {
  1982. continue;
  1983. }
  1984. $yhash[$line] = 1;
  1985. $this->yv[] = $line;
  1986. $this->yind[] = $yi;
  1987. }
  1988. for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
  1989. $line = $from_lines[$xi];
  1990. if (($this->xchanged[$xi] = empty($yhash[$line]))) {
  1991. continue;
  1992. }
  1993. $this->xv[] = $line;
  1994. $this->xind[] = $xi;
  1995. }
  1996. // Find the LCS.
  1997. $this->_compareseq(0, count($this->xv), 0, count($this->yv));
  1998. // Merge edits when possible.
  1999. $this->_shiftBoundaries($from_lines, $this->xchanged, $this->ychanged);
  2000. $this->_shiftBoundaries($to_lines, $this->ychanged, $this->xchanged);
  2001. // Compute the edit operations.
  2002. $edits = /*.(array[int]object).*/ array();
  2003. $xi = $yi = 0;
  2004. while ($xi < $n_from || $yi < $n_to) {
  2005. assert($yi < $n_to || $this->xchanged[$xi]);
  2006. assert($xi < $n_from || $this->ychanged[$yi]);
  2007. // Skip matching "snake".
  2008. $copy = /*.(array[int]string).*/ array();
  2009. while ($xi < $n_from && $yi < $n_to
  2010. && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
  2011. $copy[] = $from_lines[$xi++];
  2012. ++$yi;
  2013. }
  2014. if (count($copy) !== 0) {
  2015. $edits[] = new ajaxUnit_Text_Diff_Op_copy($copy);
  2016. }
  2017. // Find deletes & adds.
  2018. $delete = /*.(array[int]string).*/ array();
  2019. while ($xi < $n_from && $this->xchanged[$xi]) {
  2020. $delete[] = $from_lines[$xi++];
  2021. }
  2022. $add = /*.(array[int]string).*/ array();
  2023. while ($yi < $n_to && $this->ychanged[$yi]) {
  2024. $add[] = $to_lines[$yi++];
  2025. }
  2026. if ((count($delete) !== 0) && (count($add) !== 0)) {
  2027. $edits[] = new ajaxUnit_Text_Diff_Op_change($delete, $add);
  2028. } elseif (count($delete) !== 0) {
  2029. $edits[] = new ajaxUnit_Text_Diff_Op_delete($delete);
  2030. } elseif (count($add) !== 0) {
  2031. $edits[] = new ajaxUnit_Text_Diff_Op_add($add);
  2032. }
  2033. }
  2034. return $edits;
  2035. }
  2036. }
  2037. /**
  2038. * General API for generating and formatting diffs - the differences between
  2039. * two sequences of strings.
  2040. *
  2041. * The original PHP version of this code was written by Geoffrey T. Dairiki
  2042. * <dairiki@dairiki.org>, and is used/adapted with his permission.
  2043. *
  2044. * $Horde: framework/Text_Diff/Diff.php,v 1.11.2.12 2009/01/06 15:23:41 jan Exp $
  2045. *
  2046. * Copyright 2004 Geoffrey T. Dairiki <dairiki@dairiki.org>
  2047. * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
  2048. *
  2049. * See the enclosed file COPYING for license information (LGPL). If you did
  2050. * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
  2051. *
  2052. * @package Text_Diff
  2053. * @author Geoffrey T. Dairiki <dairiki@dairiki.org>
  2054. */
  2055. class ajaxUnit_Text_Diff {
  2056. /**
  2057. * Array of changes.
  2058. *
  2059. * @var array[int]object
  2060. */
  2061. private $_edits;
  2062. /**
  2063. * Computes diffs between sequences of strings.
  2064. */
  2065. public /*.void.*/ function __construct(/*.array[int]string.*/ $array1, /*.array[int]string.*/ $array2)
  2066. {
  2067. $params = array($array1, $array2);
  2068. $diff_engine = new ajaxUnit_Text_Diff_Engine_native();
  2069. $this->_edits = /*.(array[int]object).*/ call_user_func_array(array($diff_engine, 'diff'), $params);
  2070. }
  2071. /**
  2072. * Returns the array of differences.
  2073. */
  2074. public /*.array[int]object.*/ function getDiff()
  2075. {
  2076. return $this->_edits;
  2077. }
  2078. /**
  2079. * Computes a reversed diff.
  2080. *
  2081. * Example:
  2082. * <code>
  2083. * $diff = new Text_Diff($lines1, $lines2);
  2084. * $rev = $diff->reverse();
  2085. * </code>
  2086. *
  2087. * @return ajaxUnit_Text_Diff A Diff object representing the inverse of the
  2088. * original diff. Note that we purposely don't return a
  2089. * reference here, since this essentially is a clone()
  2090. * method.
  2091. */
  2092. private function reverse()
  2093. {
  2094. $rev = ((boolean) version_compare(zend_version(), '2', '>')) ? clone($this) : $this;
  2095. $rev->_edits = /*.(array[int]object).*/ array();
  2096. foreach ($this->_edits as $item) {
  2097. $edit = /*.(ajaxUnit_Text_Diff_Op).*/ $item;
  2098. $rev->_edits[] = $edit->reverse();
  2099. }
  2100. return $rev;
  2101. }
  2102. /**
  2103. * Computes the length of the Longest Common Subsequence (LCS).
  2104. *
  2105. * This is mostly for diagnostic purposes.
  2106. *
  2107. * @return integer The length of the LCS.
  2108. */
  2109. function lcs()
  2110. {
  2111. $lcs = 0;
  2112. foreach ($this->_edits as $item) {
  2113. $edit = /*.(ajaxUnit_Text_Diff_Op).*/ $item;
  2114. if (is_a($edit, 'ajaxUnit_Text_Diff_Op_copy')) {
  2115. $lcs += count($edit->originalArray);
  2116. }
  2117. }
  2118. return $lcs;
  2119. }
  2120. /**
  2121. * Gets the original set of lines.
  2122. *
  2123. * This reconstructs the $from_lines parameter passed to the constructor.
  2124. *
  2125. * @return array The original sequence of strings.
  2126. */
  2127. private function getOriginal()
  2128. {
  2129. $lines = array();
  2130. foreach ($this->_edits as $item) {
  2131. $edit = /*.(ajaxUnit_Text_Diff_Op).*/ $item;
  2132. if (isset($edit->originalArray)) {
  2133. array_splice($lines, count($lines), 0, $edit->originalArray);
  2134. }
  2135. }
  2136. return $lines;
  2137. }
  2138. /**
  2139. * Gets the final set of lines.
  2140. *
  2141. * This reconstructs the $to_lines parameter passed to the constructor.
  2142. *
  2143. * @return array The sequence of strings.
  2144. */
  2145. private function getFinal()
  2146. {
  2147. $lines = array();
  2148. foreach ($this->_edits as $item) {
  2149. $edit = /*.(ajaxUnit_Text_Diff_Op).*/ $item;
  2150. if (isset($edit->finalArray)) {
  2151. array_splice($lines, count($lines), 0, $edit->finalArray);
  2152. }
  2153. }
  2154. return $lines;
  2155. }
  2156. /**
  2157. * Removes trailing newlines from a line of text. This is meant to be used
  2158. * with array_walk().
  2159. *
  2160. * @param string &$line The line to trim.
  2161. * @param integer $key The index of the line in the array. Not used.
  2162. */
  2163. public static /*.void.*/ function trimNewlines(&$line, $key)
  2164. {
  2165. // $key is needed because that's the format of the array we're walking
  2166. $line = (string) str_replace(array("\n", "\r"), '', $line);
  2167. }
  2168. }
  2169. /**
  2170. * A class to render Diffs in different formats.
  2171. *
  2172. * This class renders the diff in classic diff format. It is intended that
  2173. * this class be customized via inheritance, to obtain fancier outputs.
  2174. *
  2175. * $Horde: framework/Text_Diff/Diff/Renderer.php,v 1.5.10.12 2009/07/24 13:26:40 jan Exp $
  2176. *
  2177. * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
  2178. *
  2179. * See the enclosed file COPYING for license information (LGPL). If you did
  2180. * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
  2181. *
  2182. * @package Text_Diff
  2183. */
  2184. class ajaxUnit_Text_Diff_Renderer {
  2185. /**
  2186. * Number of leading context "lines" to preserve.
  2187. *
  2188. * This should be left at zero for this class, but subclasses may want to
  2189. * set this to other values.
  2190. */
  2191. private $_leading_context_lines = 0;
  2192. /**
  2193. * Number of trailing context "lines" to preserve.
  2194. *
  2195. * This should be left at zero for this class, but subclasses may want to
  2196. * set this to other values.
  2197. */
  2198. private $_trailing_context_lines = 0;
  2199. private /*.string.*/ function _startDiff()
  2200. {
  2201. return '';
  2202. }
  2203. private /*.string.*/ function _blockHeader(/*.int.*/ $xbeg, /*.int.*/ $xlen, /*.int.*/ $ybeg, /*.int.*/ $ylen)
  2204. {
  2205. if ($xlen > 1) {
  2206. $xbegString = $xbeg . ',' . ($xbeg + $xlen - 1);
  2207. }
  2208. if ($ylen > 1) {
  2209. $ybegString = $ybeg . ',' . ($ybeg + $ylen - 1);
  2210. }
  2211. // this matches the GNU Diff behaviour
  2212. if (($xlen !== 0) && ($ylen === 0)) {
  2213. $ybeg--;
  2214. } elseif ($xlen === 0) {
  2215. $xbeg--;
  2216. }
  2217. if (!isset($xbegString)) $xbegString = (string) $xbeg;
  2218. if (!isset($ybegString)) $ybegString = (string) $ybeg;
  2219. return $xbegString . (($xlen === 0) ? 'a' : (($ylen === 0) ? 'd' : 'c')) . $ybegString;
  2220. }
  2221. private /*.string.*/ function _startBlock(/*.string.*/ $header)
  2222. {
  2223. return $header . "\n";
  2224. }
  2225. private /*.string.*/ function _lines(/*.array[int]string.*/ $lines, $prefix = ' ')
  2226. {
  2227. return $prefix . implode("\n$prefix", $lines) . "\n";
  2228. }
  2229. private /*.string.*/ function _context(/*.array[int]string.*/ $lines)
  2230. {
  2231. return $this->_lines($lines, ' ');
  2232. }
  2233. private /*.string.*/ function _added(/*.array[int]string.*/ $lines)
  2234. {
  2235. return $this->_lines($lines, '> ');
  2236. }
  2237. private /*.string.*/ function _deleted(/*.array[int]string.*/ $lines)
  2238. {
  2239. return $this->_lines($lines, '< ');
  2240. }
  2241. private /*.string.*/ function _changed(/*.array[int]string.*/ $originalArray, /*.array[int]string.*/ $finalArray)
  2242. {
  2243. return $this->_deleted($originalArray) . "---\n" . $this->_added($finalArray);
  2244. }
  2245. private /*.string.*/ function _endBlock()
  2246. {
  2247. return '';
  2248. }
  2249. private /*.string.*/ function _block(/*.int.*/ $xbeg, /*.int.*/ $xlen, /*.int.*/ $ybeg, /*.int.*/ $ylen, /*.array[int]object.*/ &$edits)
  2250. {
  2251. $output = $this->_startBlock($this->_blockHeader($xbeg, $xlen, $ybeg, $ylen));
  2252. foreach ($edits as $item) {
  2253. $edit = /*.(ajaxUnit_Text_Diff_Op).*/ $item;
  2254. switch (strtolower(get_class($edit))) {
  2255. case 'ajaxunit_text_diff_op_copy':
  2256. $output .= $this->_context($edit->originalArray);
  2257. break;
  2258. case 'ajaxunit_text_diff_op_add':
  2259. $output .= $this->_added($edit->finalArray);
  2260. break;
  2261. case 'ajaxunit_text_diff_op_delete':
  2262. $output .= $this->_deleted($edit->originalArray);
  2263. break;
  2264. case 'ajaxunit_text_diff_op_change':
  2265. $output .= $this->_changed($edit->originalArray, $edit->finalArray);
  2266. break;
  2267. default:
  2268. }
  2269. }
  2270. return $output . $this->_endBlock();
  2271. }
  2272. private /*.string.*/ function _endDiff()
  2273. {
  2274. return '';
  2275. }
  2276. /**
  2277. * Renders a diff.
  2278. *
  2279. * @param ajaxUnit_Text_Diff $diff A ajaxUnit_Text_Diff object.
  2280. *
  2281. * @return string The formatted output.
  2282. */
  2283. function render($diff)
  2284. {
  2285. $x0 = $y0 = 0;
  2286. $xi = $yi = 1;
  2287. $context = /*.(array[int]string).*/ array();
  2288. $block = /*.(array[int]object).*/ array();
  2289. $nlead = $this->_leading_context_lines;
  2290. $ntrail = $this->_trailing_context_lines;
  2291. $output = $this->_startDiff();
  2292. $diffs = $diff->getDiff();
  2293. foreach ($diffs as $i => $item) {
  2294. $edit = /*.(ajaxUnit_Text_Diff_Op).*/ $item;
  2295. /* If these are unchanged (copied) lines, and we want to keep
  2296. * leading or trailing context lines, extract them from the copy
  2297. * block. */
  2298. if (is_a($edit, 'ajaxUnit_Text_Diff_Op_copy')) {
  2299. /* Do we have any diff blocks yet? */
  2300. if (isset($block)) {
  2301. /* How many lines to keep as context from the copy
  2302. * block. */
  2303. $keep = $i == count($diffs) - 1 ? $ntrail : $nlead + $ntrail;
  2304. if (count($edit->originalArray) <= $keep) {
  2305. /* We have less lines in the block than we want for
  2306. * context => keep the whole block. */
  2307. $block[] = $edit;
  2308. } else {
  2309. if ($ntrail !== 0) {
  2310. /* Create a new block with as many lines as we need
  2311. * for the trailing context. */
  2312. $context = /*.(array[int]string).*/ array_slice($edit->originalArray, 0, $ntrail);
  2313. $block[] = new ajaxUnit_Text_Diff_Op_copy($context);
  2314. }
  2315. /* @todo */
  2316. $output .= $this->_block($x0, $ntrail + $xi - $x0,
  2317. $y0, $ntrail + $yi - $y0,
  2318. $block);
  2319. unset($block);
  2320. }
  2321. }
  2322. /* Keep the copy block as the context for the next block. */
  2323. $context = $edit->originalArray;
  2324. } else {
  2325. /* Don't we have any diff blocks yet? */
  2326. if (!isset($block)) {
  2327. /* Extract context lines from the preceding copy block. */
  2328. $context = /*.(array[int]string).*/ array_slice($context, count($context) - $nlead);
  2329. $x0 = $xi - count($context);
  2330. $y0 = $yi - count($context);
  2331. $block = /*.(array[int]object).*/ array();
  2332. if (count($context) !== 0) {
  2333. $block[] = new ajaxUnit_Text_Diff_Op_copy($context);
  2334. }
  2335. }
  2336. $block[] = $edit;
  2337. }
  2338. if (isset($edit->originalArray)) {
  2339. $xi += count($edit->originalArray);
  2340. }
  2341. if (isset($edit->finalArray)) {
  2342. $yi += count($edit->finalArray);
  2343. }
  2344. }
  2345. if (isset($block)) {
  2346. $output .= $this->_block($x0, $xi - $x0,
  2347. $y0, $yi - $y0,
  2348. $block);
  2349. }
  2350. return $output . $this->_endDiff();
  2351. }
  2352. }
  2353. class ajaxUnit_compare {
  2354. private static /*.string.*/ function analyze(/*.array[int]string.*/ $array1, /*.array[int]string.*/ $array2) {
  2355. $diff = new ajaxUnit_Text_Diff($array1, $array2);
  2356. $renderer = new ajaxUnit_Text_Diff_Renderer();
  2357. return htmlspecialchars($renderer->render($diff));
  2358. }
  2359. public static /*.string.*/ function investigateDifference(/*.string.*/ $results, /*.string.*/ $expected, /*.string.*/ $diffID, $dummyRun = false) {
  2360. $logEntry = '';
  2361. $resultsMetric = strlen($results);
  2362. $expectedMetric = strlen($expected);
  2363. $analysis = "Response has $resultsMetric characters, expected response has $expectedMetric<br />\n";
  2364. $resultsMetric = (int) str_word_count($results);
  2365. $expectedMetric = (int) str_word_count($expected);
  2366. $analysis .= "\t\t\t\tResponse has $resultsMetric words, expected response has $expectedMetric<br />\n";
  2367. $percentFloat = 0;
  2368. $metric = similar_text($results, $expected, $percentFloat);
  2369. $percentString = number_format($percentFloat, 2);
  2370. $analysis .= "\t\t\t\tText similarity: $metric characters ($percentString%)";
  2371. $logEntry .= ajaxUnit_log::format($analysis, 6);
  2372. $diffText = self::analyze(explode("\n", $expected), explode("\n", $results));
  2373. $logEntry .= ajaxUnit_log::format("<span class=\"ajaxunit-testlog\" onclick=\"ajaxUnit_toggle_log(this, '$diffID')\">+</span> Actual results compared with expected results", 6);
  2374. $logEntry .= ajaxUnit_log::format("<pre class=\"ajaxunit-testlog\" id=\"$diffID\">$diffText</pre>", 8, '');
  2375. $logEntry .= ajaxUnit_log::format("<span class=\"ajaxunit-testlog\" onclick=\"ajaxUnit_toggle_log(this, '$diffID-expected')\">+</span> Expected results", 6);
  2376. $logEntry .= ajaxUnit_log::format("<pre class=\"ajaxunit-testlog\" id=\"$diffID-expected\">" . bin2hex($expected) . "</pre>", 8, '');
  2377. $logEntry .= ajaxUnit_log::format("<span class=\"ajaxunit-testlog\" onclick=\"ajaxUnit_toggle_log(this, '$diffID-results')\">+</span> Actual results", 6);
  2378. $logEntry .= ajaxUnit_log::format("<pre class=\"ajaxunit-testlog\" id=\"$diffID-results\">" . bin2hex($results) . "</pre>", 8, '');
  2379. $logEntry .= ajaxUnit_log::format('<div style="margin-left:6em">' . ajaxUnit_cookies::toTable() . '</div>');
  2380. return $logEntry;
  2381. }
  2382. private static /*.boolean.*/ function compareHTML($results = '', $expected = '', &$logEntry = '') {
  2383. preg_match_all('/(?<=>|^)[^><\\r\\n]+(?=<|$|\\r|\\n)|<.*>/Um', $results, $matches);
  2384. $resultsArray = $matches[0];
  2385. preg_match_all('/(?<=>|^)[^><\\r\\n]+(?=<|$|\\r|\\n)|<.*>/Um', $expected, $matches);
  2386. $expectedArray = $matches[0];
  2387. $resultsLineCount = count($resultsArray);
  2388. $expectedLineCount = count($expectedArray);
  2389. $logEntry .= ajaxUnit_log::format("Trying element-by-element comparison of $resultsLineCount elements...", 6);
  2390. if ($resultsLineCount === $expectedLineCount) {
  2391. $success = true;
  2392. } else {
  2393. $logEntry .= ajaxUnit_log::format("Different number of lines: expecting $expectedLineCount", 8);
  2394. $success = false;
  2395. }
  2396. for ($line = 0; $line < $resultsLineCount; $line++) {
  2397. $thisResultsLine = $resultsArray[$line];
  2398. $thisExpectedLine = $expectedArray[$line];
  2399. $lineOrdinal = $line + 1;
  2400. if ($thisResultsLine !== $thisExpectedLine) {
  2401. $logEntry .= ajaxUnit_log::format("Line $lineOrdinal is different to the expected line", 8);
  2402. if ($thisResultsLine[0] === '<' && $thisExpectedLine[0] === '<') {
  2403. $logEntry .= ajaxUnit_log::format("Comparing line $lineOrdinal according to its HTML attributes", 8);
  2404. // Compare attributes
  2405. $thisResultsAttributes = preg_split('/[\\s]+/', substr($thisResultsLine, 1, strlen($thisResultsLine) - 2));
  2406. $thisExpectedAttributes = preg_split('/[\\s]+/', substr($thisExpectedLine, 1, strlen($thisExpectedLine) - 2));
  2407. $attributeCount = count($thisResultsAttributes);
  2408. $logEntry .= ajaxUnit_log::format("Comparing $attributeCount attributes on line $lineOrdinal...", 8);
  2409. if ($attributeCount === count($thisExpectedAttributes)) {
  2410. $allAttributesSame = true;
  2411. sort($thisResultsAttributes);
  2412. sort($thisExpectedAttributes);
  2413. $log = "<pre>|Actual|Expected|<br />\n";
  2414. for ($attribute = 0; $attribute < $attributeCount; $attribute++) {
  2415. $thisResultsAttribute = $thisResultsAttributes[$attribute];
  2416. $thisExpectedAttribute = $thisExpectedAttributes[$attribute];
  2417. $log .= '|'. htmlspecialchars($thisResultsAttribute) . "|" . htmlspecialchars($thisExpectedAttribute) . "|<br />\n";
  2418. if ($thisResultsAttribute !== $thisExpectedAttribute) {
  2419. if ($success) $logEntry .= ajaxUnit_log::format("In line $lineOrdinal, '". htmlspecialchars($thisResultsAttribute) . "' (actual) was not the same as '" . htmlspecialchars($thisExpectedAttribute) . "' (expected)", 8);
  2420. $success = false;
  2421. $allAttributesSame = false;
  2422. }
  2423. }
  2424. if ($allAttributesSame) {
  2425. $logEntry .= ajaxUnit_log::format('All attributes are identical. Lines must differ in another way (e.g. white space)', 8);
  2426. $log = "<pre>Actual:<br />\n".htmlspecialchars($thisResultsLine)."<br />\nExpected:<br />\n".htmlspecialchars($thisExpectedLine)."</pre>\n";
  2427. $logEntry .= ajaxUnit_log::format($log, 8);
  2428. } else if (!$success) {
  2429. $logEntry .= ajaxUnit_log::format("$log</pre>", 8);
  2430. break;
  2431. }
  2432. } else {
  2433. $logEntry .= ajaxUnit_log::format("Different number of attributes in line $lineOrdinal", 8);
  2434. $success = false;
  2435. break;
  2436. }
  2437. } else {
  2438. $log = "<pre>Actual:<br />\n".htmlspecialchars($thisResultsLine)."<br />\nExpected:<br />\n".htmlspecialchars($thisExpectedLine)."</pre>\n";
  2439. $logEntry .= ajaxUnit_log::format($log, 8);
  2440. $success = false;
  2441. }
  2442. }
  2443. }
  2444. return $success;
  2445. }
  2446. public static /*.boolean.*/ function compare(/*.string.*/ $results, /*.string.*/ $expected, /*.string.*/ &$logEntry = '') {
  2447. $success = ($results === $expected); // This is the whole point of all this!
  2448. if (!$success) {
  2449. $logEntry = ajaxUnit_log::format('Byte-for-byte match not successful, trying looser EOL definition...', 6);
  2450. // Try substituting \r\n for \n in strings (because bits of either may come from a Windows file)
  2451. $results = (string) str_replace("\r\n", "\n", $results);
  2452. $expected = (string) str_replace("\r\n", "\n", $expected);
  2453. $success = ($results === $expected);
  2454. }
  2455. if (!$success) {
  2456. $logEntry = ajaxUnit_log::format('Still different even with looser EOL definition', 6);
  2457. $success = self::compareHTML($results, $expected, $logEntry);
  2458. }
  2459. return $success;
  2460. }
  2461. }
  2462. /**
  2463. * The methods needed to manage the running of an individual test
  2464. *
  2465. * @package ajaxUnit
  2466. */
  2467. interface I_ajaxUnit_test extends I_ajaxUnit_environment {
  2468. // public /*.int.*/ function count();
  2469. public /*.string.*/ function name();
  2470. public /*.string.*/ function description();
  2471. public /*.string.*/ function rubric();
  2472. public /*.string.*/ function resultSet();
  2473. public /*.DOMElement.*/ function results();
  2474. public /*.void.*/ function update();
  2475. // public /*.boolean.*/ function initiate($dummyRun = false);
  2476. public /*.void.*/ function doNextTest($dummyRun = false);
  2477. }
  2478. /**
  2479. * The methods needed to manage the running of an individual test
  2480. *
  2481. * @package ajaxUnit
  2482. */
  2483. class ajaxUnit_test extends ajaxUnit_environment implements I_ajaxUnit_test {
  2484. private /*.string.*/ $suite;
  2485. private /*.int.*/ $testIndex = -1;
  2486. private /*.DOMDocument.*/ $document;
  2487. private /*.DOMNodeList.*/ $testList;
  2488. private /*.DOMElement.*/ $test;
  2489. private /*.string.*/ $resultsNodeName;
  2490. private /*.int.*/ function count() {
  2491. $count = $this->testList->length;
  2492. if (empty($count)) $count = 0;
  2493. return $count;
  2494. }
  2495. public /*.string.*/ function name() {
  2496. return ($this->test->hasAttribute(self::ATTRNAME_NAME)) ? $this->test->getAttribute(self::ATTRNAME_NAME) : (string) $this->testIndex;
  2497. }
  2498. public /*.string.*/ function description() {
  2499. $numberType = ($this->test->hasAttribute(self::ATTRNAME_NAME)) ? 'name' : 'index';
  2500. $testName = $this->name();
  2501. return "Test $numberType is <strong>$testName</strong>";
  2502. }
  2503. public /*.string.*/ function rubric() {
  2504. $rubricList = $this->test->getElementsByTagName(self::TAGNAME_RUBRIC);
  2505. return (empty($rubricList->length) || ($rubricList->length === 0)) ? '' : $rubricList->item(0)->nodeValue;
  2506. }
  2507. public /*.string.*/ function resultSet() {
  2508. return $this->resultsNodeName;
  2509. }
  2510. public /*.DOMElement.*/ function results() {
  2511. $resultsList = $this->test->getElementsByTagName($this->resultsNodeName);
  2512. if (empty($resultsList->length)) {
  2513. return $this->document->createElement($this->resultsNodeName);
  2514. } else {
  2515. $resultsNode = $resultsList->item(0);
  2516. /*.object.*/ $resultsObject = $resultsNode; // PHP-compliant typecasting
  2517. return /*.(DOMElement).*/ $resultsObject; // PHP-compliant typecasting
  2518. }
  2519. }
  2520. public /*.void.*/ function update() {
  2521. self::updateTestSuite($this->suite, $this->document);
  2522. }
  2523. private /*.void.*/ function getTestFromList() {
  2524. $node = $this->testList->item($this->testIndex); // Get this particular test
  2525. /*.object.*/ $nodeObject = $node; // PHP-compliant typecasting
  2526. $this->test = /*.(DOMElement).*/ $nodeObject; // PHP-compliant typecasting
  2527. }
  2528. public /*.void.*/ function __construct() {
  2529. // Get some context for this test
  2530. $context = /*.(array[string]string).*/ self::getTestContext();
  2531. $this->resultsNodeName = $context[self::TAGNAME_RESULTSNODENAME];
  2532. $this->suite = $context[self::TAGNAME_SUITE];
  2533. $this->testIndex = (int) $context[self::TAGNAME_INDEX];
  2534. $this->document = self::getDOMDocument($this->suite); // Get the test suite information
  2535. $this->testList = self::getTestList($this->document); // Get list of tests
  2536. if ($this->count() === 0) self::terminate(self::STATUS_FATALERROR, 'There are no tests defined in this test suite');
  2537. $this->getTestFromList();
  2538. }
  2539. private /*.boolean.*/ function next() {
  2540. $this->testIndex++;
  2541. if ($this->testIndex < $this->count()) {
  2542. self::setTestContext(self::TAGNAME_INDEX, (string) $this->testIndex);
  2543. $this->getTestFromList();
  2544. return true;
  2545. } else {
  2546. return false;
  2547. }
  2548. }
  2549. private static /*.boolean.*/ function doClick(DOMElement $element, $dummyRun = false) {
  2550. $id = self::substituteParameters($element->getAttribute(self::ATTRNAME_ID));
  2551. self::appendLog("Click button <strong>$id</strong>", $dummyRun, 2);
  2552. return true;
  2553. }
  2554. private static /*.boolean.*/ function doCookies(DOMNodeList $nodeList, $dummyRun = false) {
  2555. self::appendLog("Update cookies", $dummyRun, 2);
  2556. for ($i = 0; $i < $nodeList->length; $i++) {
  2557. $node = $nodeList->item($i);
  2558. if ($node->nodeType === XML_ELEMENT_NODE) {
  2559. /*.object.*/ $nodeObject = $node; // PHPLint-compliant typecasting
  2560. $element = /*.(DOMElement).*/ $nodeObject; // PHPLint-compliant typecasting
  2561. $action = $element->nodeName;
  2562. $name = self::substituteParameters($element->getAttribute(self::ATTRNAME_NAME));
  2563. if ($dummyRun) {
  2564. $value = $element->getAttribute(self::ATTRNAME_VALUE);
  2565. $days = $element->getAttribute(self::ATTRNAME_DAYS);
  2566. self::appendLog("$action: $name -> $value ($days)", $dummyRun);
  2567. } else {
  2568. switch ($action) {
  2569. case self::TAGNAME_DELETE:
  2570. self::appendLog(ajaxUnit_cookies::remove($name), $dummyRun);
  2571. break;
  2572. case self::TAGNAME_SET:
  2573. $value = $element->getAttribute(self::ATTRNAME_VALUE);
  2574. $days = $element->getAttribute(self::ATTRNAME_DAYS);
  2575. self::appendLog(ajaxUnit_cookies::set($name, $value, $days), $dummyRun);
  2576. break;
  2577. default:
  2578. }
  2579. }
  2580. }
  2581. }
  2582. return true;
  2583. }
  2584. private static /*.boolean.*/ function doCustomURL($dummyRun = false) {
  2585. self::appendLog("Custom URL", $dummyRun, 2);
  2586. return true;
  2587. }
  2588. private static /*.boolean.*/ function doRubric(DOMElement $element, $dummyRun = false) {
  2589. $log = '<em>' . $element->nodeValue . '</em>';
  2590. self::appendLog($log, $dummyRun, 2);
  2591. return true;
  2592. }
  2593. private static /*.boolean.*/ function doFileOps(DOMNodeList $nodeList, $dummyRun = false) {
  2594. self::appendLog("File operations", $dummyRun, 2);
  2595. $success = true; // Assume true for once
  2596. for ($i = 0; $i < $nodeList->length; $i++) {
  2597. $node = $nodeList->item($i);
  2598. if ($node->nodeType === XML_ELEMENT_NODE) {
  2599. /*.object.*/ $nodeObject = $node; // PHPLint-compliant typecasting
  2600. $element = /*.(DOMElement).*/ $nodeObject; // PHPLint-compliant typecasting
  2601. $action = $element->nodeName;
  2602. switch ($action) {
  2603. case self::TAGNAME_COPY:
  2604. $source = self::substituteParameters($element->getAttribute(self::ATTRNAME_SOURCE));
  2605. $destination = self::substituteParameters($element->getAttribute(self::ATTRNAME_DESTINATION));
  2606. self::appendLog("Copying <em>$source</em> to <em>$destination</em>", $dummyRun);
  2607. if (!$dummyRun) @copy($source, $destination);
  2608. break;
  2609. case self::TAGNAME_DELETE:
  2610. $name = self::substituteParameters($element->getAttribute(self::ATTRNAME_NAME));
  2611. self::appendLog("Deleting <em>$name</em>", $dummyRun);
  2612. if (!$dummyRun && is_file($name)) @unlink($name);
  2613. break;
  2614. case self::TAGNAME_CHECKSIZE:
  2615. $name = self::substituteParameters($element->getAttribute(self::ATTRNAME_NAME));
  2616. $expected = (int) $element->nodeValue;
  2617. self::appendLog("Checking size of <em>$name</em>, should be " . (string) $expected . ' bytes', $dummyRun);
  2618. if ($dummyRun) break;
  2619. if (is_file($name)) {
  2620. $result = @filesize($name);
  2621. if ($result === false) {
  2622. self::appendLog('Unexpected problem getting file size', $dummyRun, 6);
  2623. $success = false;
  2624. } else {
  2625. if ($result === $expected) {
  2626. self::appendLog('File size is correct', $dummyRun, 6);
  2627. } else {
  2628. self::appendLog('File size is ' . (string) $result . ' bytes', $dummyRun, 6);
  2629. $success = false;
  2630. }
  2631. }
  2632. } else {
  2633. self::appendLog('File not found', $dummyRun, 6);
  2634. $success = false;
  2635. }
  2636. break;
  2637. default:
  2638. self::appendLog("Unknown file operation: <em>$action</em>", $dummyRun);
  2639. $success = false;
  2640. }
  2641. }
  2642. }
  2643. return $success;
  2644. }
  2645. private static /*.boolean.*/ function doFormFill(DOMNodeList $nodeList, $dummyRun = false) {
  2646. self::appendLog("Update form fields", $dummyRun, 2);
  2647. for ($i = 0; $i < $nodeList->length; $i++) {
  2648. $node = $nodeList->item($i);
  2649. if ($node->nodeType === XML_ELEMENT_NODE) {
  2650. /*.object.*/ $nodeObject = $node; // PHPLint-compliant typecasting
  2651. $element = /*.(DOMElement).*/ $nodeObject; // PHPLint-compliant typecasting
  2652. $type = self::substituteParameters($element->nodeName);
  2653. $value = self::substituteParameters($element->nodeValue);
  2654. $id = self::substituteParameters($element->getAttribute(self::ATTRNAME_ID));
  2655. self::appendLog("Setting $type control <strong>$id</strong> to <em>$value</em>", $dummyRun);
  2656. }
  2657. }
  2658. return true;
  2659. }
  2660. private static /*.boolean.*/ function doHeaders(DOMNodeList $nodeList, $dummyRun = false) {
  2661. self::appendLog("Send HTTP headers", $dummyRun, 2);
  2662. if (headers_sent()) {
  2663. self::appendLog("<strong>Warning: headers have already been sent</strong>");
  2664. return false;
  2665. }
  2666. for ($i = 0; $i < $nodeList->length; $i++) {
  2667. $node = $nodeList->item($i);
  2668. if ($node->nodeType === XML_ELEMENT_NODE) {
  2669. $header = $node->nodeName;
  2670. $value = $node->nodeValue;
  2671. $value = self::substituteParameters($value);
  2672. self::appendLog("Setting <strong>$header</strong> to <em>$value</em>", $dummyRun);
  2673. if (!$dummyRun) {
  2674. if (strtolower($header) === 'location') {
  2675. header("Cache-Control: no-cache");
  2676. header("Pragma: no-cache");
  2677. header("$header: $value");
  2678. // echo '.';
  2679. // exit;
  2680. } else {
  2681. header("$header: $value");
  2682. }
  2683. }
  2684. }
  2685. }
  2686. return true;
  2687. }
  2688. private static /*.boolean.*/ function doIncludePath(DOMNodeList $nodeList, $dummyRun = false) {
  2689. self::appendLog("Include path", $dummyRun, 2);
  2690. self::appendLog("Current include path is <em>" . get_include_path() . "</em>", $dummyRun);
  2691. for ($i = 0; $i < $nodeList->length; $i++) {
  2692. $node = $nodeList->item($i);
  2693. if ($node->nodeType === XML_ELEMENT_NODE) {
  2694. switch ($node->nodeName) {
  2695. case self::TAGNAME_ADD:
  2696. $path = realpath(self::substituteParameters($node->nodeValue));
  2697. self::appendLog("Adding <em>$path</em> to include path", $dummyRun);
  2698. if (!$dummyRun) {
  2699. $newPath = get_include_path() . PATH_SEPARATOR . $path;
  2700. set_include_path($newPath);
  2701. }
  2702. break;
  2703. case self::TAGNAME_RESET:
  2704. self::appendLog("Restoring include path", $dummyRun);
  2705. if (!$dummyRun) restore_include_path();
  2706. break;
  2707. default:
  2708. }
  2709. }
  2710. }
  2711. self::appendLog("New include path is <em>" . get_include_path() . "</em>", $dummyRun);
  2712. return true;
  2713. }
  2714. private static /*.boolean.*/ function doLocation(DOMElement $element, $dummyRun = false) {
  2715. $url = self::substituteParameters($element->getAttribute(self::ATTRNAME_URL));
  2716. self::appendLog("Set location <strong>$url</strong>", $dummyRun, 2);
  2717. return true;
  2718. }
  2719. private static /*.boolean.*/ function doLogAppend(DOMElement $element, $dummyRun = false) {
  2720. $content = self::substituteParameters($element->nodeValue);
  2721. self::appendLog("Adding to browser log: <strong>$content</strong>", $dummyRun, 2);
  2722. return true;
  2723. }
  2724. private static /*.boolean.*/ function doPost(DOMElement $element, $dummyRun = false) {
  2725. $id = self::substituteParameters($element->getAttribute(self::ATTRNAME_ID));
  2726. self::appendLog("Posting contents of element <strong>$id</strong>", $dummyRun, 2);
  2727. return true;
  2728. }
  2729. private /*.boolean.*/ function doRemove(DOMElement $step, $dummyRun = false) {
  2730. $stepType = $step->nodeName;
  2731. $this->test->removeChild($step);
  2732. self::appendLog("Removing element <strong>$stepType</strong> from the instructions to be sent to the browser", $dummyRun, 2);
  2733. return true;
  2734. }
  2735. private static /*.boolean.*/ function doSession(DOMNodeList $nodeList, $dummyRun = false) {
  2736. self::appendLog("Server session handling", $dummyRun, 2);
  2737. for ($i = 0; $i < $nodeList->length; $i++) {
  2738. $node = $nodeList->item($i);
  2739. if ($node->nodeType === XML_ELEMENT_NODE) {
  2740. switch ($node->nodeName) {
  2741. case self::TAGNAME_RESET:
  2742. self::appendLog("Resetting session...", $dummyRun);
  2743. if (!$dummyRun) {
  2744. session_start();
  2745. session_destroy();
  2746. $_SESSION = /*.(array[string]mixed).*/ array();
  2747. }
  2748. self::appendLog("Session reset", $dummyRun);
  2749. break;
  2750. default:
  2751. }
  2752. }
  2753. }
  2754. return true;
  2755. }
  2756. private /*.boolean.*/ function initiate($dummyRun = false) {
  2757. $context = self::setInitialContext($this->test, $this->testIndex);
  2758. $testName = $this->test->hasAttribute(self::ATTRNAME_NAME) ? $this->test->getAttribute(self::ATTRNAME_NAME) : "#" . ($this->testIndex + 1);
  2759. $testId = htmlspecialchars($testName);
  2760. $sendToBrowser = false;
  2761. $success = false;
  2762. self::appendLog("<span class=\"ajaxunit-testlog\" onclick=\"ajaxUnit_toggle_log(this, 'ajaxunit-$testId')\">+</span> Test <strong>$testName</strong>", $dummyRun, 0, 'p', 3);
  2763. self::appendLog("<div class=\"ajaxunit-testlog\" id=\"ajaxunit-$testId\">", $dummyRun, 0, '', 3);
  2764. $childNodes = $this->test->childNodes;
  2765. // Do any server-based actions
  2766. for ($i = 0; $i < $childNodes->length; $i++) {
  2767. $node = $childNodes->item($i);
  2768. if ($node->nodeType === XML_ELEMENT_NODE) {
  2769. /*.object.*/ $nodeObject = $node; // PHPLint-compliant typecasting
  2770. $step = /*.(DOMElement).*/ $nodeObject; // PHPLint-compliant typecasting
  2771. $stepType = $step->nodeName;
  2772. $stepList = $step->childNodes;
  2773. if (substr($stepType, 0, strlen(self::TAGNAME_IGNORE)) === self::TAGNAME_IGNORE) $stepType = self::TAGNAME_IGNORE;
  2774. if (substr($stepType, 0, strlen(self::TAGNAME_RESULTS)) === self::TAGNAME_RESULTS) $stepType = self::TAGNAME_RESULTS;
  2775. switch ($stepType) {
  2776. case self::TAGNAME_CLICK: $success = self::doClick ($step, $dummyRun); $sendToBrowser = !$dummyRun; break;
  2777. case self::TAGNAME_COOKIES: $success = self::doCookies ($stepList, $dummyRun); break;
  2778. case self::TAGNAME_CUSTOMURL: $success = self::doCustomURL ( $dummyRun); break;
  2779. case self::TAGNAME_FILE: $success = self::doFileOps ($stepList, $dummyRun); break;
  2780. case self::TAGNAME_FORMFILL: $success = self::doFormFill ($stepList, $dummyRun); $sendToBrowser = !$dummyRun; break;
  2781. case self::TAGNAME_HEADERS: $success = self::doHeaders ($stepList, $dummyRun); break;
  2782. case self::TAGNAME_IGNORE: $success = $this->doRemove ($step, $dummyRun); break;
  2783. case self::TAGNAME_INCLUDEPATH: $success = self::doIncludePath ($stepList, $dummyRun); break;
  2784. case self::TAGNAME_LOCATION: $success = self::doLocation ($step, $dummyRun); $sendToBrowser = !$dummyRun; break;
  2785. case self::TAGNAME_LOGAPPEND: $success = self::doLogAppend ($step, $dummyRun); $sendToBrowser = !$dummyRun; break;
  2786. case self::TAGNAME_POST: $success = self::doPost ($step, $dummyRun); $sendToBrowser = !$dummyRun; break;
  2787. case self::TAGNAME_RESULTS: $success = $this->doRemove ($step, $dummyRun); break;
  2788. case self::TAGNAME_RUBRIC: $success = self::doRubric ($step, $dummyRun); break;
  2789. case self::TAGNAME_SESSION: $success = self::doSession ($stepList, $dummyRun); break;
  2790. case self::TAGNAME_STOP: $success = false; break;
  2791. default: $success = true; break;
  2792. }
  2793. if (!$success) break;
  2794. }
  2795. }
  2796. if ($success) {
  2797. if ($sendToBrowser) {
  2798. // We've got the next test details so send them to the browser script
  2799. self::appendLog('Sending instructions to browser', false, 2);
  2800. $xml = self::substituteParameters($this->document->saveXML($this->test));
  2801. self::sendContent($xml, self::ACTION_PARSE, 'text/xml');
  2802. } else {
  2803. self::appendLog('Nothing further to send to browser', false, 2);
  2804. }
  2805. if ($context[self::TAGNAME_EXPECTINGCOUNT] === '0') {
  2806. self::appendLog('No results expected from browser', false, 2);
  2807. self::logResult($success, $dummyRun); // $success = true at this point
  2808. } else {
  2809. self::appendLog('Waiting for ' . $context[self::TAGNAME_EXPECTINGCOUNT] . ' results', false, 2);
  2810. exit;
  2811. }
  2812. } else {
  2813. self::terminate(self::STATUS_FATALERROR, '', $success, $dummyRun);
  2814. }
  2815. return $success;
  2816. }
  2817. public /*.void.*/ function doNextTest($dummyRun = false) {
  2818. // If $this->initiate returns then we're not expecting any results, so move on to the next test.
  2819. do {
  2820. if (!$this->next()) {$success = true; break;}
  2821. $success = $this->initiate($dummyRun);
  2822. } while ($success);
  2823. if ($success) {
  2824. // Should only get here if we've successfully completed all the tests
  2825. self::terminate(self::STATUS_FINISHED, '', $success, $dummyRun);
  2826. }
  2827. }
  2828. }
  2829. // End of class ajaxUnit_test
  2830. /**
  2831. * The methods needed to parse some results
  2832. *
  2833. * @package ajaxUnit
  2834. */
  2835. interface I_ajaxUnit_results extends I_ajaxUnit_environment {
  2836. public static /*.boolean.*/ function parse(/*.boolean.*/ $dummyRun = false);
  2837. }
  2838. /**
  2839. * The methods needed to parse some results
  2840. *
  2841. * @package ajaxUnit
  2842. */
  2843. class ajaxUnit_results extends ajaxUnit_environment implements I_ajaxUnit_results {
  2844. private static /*.boolean.*/ function doResults(DOMNodeList $nodeList, /*.string.*/ $results, $dummyRun = false) {
  2845. self::appendLog("Compare actual results with expected", $dummyRun);
  2846. $context = /*.(array[string]string).*/ self::getTestContext();
  2847. if (!array_key_exists(self::TAGNAME_STATUS, $context)) {
  2848. echo 'Unknown testing status';
  2849. return false;
  2850. }
  2851. if (in_array($context[self::TAGNAME_STATUS], array('', self::STATUS_FINISHED, self::STATUS_FAIL, self::STATUS_FATALERROR))) {
  2852. echo 'No testing in progress';
  2853. return false;
  2854. }
  2855. $responseList = $context[self::TAGNAME_RESPONSELIST];
  2856. $success = false;
  2857. $resultIndex = -1;
  2858. // Each <result> element is a potential match for $results
  2859. // If none of them match then it's a FAIL
  2860. for ($i = 0; $i < $nodeList->length; $i++) {
  2861. $node = $nodeList->item($i);
  2862. if ($node->nodeType === XML_ELEMENT_NODE) {
  2863. $resultIndex++;
  2864. $indexText = (string) ($resultIndex + 1);
  2865. // Have we already matched this for a previous result?
  2866. if (strpos($responseList, (string) $resultIndex) === false) {
  2867. self::appendLog("Test data set #$indexText has already been matched with a previous result", $dummyRun, 6);
  2868. } else {
  2869. self::appendLog("Comparing result with test data set #$indexText...", $dummyRun, 6);
  2870. $results = htmlspecialchars_decode($results);
  2871. if (get_magic_quotes_gpc() !== 0) $results = stripslashes($results); // magic_quotes_gpc will go away soon, but for now
  2872. $expected = htmlspecialchars_decode(self::substituteParameters($node->nodeValue));
  2873. $expected = (string) str_replace(chr(0xEF).chr(0xBB).chr(0xBF), '', $expected); // Get rid of stray UTF-8 BOMs from XIncluded files
  2874. if ($dummyRun)
  2875. $success = true;
  2876. else {
  2877. $logEntry = '';
  2878. $success = ajaxUnit_compare::compare($results, $expected, $logEntry); // Compare results with expected
  2879. self::appendLog($logEntry, $dummyRun, 0, '');
  2880. }
  2881. if ($success) {
  2882. self::appendLog("Match with test data set #$indexText", $dummyRun, 6);
  2883. // Remove this results set from the list of responses we are expecting
  2884. $indexPos = strpos($responseList, (string) $resultIndex);
  2885. $responseList = substr($responseList, 0, $indexPos) . substr($responseList, $indexPos + 1);
  2886. self::setTestContext(self::TAGNAME_RESPONSELIST, $responseList);
  2887. break;
  2888. } else {
  2889. $diffID = 'ajaxunit-diff-' . $context[self::TAGNAME_INDEX] . '-' . $context[self::TAGNAME_RESPONSECOUNT] . '-' . (string) $i;
  2890. $logEntry = ajaxUnit_compare::investigateDifference($results, $expected, $diffID, $dummyRun);
  2891. self::appendLog($logEntry, $dummyRun, 0, '');
  2892. }
  2893. }
  2894. }
  2895. }
  2896. $result = ($success) ? "Match found!" : "No match found";
  2897. self::appendLog($result, $dummyRun, 6);
  2898. return $success;
  2899. }
  2900. /**
  2901. * If the test suite needs some model results then use these
  2902. */
  2903. private static /*.boolean.*/ function modelRequired(DOMElement $element, /*.boolean.*/ $finalResult = false) {
  2904. if (!$element->hasAttribute(self::ATTRNAME_UPDATE)) return false;
  2905. // Use these as model results
  2906. self::appendLog("Using these as model results");
  2907. $updateStatus = $element->getAttribute(self::ATTRNAME_UPDATE);
  2908. if ($updateStatus !== self::STATUS_INPROGRESS) {
  2909. self::appendLog("Clearing existing model results", false, 6);
  2910. // Clear away the children & set the status
  2911. while ($element->hasChildNodes()) $element->removeChild($element->lastChild);
  2912. $element->setAttribute(self::ATTRNAME_UPDATE, self::STATUS_INPROGRESS);
  2913. }
  2914. self::appendLog("Adding this result", false, 6);
  2915. $element->appendChild(new DOMElement(self::TAGNAME_RESULT, $_POST['responseText'])); // Add model result
  2916. if ($finalResult) {
  2917. self::appendLog("No more results expected", false, 6);
  2918. $element->removeAttribute(self::ATTRNAME_UPDATE);
  2919. }
  2920. return true;
  2921. }
  2922. /**
  2923. * Called by the browser when it receives an AJAX responseText
  2924. *
  2925. * @return boolean OK to move on to next test
  2926. */
  2927. public static function parse(/*.boolean.*/ $dummyRun = false) {
  2928. self::appendLog('Results received', $dummyRun, 2);
  2929. // Get some context for this test
  2930. $context = /*.(array[string]string).*/ self::getTestContext();
  2931. // Wait for previous reponse to be parsed
  2932. $totalSleeps = 0;
  2933. $maxCounter = (int) (self::TEST_MAXWAIT / self::TEST_WAITUSECS);
  2934. while ($context[self::TAGNAME_PARSING] === '1') {
  2935. if ($totalSleeps++ > $maxCounter) return self::terminate(self::STATUS_FATALERROR, 'Waited too long for another result to be parsed', false, $dummyRun);
  2936. self::appendLog('(Another response is being parsed. Waiting ' . self::TEST_WAITUSECS . ' microseconds)', $dummyRun, 2);
  2937. usleep(self::TEST_WAITUSECS);
  2938. $context = /*.(array[string]string).*/ self::getTestContext();
  2939. }
  2940. self::setTestContext(self::TAGNAME_PARSING, '1');
  2941. // self::appendLog("Test results being parsed at request of browser", $dummyRun, 2);
  2942. // Get some context for this test
  2943. $test = new ajaxUnit_test();
  2944. self::appendLog($test->description(), $dummyRun);
  2945. $expected = (int) $context[self::TAGNAME_EXPECTINGCOUNT];
  2946. if ($expected < 1) {
  2947. self::appendLog("No further responses expected", $dummyRun);
  2948. $success = true;
  2949. self::logResult($success, $dummyRun); // $success = true at this point
  2950. return true;
  2951. }
  2952. // Check response count is (a) valid and (b) all we are expecting on this page
  2953. if (!isset($context[self::TAGNAME_RESPONSECOUNT])) return self::terminate(self::STATUS_FATALERROR, '<strong>No response count set!</strong>', false, $dummyRun);
  2954. $responseCount = (int) $context[self::TAGNAME_RESPONSECOUNT];
  2955. if (++$responseCount < 1) return self::terminate(self::STATUS_FATALERROR, "<strong>Response count hasn't been set correctly! ($responseCount)</strong>", false, $dummyRun);
  2956. else self::setTestContext(self::TAGNAME_RESPONSECOUNT, (string) $responseCount);
  2957. self::appendLog("Response count is $responseCount (expecting $expected).", $dummyRun);
  2958. self::appendLog('Using result set <em>' . $test->resultSet() . '</em>', $dummyRun);
  2959. // Examine the results element
  2960. $resultsElement = $test->results();
  2961. $success = self::modelRequired($resultsElement, ($responseCount >= $expected));
  2962. if ($success) {
  2963. // We've added model answers, so write out the test suite
  2964. self::appendLog("Updating test suite", false, 6);
  2965. if (!$dummyRun) $test->update();
  2966. } else {
  2967. if (isset($_POST['responseText'])) {
  2968. // Compare these results against the model results
  2969. $results = ($dummyRun) ? '' : (string) $_POST['responseText'];
  2970. $success = self::doResults($resultsElement->childNodes, $results, $dummyRun); // Is it the same?
  2971. } else {
  2972. self::appendLog("No responseText in results received", $dummyRun, 2);
  2973. $success = false;
  2974. }
  2975. if (!$success) return self::terminate(self::STATUS_FAIL, '', $success, $dummyRun);
  2976. }
  2977. if (!$dummyRun && ($responseCount < $expected)) {
  2978. self::appendLog("Waiting for further responses from browser", $dummyRun, 2);
  2979. self::setTestContext(self::TAGNAME_PARSING, '0');
  2980. return false;
  2981. }
  2982. self::logResult($success, $dummyRun); // $success = true at this point
  2983. return $success;
  2984. }
  2985. }
  2986. // End of class ajaxUnit_results
  2987. /**
  2988. * All the methods needed to control the test run
  2989. *
  2990. * @package ajaxUnit
  2991. */
  2992. interface I_ajaxUnit_control extends I_ajaxUnit_environment {
  2993. public static /*.void.*/ function parseResults(/*.boolean.*/ $dummyRun = false);
  2994. public static /*.void.*/ function runTestSuite(/*.string.*/ $suite, $dummyRun = false);
  2995. public static /*.void.*/ function endTestSuite($message = '', $dummyRun = false);
  2996. }
  2997. /**
  2998. * All the methods needed to control the test run
  2999. *
  3000. * @package ajaxUnit
  3001. */
  3002. abstract class ajaxUnit_control extends ajaxUnit_environment implements I_ajaxUnit_control {
  3003. public static /*.void.*/ function parseResults(/*.boolean.*/ $dummyRun = false) {
  3004. if (ajaxUnit_results::parse($dummyRun)) {
  3005. $test = new ajaxUnit_test();
  3006. $test->doNextTest($dummyRun);
  3007. }
  3008. }
  3009. /**
  3010. * Initiate a named series of tests
  3011. */
  3012. public static /*.void.*/ function runTestSuite(/*.string.*/ $suite, /*.boolean.*/ $dummyRun = false) {
  3013. $context = /*.(array[string]string).*/ self::getTestContext();
  3014. $context[self::TAGNAME_UID] = gmdate('YmdHis');
  3015. $context[self::TAGNAME_SUITE] = $suite;
  3016. $context[self::TAGNAME_STATUS] = self::STATUS_INPROGRESS;
  3017. $context[self::TAGNAME_COUNT] = "0";
  3018. $context[self::TAGNAME_INDEX] = "-1";
  3019. $browser = new ajaxUnit_browser();
  3020. $context[self::TAGNAME_BROWSER] = $browser->Name;
  3021. $context[self::TAGNAME_BROWSERVERSION] = $browser->Version;
  3022. self::setTestContext($context);
  3023. $document = self::getDOMDocument($suite);
  3024. $text = ($dummyRun) ? "Dummy" : "Starting";
  3025. $suiteNode = $document->getElementsByTagName(self::TAGNAME_SUITE)->item(0);
  3026. /*.object.*/ $suiteObject = $suiteNode; // PHPLint-compliant typecasting
  3027. $suiteElement = /*.(DOMElement).*/ $suiteObject; // PHPLint-compliant typecasting
  3028. $suiteName = htmlspecialchars($suiteElement->getAttribute(self::ATTRNAME_NAME));
  3029. $suiteVersion = $suiteElement->getAttribute("version");
  3030. self::appendLog(self::htmlPageTop(), $dummyRun, 0, '', 0);
  3031. self::appendLog("<h3>$text run of test suite \"$suiteName\" version $suiteVersion</h3>", $dummyRun, 0, '', 3);
  3032. self::appendLog("<hr />", $dummyRun, 0, '', 3);
  3033. // Add test script to log
  3034. // Not often useful and makes the log a lot bigger
  3035. // self::logTestScript($document, $dummyRun);
  3036. // Get global parameters
  3037. $nodeList = $document->getElementsByTagName(self::TAGNAME_PARAMETERS);
  3038. if ($nodeList->length !== 0) {
  3039. $node = $nodeList->item(0);
  3040. $length = $node->childNodes->length;
  3041. for ($i = 0; $i < $length; $i++) {
  3042. $parameter = $node->childNodes->item($i);
  3043. if ($parameter->nodeType === XML_ELEMENT_NODE) $context[$parameter->nodeName] = self::substituteParameters($parameter->nodeValue);
  3044. }
  3045. }
  3046. // Set the controls for the heart of the sun
  3047. self::setTestContext($context);
  3048. self::logTestContext($dummyRun);
  3049. // Run first test
  3050. $test = new ajaxUnit_test();
  3051. $test->doNextTest($dummyRun);
  3052. }
  3053. /**
  3054. * Abandon running tests
  3055. */
  3056. public static /*.void.*/ function endTestSuite(/*.string.*/ $message = '', /*.boolean.*/ $dummyRun = false) {
  3057. self::appendLog("Testing ended at request of browser", $dummyRun);
  3058. self::terminate(self::STATUS_FINISHED, $message, false, $dummyRun);
  3059. }
  3060. }
  3061. // End of class ajaxUnit_control
  3062. /**
  3063. * User interface class
  3064. *
  3065. * @package ajaxUnit
  3066. */
  3067. interface I_ajaxUnit extends I_ajaxUnit_control {
  3068. // public static /*.void.*/ function setProject(/*.string.*/ $project);
  3069. public static /*.void.*/ function setRoot(/*.string.*/ $folder);
  3070. public static /*.void.*/ function getCSS();
  3071. public static /*.void.*/ function getJavascript();
  3072. public static /*.string.*/ function getIcon($filename = '', $sendToBrowser = true);
  3073. public static /*.void.*/ function getControlPanel();
  3074. }
  3075. /**
  3076. * User interface class
  3077. *
  3078. * @package ajaxUnit
  3079. */
  3080. class ajaxUnit extends ajaxUnit_control implements I_ajaxUnit {
  3081. // ---------------------------------------------------------------------------
  3082. // Helper functions
  3083. // ---------------------------------------------------------------------------
  3084. private static /*.void.*/ function setProject(/*.string.*/ $project) {
  3085. self::setTestContext(self::TAGNAME_PROJECT, $project);
  3086. }
  3087. public static /*.void.*/ function setRoot(/*.string.*/ $folder) {
  3088. $baseURL = (string) str_replace(DIRECTORY_SEPARATOR, '/', $folder);
  3089. self::setTestContext(self::TAGNAME_FOLDER_BASE, $folder);
  3090. self::setTestContext(self::TAGNAME_FOLDER_LOGS, $folder.DIRECTORY_SEPARATOR.self::LOG_FOLDER);
  3091. self::setTestContext(self::TAGNAME_FOLDER_TESTS, $folder.DIRECTORY_SEPARATOR.self::TESTS_FOLDER);
  3092. self::setTestContext(self::TAGNAME_URL_BASE, $baseURL);
  3093. self::setTestContext(self::TAGNAME_URL_LOGS, $baseURL.'/'.self::LOG_FOLDER);
  3094. self::setTestContext(self::TAGNAME_URL_TESTS, $baseURL.'/'.self::TESTS_FOLDER);
  3095. }
  3096. // ---------------------------------------------------------------------------
  3097. // UI features
  3098. // ---------------------------------------------------------------------------
  3099. private static /*.string.*/ function htmlCSS() {
  3100. $css = <<<GENERATED
  3101. @charset "UTF-8";
  3102. /**
  3103. * Testing PHP and javascript by controlling the interactions automatically
  3104. *
  3105. * Copyright (c) 2008-2010, Dominic Sayers <br>
  3106. * All rights reserved.
  3107. *
  3108. * Redistribution and use in source and binary forms, with or without modification,
  3109. * are permitted provided that the following conditions are met:
  3110. *
  3111. * - Redistributions of source code must retain the above copyright notice,
  3112. * this list of conditions and the following disclaimer.
  3113. * - Redistributions in binary form must reproduce the above copyright notice,
  3114. * this list of conditions and the following disclaimer in the documentation
  3115. * and/or other materials provided with the distribution.
  3116. * - Neither the name of Dominic Sayers nor the names of its contributors may be
  3117. * used to endorse or promote products derived from this software without
  3118. * specific prior written permission.
  3119. *
  3120. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  3121. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  3122. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  3123. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
  3124. * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  3125. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  3126. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  3127. * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  3128. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  3129. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  3130. *
  3131. * @package ajaxUnit
  3132. * @author Dominic Sayers <dominic@sayers.cc>
  3133. * @copyright 2008-2010 Dominic Sayers
  3134. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  3135. * @link http://code.google.com/p/ajaxUnit/
  3136. * @version 0.17.30 - Added 'ignore' tag - just put 'ignore' before any test element to ignore it
  3137. */
  3138. .dummy {} /* Webkit is ignoring the first item so we'll put a dummy one in */
  3139. form.ajaxunit-form {margin:0;}
  3140. fieldset.ajaxunit-fieldset {padding:0;border:0;margin:0 0 1em 0}
  3141. input.ajaxunit-buttonstate-0 {background-color:#FFFFFF;color:#444444;border-color:#666666 #333333 #333333 #666666;}
  3142. div.ajaxunit-testlog {display:none;}
  3143. span.ajaxunit-testlog {font-weight:bold;font-size:16px;cursor:pointer;}
  3144. p.ajaxunit-testlog {margin:0;}
  3145. p.ajaxunit-footnote {font-size:9px;color:#666666;}
  3146. p.ajaxunit-footnote strong {font-weight:bold;color:red;}
  3147. p.ajaxunit-rubric {width:640px;font-family:Cambria, "Times New Roman", Times, sans-serif;font-size:12px;font-style:italic;}
  3148. pre.ajaxunit-testlog {margin:0 0 0 6em;background-color:#F0F0F0;display:none;}
  3149. iframe.ajaxunit-iframe {border:1px black solid;}
  3150. table {border: 1px solid #FFFFFF;background-color: #C0C0C0;}
  3151. div#ajaxunit {
  3152. font-family:"Segoe UI", Calibri, Arial, Helvetica, sans-serif;
  3153. font-size:11px;
  3154. line-height:16px;
  3155. float:left;
  3156. margin:0;
  3157. }
  3158. label.ajaxunit-label {
  3159. margin:5px 0 0 0;
  3160. float:left;
  3161. }
  3162. input.ajaxunit-text {
  3163. float:left;
  3164. font-size:11px;
  3165. width:480px;
  3166. margin:3px 0 0 7px;
  3167. }
  3168. input.ajaxunit-button {
  3169. float:left;
  3170. padding:2px;
  3171. margin:0 7px 0 7px;
  3172. font-family:"Segoe UI", Calibri, Arial, Helvetica, sans-serif;
  3173. border-style:solid;
  3174. border-width:1px;
  3175. cursor:pointer;
  3176. }
  3177. GENERATED;
  3178. // Generated code - do not modify in built package
  3179. return $css;
  3180. }
  3181. public static /*.void.*/ function getCSS() {
  3182. $html = self::htmlCSS();
  3183. self::sendContent($html, 'CSS', 'text/css');
  3184. }
  3185. // ---------------------------------------------------------------------------
  3186. private static /*.string.*/ function htmlJavascript() {
  3187. $parseURL = $URL = self::thisURL();
  3188. $actionCSS = self::ACTION_CSS;
  3189. $actionParse = self::ACTION_PARSE;
  3190. $tagCheckbox = self::TAGNAME_CHECKBOX;
  3191. $tagClick = self::TAGNAME_CLICK;
  3192. $tagFormFill = self::TAGNAME_FORMFILL;
  3193. $tagOpen = self::TAGNAME_OPEN;
  3194. $tagLocation = self::TAGNAME_LOCATION;
  3195. $tagLogAppend = self::TAGNAME_LOGAPPEND;
  3196. $tagPost = self::TAGNAME_POST;
  3197. $tagRadio = self::TAGNAME_RADIO;
  3198. $tagRubric = self::TAGNAME_RUBRIC;
  3199. $attrID = self::ATTRNAME_ID;
  3200. $attrURL = self::ATTRNAME_URL;
  3201. $js = <<<GENERATED
  3202. /**
  3203. * Testing PHP and javascript by controlling the interactions automatically
  3204. *
  3205. * Copyright (c) 2008-2010, Dominic Sayers <br>
  3206. * All rights reserved.
  3207. *
  3208. * Redistribution and use in source and binary forms, with or without modification,
  3209. * are permitted provided that the following conditions are met:
  3210. *
  3211. * - Redistributions of source code must retain the above copyright notice,
  3212. * this list of conditions and the following disclaimer.
  3213. * - Redistributions in binary form must reproduce the above copyright notice,
  3214. * this list of conditions and the following disclaimer in the documentation
  3215. * and/or other materials provided with the distribution.
  3216. * - Neither the name of Dominic Sayers nor the names of its contributors may be
  3217. * used to endorse or promote products derived from this software without
  3218. * specific prior written permission.
  3219. *
  3220. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  3221. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  3222. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  3223. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
  3224. * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  3225. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  3226. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  3227. * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  3228. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  3229. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  3230. *
  3231. * @package ajaxUnit
  3232. * @author Dominic Sayers <dominic@sayers.cc>
  3233. * @copyright 2008-2010 Dominic Sayers
  3234. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  3235. * @link http://code.google.com/p/ajaxUnit/
  3236. * @version 0.17.30 - Added 'ignore' tag - just put 'ignore' before any test element to ignore it
  3237. */
  3238. /*jslint onevar: true, undef: true, nomen: true, eqeqeq: true, regexp: true, newcap: true, immed: true, strict: true */
  3239. /*global window, document, event, ActiveXObject */ // For JSLint
  3240. //"use strict";
  3241. var ajaxUnitInstances = [];
  3242. // ---------------------------------------------------------------------------
  3243. // ajaxUnit
  3244. // ---------------------------------------------------------------------------
  3245. // The main ajaxUnit client-side class
  3246. // ---------------------------------------------------------------------------
  3247. function C_ajaxUnit() {
  3248. this.getControl = function (id) {return document.getElementById(id);};
  3249. this.getValue = function (id) {return this.getControl(id).value;};
  3250. this.setValue = function (id, value) {this.getControl(id).value = value;};
  3251. var that = this;
  3252. // ---------------------------------------------------------------------------
  3253. function fillContainer(id, html) {
  3254. var container = that.getControl(id);
  3255. if (container === null || typeof container === 'undefined') {return;}
  3256. // IE6 container.class = id;
  3257. container.className = id;
  3258. container.innerHTML = html;
  3259. }
  3260. // ---------------------------------------------------------------------------
  3261. function addStyleSheet() {
  3262. var htmlHead = document.getElementsByTagName('head')[0],
  3263. nodeList = htmlHead.getElementsByTagName('link'),
  3264. elementCount = nodeList.length,
  3265. found = false,
  3266. i, node;
  3267. for (i = 0; i < elementCount; i++) {
  3268. if (nodeList[i].title === 'ajaxUnit') {
  3269. found = true;
  3270. break;
  3271. }
  3272. }
  3273. if (found === false) {
  3274. node = document.createElement('link');
  3275. node.type = 'text/css';
  3276. node.rel = 'stylesheet';
  3277. node.href = '$URL?$actionCSS';
  3278. node.title = 'ajaxUnit';
  3279. htmlHead.appendChild(node);
  3280. }
  3281. }
  3282. // ---------------------------------------------------------------------------
  3283. function logAppend(text, fail) {
  3284. var id = 'ajaxunit-log',
  3285. container = that.getControl(id),
  3286. markupStart = '',
  3287. markupEnd = '',
  3288. element;
  3289. // if log div doesn't exist then add it to the page
  3290. if (container === null || typeof container === 'undefined') {
  3291. container = document.createElement('div');
  3292. container.id = id;
  3293. //- container.style.cssText = 'width:420px;font-family:Segoe UI, Calibri, Arial, Helvetica, sans-serif;font-size:11px;line-height:16px;margin:0;clear:left;background-color:#FFFF88;';
  3294. container.style.cssText = 'font-family:Segoe UI, Calibri, Arial, Helvetica, sans-serif;font-size:11px;line-height:16px;margin:0;clear:left;background-color:#FFFF88;';
  3295. document.getElementsByTagName('body')[0].appendChild(container);
  3296. }
  3297. // Log a fail?
  3298. if (arguments.length === 1) {fail = false;}
  3299. if (fail) {
  3300. markupStart = '<strong>';
  3301. markupEnd = '</strong>';
  3302. that.ajax.serverTalk('GET', 'end=' + encodeURI(text));
  3303. }
  3304. // Add to the log
  3305. element = document.createElement('p');
  3306. element.style.margin = 0;
  3307. element.innerHTML = markupStart + text + markupEnd;
  3308. container.appendChild(element);
  3309. // window.alert(text); // Uncomment this to step through the tests
  3310. }
  3311. // ---------------------------------------------------------------------------
  3312. function fireEvent(control, eventType, detail) {
  3313. var e, result; // Returned result from dispatchEvent
  3314. switch (eventType.toLowerCase()) {
  3315. case 'keyup':
  3316. case 'keydown':
  3317. if (document.createEventObject) {
  3318. // IE
  3319. e = document.createEventObject();
  3320. e.keyCode = detail;
  3321. result = control.fireEvent('on' + eventType);
  3322. } else if (window.KeyEvent) {
  3323. // Firefox
  3324. e = document.createEvent('KeyEvents');
  3325. e.initKeyEvent(eventType, true, true, window, false, false, false, false, detail, 0);
  3326. result = control.dispatchEvent(e);
  3327. } else {
  3328. e = document.createEvent('UIEvents');
  3329. e.initUIEvent(eventType, true, true, window, 1);
  3330. e.keyCode = detail;
  3331. result = control.dispatchEvent(e);
  3332. }
  3333. break;
  3334. case 'focus':
  3335. case 'blur':
  3336. case 'change':
  3337. if (document.createEventObject) {
  3338. // IE
  3339. e = document.createEventObject();
  3340. result = control.fireEvent('on' + eventType);
  3341. } else {
  3342. e = document.createEvent('UIEvents');
  3343. e.initUIEvent(eventType, true, true, window, 1);
  3344. result = control.dispatchEvent(e);
  3345. }
  3346. break;
  3347. case 'click':
  3348. if (document.createEventObject) {
  3349. // IE
  3350. e = document.createEventObject();
  3351. result = control.fireEvent('on' + eventType);
  3352. } else {
  3353. e = document.createEvent('MouseEvents');
  3354. e.initMouseEvent(eventType, true, true, window, 1);
  3355. result = control.dispatchEvent(e);
  3356. }
  3357. break;
  3358. }
  3359. return result;
  3360. }
  3361. // ---------------------------------------------------------------------------
  3362. function doFormFill(controlNode) {
  3363. var controlId = controlNode.getAttribute('$attrID'),
  3364. controlType = controlNode.nodeName,
  3365. controlValue = (typeof controlNode.textContent === 'undefined') ? controlNode.text : controlNode.textContent,
  3366. control = that.getControl(controlId),
  3367. keyCode, doEvent;
  3368. if (control === null) {
  3369. logAppend(' - - No control with id ' + controlId, true);
  3370. } else {
  3371. logAppend(' - - setting ' + controlId + ' (' + controlType + ') to ' + controlValue);
  3372. if (typeof document.activeElement.onBlur === 'function') {fireEvent(document.activeElement, 'blur');}
  3373. if (typeof document.activeElement.onblur === 'function') {fireEvent(document.activeElement, 'blur');}
  3374. if (typeof control.onFocus === 'function') {doEvent = fireEvent(control, 'focus');}
  3375. if (typeof control.onfocus === 'function') {doEvent = fireEvent(control, 'focus');}
  3376. if (doEvent !== false) {control.focus();}
  3377. switch (controlType) {
  3378. case '$tagCheckbox':
  3379. control.checked = (controlValue === 'checked') ? true : false;
  3380. if (typeof control.onClick === 'function') {fireEvent(control, 'click');}
  3381. if (typeof control.onclick === 'function') {fireEvent(control, 'click');}
  3382. break;
  3383. case '$tagRadio':
  3384. control.checked = (controlValue === 'checked') ? true : false;
  3385. if (typeof control.onClick === 'function') {fireEvent(control, 'click');}
  3386. if (typeof control.onclick === 'function') {fireEvent(control, 'click');}
  3387. break;
  3388. default:
  3389. control.defaultValue = controlValue;
  3390. control.value = controlValue;
  3391. if (typeof control.onChange === 'function') {fireEvent(control, 'change');}
  3392. if (typeof control.onchange === 'function') {fireEvent(control, 'change');}
  3393. keyCode = (controlValue.length === 0) ? 0 : controlValue.charCodeAt(controlValue.length - 1);
  3394. if (typeof control.onKeyDown === 'function') {fireEvent(control, 'keydown', keyCode);}
  3395. if (typeof control.onkeydown === 'function') {fireEvent(control, 'keydown', keyCode);}
  3396. if (typeof control.onKeyUp === 'function') {fireEvent(control, 'keyup', keyCode);}
  3397. if (typeof control.onkeyup === 'function') {fireEvent(control, 'keyup', keyCode);}
  3398. break;
  3399. }
  3400. }
  3401. }
  3402. // ---------------------------------------------------------------------------
  3403. function doStep(step) {
  3404. var url, control, controlId, controlNode, doEvent, element, html, postData, j, controlList;
  3405. switch (step.nodeName) {
  3406. case '$tagLocation':
  3407. url = step.getAttribute('$attrURL');
  3408. logAppend(' - changing location to ' + url);
  3409. window.location.assign(url);
  3410. break;
  3411. case '$tagOpen':
  3412. url = step.getAttribute('$attrURL');
  3413. logAppend(' - popping up ' + url);
  3414. window.open(url);
  3415. break;
  3416. case '$tagClick':
  3417. controlId = step.getAttribute('$attrID');
  3418. control = that.getControl(controlId);
  3419. if (control === null) {
  3420. logAppend(' - No control with id ' + controlId, true);
  3421. } else {
  3422. logAppend(' - clicking button ' + controlId);
  3423. if (typeof document.activeElement.onBlur === 'function') {fireEvent(document.activeElement, 'blur');}
  3424. if (typeof document.activeElement.onblur === 'function') {fireEvent(document.activeElement, 'blur');}
  3425. doEvent = true;
  3426. if (typeof control.onFocus === 'function') {doEvent = fireEvent(control, 'focus');}
  3427. if (typeof control.onfocus === 'function') {doEvent = fireEvent(control, 'focus');}
  3428. if (doEvent !== false) {
  3429. control.focus();
  3430. control.click();
  3431. }
  3432. }
  3433. break;
  3434. case '$tagPost':
  3435. controlId = step.getAttribute('$attrID');
  3436. control = that.getControl(controlId);
  3437. if (control === null) {
  3438. logAppend(' - No control with id ' + controlId, true);
  3439. } else {
  3440. element = document.createElement('dummy');
  3441. element.appendChild(control.cloneNode(true));
  3442. html = element.innerHTML;
  3443. postData = '$actionParse&responseText=' + encodeURIComponent(html);
  3444. logAppend(' - posting element ' + controlId);
  3445. that.ajax.serverTalk('POST', postData);
  3446. }
  3447. break;
  3448. case '$tagFormFill':
  3449. logAppend(' - filling form fields');
  3450. controlList = step.childNodes;
  3451. for (j = 0; j < controlList.length; j++) {
  3452. controlNode = controlList[j];
  3453. if (controlNode.nodeType === 1) {doFormFill(controlNode);}
  3454. }
  3455. break;
  3456. case '$tagLogAppend':
  3457. logAppend(' - ' + step.nodeValue);
  3458. break;
  3459. case '$tagRubric':
  3460. logAppend('<em>' + step.nodeValue + '</em>');
  3461. break;
  3462. default:
  3463. logAppend(' - unknown action: ' + step.nodeName, true);
  3464. logAppend(' - content is ' + step.nodeValue);
  3465. break;
  3466. }
  3467. }
  3468. // ---------------------------------------------------------------------------
  3469. function doActions(actionNode) {
  3470. logAppend('Doing prescribed actions:');
  3471. if (actionNode === null) {
  3472. logAppend(' - nothing to do');
  3473. return;
  3474. }
  3475. // Do whatever the test dictates
  3476. var i, step, stepList = actionNode.firstChild.childNodes;
  3477. for (i = 0; i < stepList.length; i++) {
  3478. step = stepList[i];
  3479. if (step.nodeType === 1) {doStep(step);}
  3480. }
  3481. }
  3482. // ---------------------------------------------------------------------------
  3483. // AJAX handling
  3484. // ---------------------------------------------------------------------------
  3485. this.ajax = {
  3486. xhr: new window.XMLHttpRequest(),
  3487. handleServerResponse: function () {
  3488. if ((this.readyState === 4) && (this.status === 200)) {
  3489. var id = this.getResponseHeader('ajaxUnit-component');
  3490. switch (id) {
  3491. case 'ajaxunit':
  3492. // Show the test console
  3493. addStyleSheet();
  3494. fillContainer(id, this.responseText);
  3495. break;
  3496. case 'ajaxunit-$actionParse':
  3497. logAppend('Actions received from test controller');
  3498. doActions(this.responseXML);
  3499. break;
  3500. case 'ajaxunit-logmessage':
  3501. logAppend(this.responseText);
  3502. break;
  3503. default:
  3504. logAppend('Response received, but no <em>ajaxUnit-component</em> header.');
  3505. logAppend(this.responseText);
  3506. }
  3507. }
  3508. },
  3509. serverTalk: function (requestType, requestData) {
  3510. var URL = '$parseURL';
  3511. if (requestType === 'POST') {
  3512. this.xhr.open(requestType, URL);
  3513. this.xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
  3514. } else {
  3515. this.xhr.open(requestType, URL + '?' + requestData);
  3516. requestData = '';
  3517. }
  3518. this.xhr.onreadystatechange = this.handleServerResponse;
  3519. this.xhr.setRequestHeader('If-Modified-Since', new Date(0)); // Internet Explorer caching 'feature'
  3520. this.xhr.send(requestData);
  3521. }
  3522. };
  3523. // ---------------------------------------------------------------------------
  3524. // Public methods
  3525. // ---------------------------------------------------------------------------
  3526. this.getResponse = function (action) {
  3527. this.ajax.serverTalk('GET', action);
  3528. };
  3529. this.postResponse = function (appAjax) {
  3530. var postData = '$actionParse';
  3531. postData += '&readyState=' + appAjax.readyState;
  3532. postData += '&status=' + appAjax.status;
  3533. postData += '&responseText=' + encodeURIComponent(appAjax.responseText);
  3534. logAppend('Relaying an XMLHttpRequest response to $parseURL');
  3535. this.ajax.serverTalk('POST', postData);
  3536. };
  3537. this.postString = function(thisString) {
  3538. var appAjax = {readyState: 4, status: 200, responseText: thisString}; // Fake ResponseText object
  3539. this.postResponse(appAjax);
  3540. };
  3541. this.postSelf = function() {this.postString(document.documentElement.innerHTML);};
  3542. this.debug = function(payload) {logAppend(payload);};
  3543. this.click = function (control) {
  3544. var action = control.getAttribute('data-ajaxunit-action'),
  3545. iFrame = document.getElementById('ajaxunit-iframe');
  3546. switch (action) {
  3547. case 'setIFrameSource':
  3548. iFrame.src = document.getElementById('ajaxunit-url').value;
  3549. break;
  3550. case 'post':
  3551. this.postString(iFrame.contentDocument.body.innerHTML);
  3552. }
  3553. return false;
  3554. };
  3555. this.keyUp = function (e) {
  3556. if (!e) {e = window.event;}
  3557. var target = (e.target) ? e.target : e.srcElement;
  3558. // Process Carriage Return and tidy up form
  3559. if (target.form.id === 'ajaxunit-iframe-form' && e.keyCode === 13) {this.click(document.getElementById('ajaxunit-ok'));}
  3560. return false;
  3561. };
  3562. }
  3563. // ---------------------------------------------------------------------------
  3564. // ajaxUnit
  3565. // ---------------------------------------------------------------------------
  3566. // Process results returned from XMLHttpRequest object
  3567. // ---------------------------------------------------------------------------
  3568. function ajaxUnit(payload, debugArgument) {
  3569. var thisAjaxUnit = new C_ajaxUnit(),
  3570. debug = debugArgument || false;
  3571. ajaxUnitInstances.push(thisAjaxUnit);
  3572. if (debug) {
  3573. thisAjaxUnit.debug(payload);
  3574. } else {
  3575. switch (typeof payload) {
  3576. case 'undefined':
  3577. thisAjaxUnit.postSelf();
  3578. break;
  3579. case 'string':
  3580. thisAjaxUnit.postString(payload);
  3581. break;
  3582. default:
  3583. thisAjaxUnit.postResponse(payload);
  3584. }
  3585. }
  3586. }
  3587. GENERATED;
  3588. // Generated code - do not modify in built package
  3589. return $js;
  3590. }
  3591. public static /*.void.*/ function getJavascript() {
  3592. $html = self::htmlJavascript();
  3593. self::sendContent($html, 'Javascript', 'text/javascript');
  3594. }
  3595. public static /*.string.*/ function getIcon($filename = '', $sendToBrowser = true) {
  3596. if (is_file($filename)) {
  3597. $icon = self::getFileContents($filename);
  3598. } else {
  3599. $icon = base64_decode('AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAD+/f4A9/n5AM+SNADerWwAzax4AMuMHADMjBwAwH4KAL97AgD9/vwA/v78APv39wDGlU4AvHYAAPP19QDPkjUA2qFVAMmLIADerGgA+PLlAM+rdAC8dgEA365rANmnXgDHiBkAx6+LAPv+/gD8/v4A/f7+AP7+/gD//v4A9PDsAL99BwDfrmwAy4ocALuXZADQp3AA+vz8AIJqSAD8/PwA+PDkANCZTQB7XTMAfmA7AMSIGgD9//wAm10AAMiKJQDFfAIAv3QAAP7+/wCidSgA//7/ALyHJgD19vUA2q1wAMB6AAD8/foAxq2KALyXZQD6/P0A+fPlAP7//QDi3NQA///9AL2dZQC9dAEAxYAAAOnbxgC9ml0A+PTzAMG1pgD46dgA+vr7AOzd0QDz7eYA1aFZAMKPQgD38+4ArG4LAMWJGQDcp2EA4dW/APfy8QC/fAQAvppmAMWvjgDGiBwA/v/+AMyQNwD68+YAvHQCAP///gC9hCgAqpmDAL53AgDbqF8Af2I4AP39/ADu5toA/v38ALtyAAB9XjMAnFsAAPz//wD9//8A2allAP///wDLix0A4KxlAPz9/QD9/f0As4hIAN6uaADEfgAAx4kbAMmFBQDAcgEAxIMLALqWYwDTmkYAx4QAAMiEAADFgwMAy5I8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa2trQAA/JmYqXmIea2tra2trazQ1UBhXGHMsSWtra2tra2sODTFkChtPFUtoa2tra2trCy5na2trI2VaHWtra2trXGs6U2tra3d1Ex1ra2trax0ZX1sfHWs7Qj1rGmtrax1WMDg4CE5rVTgTHT5ra2snVHBDByAzAUFyKDZYa2trHTlcenZvY3RFeVJ7Rmtra2sdLWwvawkRIgUGXTxra2traxwCfGtrblkPTSVca2tra2sdeClra2tcDD5ra2tra2traRBMa2trK2FHa2tra2trax0XUScyHWpgSmtra2tra2trNxYSAyFtcR1ra2tra2tra2tIFCQERDQda2trawAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='); // Generated code - do not modify in built package
  3600. }
  3601. if ($sendToBrowser) {self::sendContent($icon, 'icon', 'image/x-icon'); return '';} else return $icon;
  3602. }
  3603. // ---------------------------------------------------------------------------
  3604. private static /*.string.*/ function htmlControlPanel($project = '') {
  3605. if ($project !== '') self::setProject($project);
  3606. $actionSuite = self::ACTION_SUITE;
  3607. $actionDummy = self::ACTION_DUMMY;
  3608. $folder = (string) self::getTestContext(self::TAGNAME_FOLDER_TESTS);
  3609. $extension = self::TESTS_EXTENSION;
  3610. $suiteList = "";
  3611. if (!is_dir($folder)) return "<h2>ajaxUnit</h2>\n<p>Test folder <em>$folder</em> can't be found</p>\n";
  3612. foreach (glob($folder.DIRECTORY_SEPARATOR."*.$extension") as $filename) {
  3613. $suite = basename($filename, '.' . self::TESTS_EXTENSION);
  3614. $document = new DOMDocument();
  3615. $document->load($filename);
  3616. $suiteNode = $document->getElementsByTagName(self::ACTION_SUITE)->item(0);
  3617. /*.object.*/ $suiteObject = $suiteNode; // PHPLint-compliant typecasting
  3618. $suiteElement = /*.(DOMElement).*/ $suiteObject; // PHPLint-compliant typecasting
  3619. if ($suiteElement->hasAttribute(self::ATTRNAME_NAME)) {
  3620. $suiteName = $suiteElement->getAttribute(self::ATTRNAME_NAME);
  3621. $suiteList .= <<<HTML
  3622. <input class="ajaxunit ajaxunit-radio" type="radio" name="$actionSuite" value="$suite" /> $suiteName<br />
  3623. HTML;
  3624. }
  3625. }
  3626. if ($suiteList === '') {
  3627. $html = "<h2>ajaxUnit</h2>\n<p>There are are no test suites in the <em>$folder</em> folder</p>\n";
  3628. } else {
  3629. if ($project === '') $project = (string) self::getTestContext(self::TAGNAME_PROJECT);
  3630. // While we're here, let's put a copy of ajaxunit.js in the tests folder
  3631. $js = self::htmlJavascript();
  3632. $filename = $folder.DIRECTORY_SEPARATOR.'ajaxunit.js';
  3633. $result = @file_put_contents($filename, $js);
  3634. $success = (boolean) $result;
  3635. $message = ($success === false) ? "<strong>Couldn't write ajaxUnit javascript to tests folder - might be out of date or missing!</strong>" : 'ajaxUnit javascript written to tests folder';
  3636. $html = <<<HTML
  3637. <h2>ajaxUnit testing for project $project</h2>
  3638. <form class="ajaxunit-form" action="ajaxunit.php" method="get">
  3639. <fieldset class="ajaxunit-fieldset">
  3640. <h3>Choose test suite to run:</h3>
  3641. $suiteList
  3642. </fieldset>
  3643. <fieldset class="ajaxunit-fieldset">
  3644. <input id="ajaxunit-run-tests" value="Run tests"
  3645. type = "submit"
  3646. class = "ajaxunit-button ajaxunit-buttonstate-0"
  3647. />
  3648. <div style="float:left;margin:9px 0 0 8px;">
  3649. <p style="margin:7px 0 0 3px;"><input class="ajaxunit ajaxunit-checkbox" type="checkbox" name = "$actionDummy" value="true" /> Dummy run</p>
  3650. </div>
  3651. </fieldset>
  3652. </form>
  3653. <p class="ajaxunit-footnote">ajaxUnit version 0.17.30 - $message</p>
  3654. HTML;
  3655. }
  3656. return $html;
  3657. }
  3658. public static /*.void.*/ function getControlPanel() {
  3659. $html = self::htmlControlPanel();
  3660. self::sendContent($html);
  3661. }
  3662. // ---------------------------------------------------------------------------
  3663. private static /*.string.*/ function htmlCustomURL() {
  3664. $URL = self::thisURL();
  3665. $content = '';
  3666. // Get some context for this test
  3667. $context = /*.(array[string]string).*/ self::getTestContext();
  3668. // Are we actually running tests at the moment?
  3669. if (!array_key_exists(self::TAGNAME_STATUS, $context) || in_array($context[self::TAGNAME_STATUS], array('', self::STATUS_FINISHED, self::STATUS_FAIL, self::STATUS_FATALERROR))) {
  3670. $content .= 'No testing in progress';
  3671. $testName = '';
  3672. $rubric = '';
  3673. } else {
  3674. // Get some context for this test
  3675. $test = new ajaxUnit_test();
  3676. $testName = $test->name();
  3677. $rubric = $test->rubric();
  3678. }
  3679. $html = <<<HTML
  3680. <!DOCTYPE html>
  3681. <html>
  3682. <head>
  3683. <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
  3684. <title>ajaxUnit Custom URL</title>
  3685. <link type="text/css" rel="stylesheet" href="$URL?css" title="ajaxUnit">
  3686. <script src="$URL?js"></script>
  3687. <script type="text/javascript">var ajaxUnit = new C_ajaxUnit()</script>
  3688. </head>
  3689. <body>
  3690. <div id="ajaxunit">
  3691. <h1>ajaxUnit manual results entry</h1>
  3692. <h2>$testName</h2>
  3693. <p class="ajaxunit-rubric">$rubric<p>
  3694. <br />
  3695. <form id="ajaxunit-iframe-form" class="ajaxunit-form" onsubmit="return false">
  3696. <fieldset class="ajaxunit-fieldset">
  3697. <label for="ajaxunit-url" class="ajaxunit-label">URL:</label>
  3698. <input type="text" id="ajaxunit-url" class="ajaxunit-text" onkeyup = "ajaxUnit.keyUp(event)">
  3699. <input type="button" id="ajaxunit-ok" class="ajaxunit-button" value="Go" data-ajaxunit-action="setIFrameSource" onclick="ajaxUnit.click(this)">
  3700. </fieldset>
  3701. </form>
  3702. </div>
  3703. <iframe width="640" height="480" id="ajaxunit-iframe" class="ajaxunit-iframe"></iframe>
  3704. <form id="ajaxunit-post-form" class="ajaxunit-form" onsubmit="return false">
  3705. <fieldset class="ajaxunit-fieldset">
  3706. <input type="button" id="ajaxunit-post" class="ajaxunit-button" value="Post" data-ajaxunit-action="post" onclick="ajaxUnit.click(this)">
  3707. </fieldset>
  3708. </form>
  3709. <pre>$content</pre>
  3710. </body>
  3711. </html>
  3712. HTML;
  3713. return $html;
  3714. }
  3715. public static /*.void.*/ function getCustomURL() {
  3716. $html = self::htmlCustomURL();
  3717. self::sendContent($html);
  3718. }
  3719. // ---------------------------------------------------------------------------
  3720. private static /*.string.*/ function htmlAddScript() {
  3721. $actionJavascript = self::ACTION_JAVASCRIPT;
  3722. $URL = self::thisURL();
  3723. /* This doesn't work in Webkit
  3724. return <<<HTML
  3725. <script type="text/javascript">
  3726. if (typeof C_ajaxUnit === 'undefined') {
  3727. var ajaxUnit_node = document.createElement('script');
  3728. ajaxUnit_node.type = 'text/javascript';
  3729. ajaxUnit_node.src = '$URL?$actionJavascript';
  3730. document.getElementsByTagName('head')[0].appendChild(ajaxUnit_node);
  3731. }
  3732. </script>
  3733. HTML;
  3734. */
  3735. return <<<HTML
  3736. <script type="text/javascript">
  3737. document.write(unescape('%3Cscript src="$URL?$actionJavascript" type="text/javascript"%3E%3C/script%3E'));
  3738. </script>
  3739. HTML;
  3740. }
  3741. public static /*.void.*/ function addScript() {
  3742. $html = self::htmlAddScript();
  3743. self::sendContent($html, 'addScript');
  3744. }
  3745. // ---------------------------------------------------------------------------
  3746. private static /*.string.*/ function htmlAbout() {
  3747. $php = self::getFileContents('ajaxunit.php', 0, NULL, -1, 4096);
  3748. return self::docBlock_to_HTML($php);
  3749. }
  3750. public static /*.void.*/ function getAbout() {
  3751. $html = self::htmlAbout();
  3752. self::sendContent($html, 'about', 'text/html');
  3753. }
  3754. private static /*.string.*/ function htmlSourceCode() {return (string) highlight_file(__FILE__, 1);}
  3755. public static /*.void.*/ function getSourceCode() {
  3756. $html = self::htmlSourceCode();
  3757. self::sendContent($html, 'sourceCode', 'text/html');
  3758. }
  3759. }
  3760. // End of class ajaxUnit
  3761. // Some code to make this all automagic and a bit RESTful
  3762. // If you want more control over how ajaxunit works then you might need to amend
  3763. // or even remove the code below here
  3764. // Is this script included in another page or is it the HTTP target itself?
  3765. if (basename($_SERVER['PHP_SELF']) === 'ajaxunit.php') {
  3766. // This script has been called directly by the client
  3767. if (is_array($_GET) && (count($_GET) > 0)) {
  3768. $dummyRun = (array_key_exists(ajaxUnit::ACTION_DUMMY, $_GET)) ? (bool) $_GET[ajaxUnit::ACTION_DUMMY] : false;
  3769. if (array_key_exists(ajaxUnit::ACTION_SUITE, $_GET)) ajaxUnit::runTestSuite((string) $_GET[ajaxUnit::ACTION_SUITE], $dummyRun);
  3770. if (array_key_exists(ajaxUnit::ACTION_END, $_GET)) ajaxUnit::endTestSuite((string) $_GET[ajaxUnit::ACTION_END], $dummyRun);
  3771. if (array_key_exists(ajaxUnit::ACTION_PARSE, $_GET)) ajaxUnit::parseResults($dummyRun);
  3772. if (array_key_exists(ajaxUnit::ACTION_CONTROL, $_GET)) ajaxUnit::getControlPanel();
  3773. if (array_key_exists(ajaxUnit::ACTION_CUSTOMURL, $_GET)) ajaxUnit::getCustomURL();
  3774. if (array_key_exists(ajaxUnit::ACTION_JAVASCRIPT, $_GET)) ajaxUnit::getJavascript();
  3775. if (array_key_exists(ajaxUnit::ACTION_CSS, $_GET)) ajaxUnit::getCSS();
  3776. if (array_key_exists(ajaxUnit::ACTION_ICON, $_GET)) ajaxUnit::getIcon();
  3777. if (array_key_exists(ajaxUnit::ACTION_ABOUT, $_GET)) ajaxUnit::getAbout();
  3778. if (array_key_exists(ajaxUnit::ACTION_SOURCECODE, $_GET)) ajaxUnit::getSourceCode();
  3779. if (array_key_exists(ajaxUnit::ACTION_LOGTIDY, $_GET)) ajaxUnit::tidyLogFiles();
  3780. } else {
  3781. if (is_array($_POST) && (count($_POST) > 0)) {
  3782. ajaxUnit::parseResults();
  3783. } else {
  3784. ajaxUnit::addScript();
  3785. }
  3786. }
  3787. }
  3788. ?>