php_parser.php 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054
  1. <?php
  2. /**
  3. * base include file for SimpleTest
  4. * @package SimpleTest
  5. * @subpackage WebTester
  6. * @version $Id: php_parser.php 1927 2009-07-31 12:45:36Z dgheath $
  7. */
  8. /**#@+
  9. * Lexer mode stack constants
  10. */
  11. foreach (array('LEXER_ENTER', 'LEXER_MATCHED',
  12. 'LEXER_UNMATCHED', 'LEXER_EXIT',
  13. 'LEXER_SPECIAL') as $i => $constant) {
  14. if (! defined($constant)) {
  15. define($constant, $i + 1);
  16. }
  17. }
  18. /**#@-*/
  19. /**
  20. * Compounded regular expression. Any of
  21. * the contained patterns could match and
  22. * when one does, it's label is returned.
  23. * @package SimpleTest
  24. * @subpackage WebTester
  25. */
  26. class ParallelRegex {
  27. private $patterns;
  28. private $labels;
  29. private $regex;
  30. private $case;
  31. /**
  32. * Constructor. Starts with no patterns.
  33. * @param boolean $case True for case sensitive, false
  34. * for insensitive.
  35. * @access public
  36. */
  37. function __construct($case) {
  38. $this->case = $case;
  39. $this->patterns = array();
  40. $this->labels = array();
  41. $this->regex = null;
  42. }
  43. /**
  44. * Adds a pattern with an optional label.
  45. * @param string $pattern Perl style regex, but ( and )
  46. * lose the usual meaning.
  47. * @param string $label Label of regex to be returned
  48. * on a match.
  49. * @access public
  50. */
  51. function addPattern($pattern, $label = true) {
  52. $count = count($this->patterns);
  53. $this->patterns[$count] = $pattern;
  54. $this->labels[$count] = $label;
  55. $this->regex = null;
  56. }
  57. /**
  58. * Attempts to match all patterns at once against
  59. * a string.
  60. * @param string $subject String to match against.
  61. * @param string $match First matched portion of
  62. * subject.
  63. * @return boolean True on success.
  64. * @access public
  65. */
  66. function match($subject, &$match) {
  67. if (count($this->patterns) == 0) {
  68. return false;
  69. }
  70. if (! preg_match($this->getCompoundedRegex(), $subject, $matches)) {
  71. $match = '';
  72. return false;
  73. }
  74. $match = $matches[0];
  75. for ($i = 1; $i < count($matches); $i++) {
  76. if ($matches[$i]) {
  77. return $this->labels[$i - 1];
  78. }
  79. }
  80. return true;
  81. }
  82. /**
  83. * Compounds the patterns into a single
  84. * regular expression separated with the
  85. * "or" operator. Caches the regex.
  86. * Will automatically escape (, ) and / tokens.
  87. * @param array $patterns List of patterns in order.
  88. * @access private
  89. */
  90. protected function getCompoundedRegex() {
  91. if ($this->regex == null) {
  92. for ($i = 0, $count = count($this->patterns); $i < $count; $i++) {
  93. $this->patterns[$i] = '(' . str_replace(
  94. array('/', '(', ')'),
  95. array('\/', '\(', '\)'),
  96. $this->patterns[$i]) . ')';
  97. }
  98. $this->regex = "/" . implode("|", $this->patterns) . "/" . $this->getPerlMatchingFlags();
  99. }
  100. return $this->regex;
  101. }
  102. /**
  103. * Accessor for perl regex mode flags to use.
  104. * @return string Perl regex flags.
  105. * @access private
  106. */
  107. protected function getPerlMatchingFlags() {
  108. return ($this->case ? "msS" : "msSi");
  109. }
  110. }
  111. /**
  112. * States for a stack machine.
  113. * @package SimpleTest
  114. * @subpackage WebTester
  115. */
  116. class SimpleStateStack {
  117. private $stack;
  118. /**
  119. * Constructor. Starts in named state.
  120. * @param string $start Starting state name.
  121. * @access public
  122. */
  123. function __construct($start) {
  124. $this->stack = array($start);
  125. }
  126. /**
  127. * Accessor for current state.
  128. * @return string State.
  129. * @access public
  130. */
  131. function getCurrent() {
  132. return $this->stack[count($this->stack) - 1];
  133. }
  134. /**
  135. * Adds a state to the stack and sets it
  136. * to be the current state.
  137. * @param string $state New state.
  138. * @access public
  139. */
  140. function enter($state) {
  141. array_push($this->stack, $state);
  142. }
  143. /**
  144. * Leaves the current state and reverts
  145. * to the previous one.
  146. * @return boolean False if we drop off
  147. * the bottom of the list.
  148. * @access public
  149. */
  150. function leave() {
  151. if (count($this->stack) == 1) {
  152. return false;
  153. }
  154. array_pop($this->stack);
  155. return true;
  156. }
  157. }
  158. /**
  159. * Accepts text and breaks it into tokens.
  160. * Some optimisation to make the sure the
  161. * content is only scanned by the PHP regex
  162. * parser once. Lexer modes must not start
  163. * with leading underscores.
  164. * @package SimpleTest
  165. * @subpackage WebTester
  166. */
  167. class SimpleLexer {
  168. private $regexes;
  169. private $parser;
  170. private $mode;
  171. private $mode_handlers;
  172. private $case;
  173. /**
  174. * Sets up the lexer in case insensitive matching
  175. * by default.
  176. * @param SimpleSaxParser $parser Handling strategy by
  177. * reference.
  178. * @param string $start Starting handler.
  179. * @param boolean $case True for case sensitive.
  180. * @access public
  181. */
  182. function __construct($parser, $start = "accept", $case = false) {
  183. $this->case = $case;
  184. $this->regexes = array();
  185. $this->parser = $parser;
  186. $this->mode = new SimpleStateStack($start);
  187. $this->mode_handlers = array($start => $start);
  188. }
  189. /**
  190. * Adds a token search pattern for a particular
  191. * parsing mode. The pattern does not change the
  192. * current mode.
  193. * @param string $pattern Perl style regex, but ( and )
  194. * lose the usual meaning.
  195. * @param string $mode Should only apply this
  196. * pattern when dealing with
  197. * this type of input.
  198. * @access public
  199. */
  200. function addPattern($pattern, $mode = "accept") {
  201. if (! isset($this->regexes[$mode])) {
  202. $this->regexes[$mode] = new ParallelRegex($this->case);
  203. }
  204. $this->regexes[$mode]->addPattern($pattern);
  205. if (! isset($this->mode_handlers[$mode])) {
  206. $this->mode_handlers[$mode] = $mode;
  207. }
  208. }
  209. /**
  210. * Adds a pattern that will enter a new parsing
  211. * mode. Useful for entering parenthesis, strings,
  212. * tags, etc.
  213. * @param string $pattern Perl style regex, but ( and )
  214. * lose the usual meaning.
  215. * @param string $mode Should only apply this
  216. * pattern when dealing with
  217. * this type of input.
  218. * @param string $new_mode Change parsing to this new
  219. * nested mode.
  220. * @access public
  221. */
  222. function addEntryPattern($pattern, $mode, $new_mode) {
  223. if (! isset($this->regexes[$mode])) {
  224. $this->regexes[$mode] = new ParallelRegex($this->case);
  225. }
  226. $this->regexes[$mode]->addPattern($pattern, $new_mode);
  227. if (! isset($this->mode_handlers[$new_mode])) {
  228. $this->mode_handlers[$new_mode] = $new_mode;
  229. }
  230. }
  231. /**
  232. * Adds a pattern that will exit the current mode
  233. * and re-enter the previous one.
  234. * @param string $pattern Perl style regex, but ( and )
  235. * lose the usual meaning.
  236. * @param string $mode Mode to leave.
  237. * @access public
  238. */
  239. function addExitPattern($pattern, $mode) {
  240. if (! isset($this->regexes[$mode])) {
  241. $this->regexes[$mode] = new ParallelRegex($this->case);
  242. }
  243. $this->regexes[$mode]->addPattern($pattern, "__exit");
  244. if (! isset($this->mode_handlers[$mode])) {
  245. $this->mode_handlers[$mode] = $mode;
  246. }
  247. }
  248. /**
  249. * Adds a pattern that has a special mode. Acts as an entry
  250. * and exit pattern in one go, effectively calling a special
  251. * parser handler for this token only.
  252. * @param string $pattern Perl style regex, but ( and )
  253. * lose the usual meaning.
  254. * @param string $mode Should only apply this
  255. * pattern when dealing with
  256. * this type of input.
  257. * @param string $special Use this mode for this one token.
  258. * @access public
  259. */
  260. function addSpecialPattern($pattern, $mode, $special) {
  261. if (! isset($this->regexes[$mode])) {
  262. $this->regexes[$mode] = new ParallelRegex($this->case);
  263. }
  264. $this->regexes[$mode]->addPattern($pattern, "_$special");
  265. if (! isset($this->mode_handlers[$special])) {
  266. $this->mode_handlers[$special] = $special;
  267. }
  268. }
  269. /**
  270. * Adds a mapping from a mode to another handler.
  271. * @param string $mode Mode to be remapped.
  272. * @param string $handler New target handler.
  273. * @access public
  274. */
  275. function mapHandler($mode, $handler) {
  276. $this->mode_handlers[$mode] = $handler;
  277. }
  278. /**
  279. * Splits the page text into tokens. Will fail
  280. * if the handlers report an error or if no
  281. * content is consumed. If successful then each
  282. * unparsed and parsed token invokes a call to the
  283. * held listener.
  284. * @param string $raw Raw HTML text.
  285. * @return boolean True on success, else false.
  286. * @access public
  287. */
  288. function parse($raw) {
  289. if (! isset($this->parser)) {
  290. return false;
  291. }
  292. $length = strlen($raw);
  293. while (is_array($parsed = $this->reduce($raw))) {
  294. list($raw, $unmatched, $matched, $mode) = $parsed;
  295. if (! $this->dispatchTokens($unmatched, $matched, $mode)) {
  296. return false;
  297. }
  298. if ($raw === '') {
  299. return true;
  300. }
  301. if (strlen($raw) == $length) {
  302. return false;
  303. }
  304. $length = strlen($raw);
  305. }
  306. if (! $parsed) {
  307. return false;
  308. }
  309. return $this->invokeParser($raw, LEXER_UNMATCHED);
  310. }
  311. /**
  312. * Sends the matched token and any leading unmatched
  313. * text to the parser changing the lexer to a new
  314. * mode if one is listed.
  315. * @param string $unmatched Unmatched leading portion.
  316. * @param string $matched Actual token match.
  317. * @param string $mode Mode after match. A boolean
  318. * false mode causes no change.
  319. * @return boolean False if there was any error
  320. * from the parser.
  321. * @access private
  322. */
  323. protected function dispatchTokens($unmatched, $matched, $mode = false) {
  324. if (! $this->invokeParser($unmatched, LEXER_UNMATCHED)) {
  325. return false;
  326. }
  327. if (is_bool($mode)) {
  328. return $this->invokeParser($matched, LEXER_MATCHED);
  329. }
  330. if ($this->isModeEnd($mode)) {
  331. if (! $this->invokeParser($matched, LEXER_EXIT)) {
  332. return false;
  333. }
  334. return $this->mode->leave();
  335. }
  336. if ($this->isSpecialMode($mode)) {
  337. $this->mode->enter($this->decodeSpecial($mode));
  338. if (! $this->invokeParser($matched, LEXER_SPECIAL)) {
  339. return false;
  340. }
  341. return $this->mode->leave();
  342. }
  343. $this->mode->enter($mode);
  344. return $this->invokeParser($matched, LEXER_ENTER);
  345. }
  346. /**
  347. * Tests to see if the new mode is actually to leave
  348. * the current mode and pop an item from the matching
  349. * mode stack.
  350. * @param string $mode Mode to test.
  351. * @return boolean True if this is the exit mode.
  352. * @access private
  353. */
  354. protected function isModeEnd($mode) {
  355. return ($mode === "__exit");
  356. }
  357. /**
  358. * Test to see if the mode is one where this mode
  359. * is entered for this token only and automatically
  360. * leaves immediately afterwoods.
  361. * @param string $mode Mode to test.
  362. * @return boolean True if this is the exit mode.
  363. * @access private
  364. */
  365. protected function isSpecialMode($mode) {
  366. return (strncmp($mode, "_", 1) == 0);
  367. }
  368. /**
  369. * Strips the magic underscore marking single token
  370. * modes.
  371. * @param string $mode Mode to decode.
  372. * @return string Underlying mode name.
  373. * @access private
  374. */
  375. protected function decodeSpecial($mode) {
  376. return substr($mode, 1);
  377. }
  378. /**
  379. * Calls the parser method named after the current
  380. * mode. Empty content will be ignored. The lexer
  381. * has a parser handler for each mode in the lexer.
  382. * @param string $content Text parsed.
  383. * @param boolean $is_match Token is recognised rather
  384. * than unparsed data.
  385. * @access private
  386. */
  387. protected function invokeParser($content, $is_match) {
  388. if (($content === '') || ($content === false)) {
  389. return true;
  390. }
  391. $handler = $this->mode_handlers[$this->mode->getCurrent()];
  392. return $this->parser->$handler($content, $is_match);
  393. }
  394. /**
  395. * Tries to match a chunk of text and if successful
  396. * removes the recognised chunk and any leading
  397. * unparsed data. Empty strings will not be matched.
  398. * @param string $raw The subject to parse. This is the
  399. * content that will be eaten.
  400. * @return array/boolean Three item list of unparsed
  401. * content followed by the
  402. * recognised token and finally the
  403. * action the parser is to take.
  404. * True if no match, false if there
  405. * is a parsing error.
  406. * @access private
  407. */
  408. protected function reduce($raw) {
  409. if ($action = $this->regexes[$this->mode->getCurrent()]->match($raw, $match)) {
  410. $unparsed_character_count = strpos($raw, $match);
  411. $unparsed = substr($raw, 0, $unparsed_character_count);
  412. $raw = substr($raw, $unparsed_character_count + strlen($match));
  413. return array($raw, $unparsed, $match, $action);
  414. }
  415. return true;
  416. }
  417. }
  418. /**
  419. * Breaks HTML into SAX events.
  420. * @package SimpleTest
  421. * @subpackage WebTester
  422. */
  423. class SimpleHtmlLexer extends SimpleLexer {
  424. /**
  425. * Sets up the lexer with case insensitive matching
  426. * and adds the HTML handlers.
  427. * @param SimpleSaxParser $parser Handling strategy by
  428. * reference.
  429. * @access public
  430. */
  431. function __construct($parser) {
  432. parent::__construct($parser, 'text');
  433. $this->mapHandler('text', 'acceptTextToken');
  434. $this->addSkipping();
  435. foreach ($this->getParsedTags() as $tag) {
  436. $this->addTag($tag);
  437. }
  438. $this->addInTagTokens();
  439. }
  440. /**
  441. * List of parsed tags. Others are ignored.
  442. * @return array List of searched for tags.
  443. * @access private
  444. */
  445. protected function getParsedTags() {
  446. return array('a', 'base', 'title', 'form', 'input', 'button', 'textarea', 'select',
  447. 'option', 'frameset', 'frame', 'label');
  448. }
  449. /**
  450. * The lexer has to skip certain sections such
  451. * as server code, client code and styles.
  452. * @access private
  453. */
  454. protected function addSkipping() {
  455. $this->mapHandler('css', 'ignore');
  456. $this->addEntryPattern('<style', 'text', 'css');
  457. $this->addExitPattern('</style>', 'css');
  458. $this->mapHandler('js', 'ignore');
  459. $this->addEntryPattern('<script', 'text', 'js');
  460. $this->addExitPattern('</script>', 'js');
  461. $this->mapHandler('comment', 'ignore');
  462. $this->addEntryPattern('<!--', 'text', 'comment');
  463. $this->addExitPattern('-->', 'comment');
  464. }
  465. /**
  466. * Pattern matches to start and end a tag.
  467. * @param string $tag Name of tag to scan for.
  468. * @access private
  469. */
  470. protected function addTag($tag) {
  471. $this->addSpecialPattern("</$tag>", 'text', 'acceptEndToken');
  472. $this->addEntryPattern("<$tag", 'text', 'tag');
  473. }
  474. /**
  475. * Pattern matches to parse the inside of a tag
  476. * including the attributes and their quoting.
  477. * @access private
  478. */
  479. protected function addInTagTokens() {
  480. $this->mapHandler('tag', 'acceptStartToken');
  481. $this->addSpecialPattern('\s+', 'tag', 'ignore');
  482. $this->addAttributeTokens();
  483. $this->addExitPattern('/>', 'tag');
  484. $this->addExitPattern('>', 'tag');
  485. }
  486. /**
  487. * Matches attributes that are either single quoted,
  488. * double quoted or unquoted.
  489. * @access private
  490. */
  491. protected function addAttributeTokens() {
  492. $this->mapHandler('dq_attribute', 'acceptAttributeToken');
  493. $this->addEntryPattern('=\s*"', 'tag', 'dq_attribute');
  494. $this->addPattern("\\\\\"", 'dq_attribute');
  495. $this->addExitPattern('"', 'dq_attribute');
  496. $this->mapHandler('sq_attribute', 'acceptAttributeToken');
  497. $this->addEntryPattern("=\s*'", 'tag', 'sq_attribute');
  498. $this->addPattern("\\\\'", 'sq_attribute');
  499. $this->addExitPattern("'", 'sq_attribute');
  500. $this->mapHandler('uq_attribute', 'acceptAttributeToken');
  501. $this->addSpecialPattern('=\s*[^>\s]*', 'tag', 'uq_attribute');
  502. }
  503. }
  504. /**
  505. * Converts HTML tokens into selected SAX events.
  506. * @package SimpleTest
  507. * @subpackage WebTester
  508. */
  509. class SimpleHtmlSaxParser {
  510. private $lexer;
  511. private $listener;
  512. private $tag;
  513. private $attributes;
  514. private $current_attribute;
  515. /**
  516. * Sets the listener.
  517. * @param SimplePhpPageBuilder $listener SAX event handler.
  518. * @access public
  519. */
  520. function __construct($listener) {
  521. $this->listener = $listener;
  522. $this->lexer = $this->createLexer($this);
  523. $this->tag = '';
  524. $this->attributes = array();
  525. $this->current_attribute = '';
  526. }
  527. /**
  528. * Runs the content through the lexer which
  529. * should call back to the acceptors.
  530. * @param string $raw Page text to parse.
  531. * @return boolean False if parse error.
  532. * @access public
  533. */
  534. function parse($raw) {
  535. return $this->lexer->parse($raw);
  536. }
  537. /**
  538. * Sets up the matching lexer. Starts in 'text' mode.
  539. * @param SimpleSaxParser $parser Event generator, usually $self.
  540. * @return SimpleLexer Lexer suitable for this parser.
  541. * @access public
  542. */
  543. static function createLexer(&$parser) {
  544. return new SimpleHtmlLexer($parser);
  545. }
  546. /**
  547. * Accepts a token from the tag mode. If the
  548. * starting element completes then the element
  549. * is dispatched and the current attributes
  550. * set back to empty. The element or attribute
  551. * name is converted to lower case.
  552. * @param string $token Incoming characters.
  553. * @param integer $event Lexer event type.
  554. * @return boolean False if parse error.
  555. * @access public
  556. */
  557. function acceptStartToken($token, $event) {
  558. if ($event == LEXER_ENTER) {
  559. $this->tag = strtolower(substr($token, 1));
  560. return true;
  561. }
  562. if ($event == LEXER_EXIT) {
  563. $success = $this->listener->startElement(
  564. $this->tag,
  565. $this->attributes);
  566. $this->tag = '';
  567. $this->attributes = array();
  568. return $success;
  569. }
  570. if ($token != '=') {
  571. $this->current_attribute = strtolower(html_entity_decode($token, ENT_QUOTES));
  572. $this->attributes[$this->current_attribute] = '';
  573. }
  574. return true;
  575. }
  576. /**
  577. * Accepts a token from the end tag mode.
  578. * The element name is converted to lower case.
  579. * @param string $token Incoming characters.
  580. * @param integer $event Lexer event type.
  581. * @return boolean False if parse error.
  582. * @access public
  583. */
  584. function acceptEndToken($token, $event) {
  585. if (! preg_match('/<\/(.*)>/', $token, $matches)) {
  586. return false;
  587. }
  588. return $this->listener->endElement(strtolower($matches[1]));
  589. }
  590. /**
  591. * Part of the tag data.
  592. * @param string $token Incoming characters.
  593. * @param integer $event Lexer event type.
  594. * @return boolean False if parse error.
  595. * @access public
  596. */
  597. function acceptAttributeToken($token, $event) {
  598. if ($this->current_attribute) {
  599. if ($event == LEXER_UNMATCHED) {
  600. $this->attributes[$this->current_attribute] .=
  601. html_entity_decode($token, ENT_QUOTES);
  602. }
  603. if ($event == LEXER_SPECIAL) {
  604. $this->attributes[$this->current_attribute] .=
  605. preg_replace('/^=\s*/' , '', html_entity_decode($token, ENT_QUOTES));
  606. }
  607. }
  608. return true;
  609. }
  610. /**
  611. * A character entity.
  612. * @param string $token Incoming characters.
  613. * @param integer $event Lexer event type.
  614. * @return boolean False if parse error.
  615. * @access public
  616. */
  617. function acceptEntityToken($token, $event) {
  618. }
  619. /**
  620. * Character data between tags regarded as
  621. * important.
  622. * @param string $token Incoming characters.
  623. * @param integer $event Lexer event type.
  624. * @return boolean False if parse error.
  625. * @access public
  626. */
  627. function acceptTextToken($token, $event) {
  628. return $this->listener->addContent($token);
  629. }
  630. /**
  631. * Incoming data to be ignored.
  632. * @param string $token Incoming characters.
  633. * @param integer $event Lexer event type.
  634. * @return boolean False if parse error.
  635. * @access public
  636. */
  637. function ignore($token, $event) {
  638. return true;
  639. }
  640. }
  641. /**
  642. * SAX event handler. Maintains a list of
  643. * open tags and dispatches them as they close.
  644. * @package SimpleTest
  645. * @subpackage WebTester
  646. */
  647. class SimplePhpPageBuilder {
  648. private $tags;
  649. private $page;
  650. private $private_content_tag;
  651. private $open_forms = array();
  652. private $complete_forms = array();
  653. private $frameset = false;
  654. private $loading_frames = array();
  655. private $frameset_nesting_level = 0;
  656. private $left_over_labels = array();
  657. /**
  658. * Frees up any references so as to allow the PHP garbage
  659. * collection from unset() to work.
  660. * @access public
  661. */
  662. function free() {
  663. unset($this->tags);
  664. unset($this->page);
  665. unset($this->private_content_tags);
  666. $this->open_forms = array();
  667. $this->complete_forms = array();
  668. $this->frameset = false;
  669. $this->loading_frames = array();
  670. $this->frameset_nesting_level = 0;
  671. $this->left_over_labels = array();
  672. }
  673. /**
  674. * This builder is always available.
  675. * @return boolean Always true.
  676. */
  677. function can() {
  678. return true;
  679. }
  680. /**
  681. * Reads the raw content and send events
  682. * into the page to be built.
  683. * @param $response SimpleHttpResponse Fetched response.
  684. * @return SimplePage Newly parsed page.
  685. * @access public
  686. */
  687. function parse($response) {
  688. $this->tags = array();
  689. $this->page = $this->createPage($response);
  690. $parser = $this->createParser($this);
  691. $parser->parse($response->getContent());
  692. $this->acceptPageEnd();
  693. $page = $this->page;
  694. $this->free();
  695. return $page;
  696. }
  697. /**
  698. * Creates an empty page.
  699. * @return SimplePage New unparsed page.
  700. * @access protected
  701. */
  702. protected function createPage($response) {
  703. return new SimplePage($response);
  704. }
  705. /**
  706. * Creates the parser used with the builder.
  707. * @param SimplePhpPageBuilder $listener Target of parser.
  708. * @return SimpleSaxParser Parser to generate
  709. * events for the builder.
  710. * @access protected
  711. */
  712. protected function createParser(&$listener) {
  713. return new SimpleHtmlSaxParser($listener);
  714. }
  715. /**
  716. * Start of element event. Opens a new tag.
  717. * @param string $name Element name.
  718. * @param hash $attributes Attributes without content
  719. * are marked as true.
  720. * @return boolean False on parse error.
  721. * @access public
  722. */
  723. function startElement($name, $attributes) {
  724. $factory = new SimpleTagBuilder();
  725. $tag = $factory->createTag($name, $attributes);
  726. if (! $tag) {
  727. return true;
  728. }
  729. if ($tag->getTagName() == 'label') {
  730. $this->acceptLabelStart($tag);
  731. $this->openTag($tag);
  732. return true;
  733. }
  734. if ($tag->getTagName() == 'form') {
  735. $this->acceptFormStart($tag);
  736. return true;
  737. }
  738. if ($tag->getTagName() == 'frameset') {
  739. $this->acceptFramesetStart($tag);
  740. return true;
  741. }
  742. if ($tag->getTagName() == 'frame') {
  743. $this->acceptFrame($tag);
  744. return true;
  745. }
  746. if ($tag->isPrivateContent() && ! isset($this->private_content_tag)) {
  747. $this->private_content_tag = &$tag;
  748. }
  749. if ($tag->expectEndTag()) {
  750. $this->openTag($tag);
  751. return true;
  752. }
  753. $this->acceptTag($tag);
  754. return true;
  755. }
  756. /**
  757. * End of element event.
  758. * @param string $name Element name.
  759. * @return boolean False on parse error.
  760. * @access public
  761. */
  762. function endElement($name) {
  763. if ($name == 'label') {
  764. $this->acceptLabelEnd();
  765. return true;
  766. }
  767. if ($name == 'form') {
  768. $this->acceptFormEnd();
  769. return true;
  770. }
  771. if ($name == 'frameset') {
  772. $this->acceptFramesetEnd();
  773. return true;
  774. }
  775. if ($this->hasNamedTagOnOpenTagStack($name)) {
  776. $tag = array_pop($this->tags[$name]);
  777. if ($tag->isPrivateContent() && $this->private_content_tag->getTagName() == $name) {
  778. unset($this->private_content_tag);
  779. }
  780. $this->addContentTagToOpenTags($tag);
  781. $this->acceptTag($tag);
  782. return true;
  783. }
  784. return true;
  785. }
  786. /**
  787. * Test to see if there are any open tags awaiting
  788. * closure that match the tag name.
  789. * @param string $name Element name.
  790. * @return boolean True if any are still open.
  791. * @access private
  792. */
  793. protected function hasNamedTagOnOpenTagStack($name) {
  794. return isset($this->tags[$name]) && (count($this->tags[$name]) > 0);
  795. }
  796. /**
  797. * Unparsed, but relevant data. The data is added
  798. * to every open tag.
  799. * @param string $text May include unparsed tags.
  800. * @return boolean False on parse error.
  801. * @access public
  802. */
  803. function addContent($text) {
  804. if (isset($this->private_content_tag)) {
  805. $this->private_content_tag->addContent($text);
  806. } else {
  807. $this->addContentToAllOpenTags($text);
  808. }
  809. return true;
  810. }
  811. /**
  812. * Any content fills all currently open tags unless it
  813. * is part of an option tag.
  814. * @param string $text May include unparsed tags.
  815. * @access private
  816. */
  817. protected function addContentToAllOpenTags($text) {
  818. foreach (array_keys($this->tags) as $name) {
  819. for ($i = 0, $count = count($this->tags[$name]); $i < $count; $i++) {
  820. $this->tags[$name][$i]->addContent($text);
  821. }
  822. }
  823. }
  824. /**
  825. * Parsed data in tag form. The parsed tag is added
  826. * to every open tag. Used for adding options to select
  827. * fields only.
  828. * @param SimpleTag $tag Option tags only.
  829. * @access private
  830. */
  831. protected function addContentTagToOpenTags(&$tag) {
  832. if ($tag->getTagName() != 'option') {
  833. return;
  834. }
  835. foreach (array_keys($this->tags) as $name) {
  836. for ($i = 0, $count = count($this->tags[$name]); $i < $count; $i++) {
  837. $this->tags[$name][$i]->addTag($tag);
  838. }
  839. }
  840. }
  841. /**
  842. * Opens a tag for receiving content. Multiple tags
  843. * will be receiving input at the same time.
  844. * @param SimpleTag $tag New content tag.
  845. * @access private
  846. */
  847. protected function openTag($tag) {
  848. $name = $tag->getTagName();
  849. if (! in_array($name, array_keys($this->tags))) {
  850. $this->tags[$name] = array();
  851. }
  852. $this->tags[$name][] = $tag;
  853. }
  854. /**
  855. * Adds a tag to the page.
  856. * @param SimpleTag $tag Tag to accept.
  857. * @access public
  858. */
  859. protected function acceptTag($tag) {
  860. if ($tag->getTagName() == "a") {
  861. $this->page->addLink($tag);
  862. } elseif ($tag->getTagName() == "base") {
  863. $this->page->setBase($tag->getAttribute('href'));
  864. } elseif ($tag->getTagName() == "title") {
  865. $this->page->setTitle($tag);
  866. } elseif ($this->isFormElement($tag->getTagName())) {
  867. for ($i = 0; $i < count($this->open_forms); $i++) {
  868. $this->open_forms[$i]->addWidget($tag);
  869. }
  870. $this->last_widget = $tag;
  871. }
  872. }
  873. /**
  874. * Opens a label for a described widget.
  875. * @param SimpleFormTag $tag Tag to accept.
  876. * @access public
  877. */
  878. protected function acceptLabelStart($tag) {
  879. $this->label = $tag;
  880. unset($this->last_widget);
  881. }
  882. /**
  883. * Closes the most recently opened label.
  884. * @access public
  885. */
  886. protected function acceptLabelEnd() {
  887. if (isset($this->label)) {
  888. if (isset($this->last_widget)) {
  889. $this->last_widget->setLabel($this->label->getText());
  890. unset($this->last_widget);
  891. } else {
  892. $this->left_over_labels[] = SimpleTestCompatibility::copy($this->label);
  893. }
  894. unset($this->label);
  895. }
  896. }
  897. /**
  898. * Tests to see if a tag is a possible form
  899. * element.
  900. * @param string $name HTML element name.
  901. * @return boolean True if form element.
  902. * @access private
  903. */
  904. protected function isFormElement($name) {
  905. return in_array($name, array('input', 'button', 'textarea', 'select'));
  906. }
  907. /**
  908. * Opens a form. New widgets go here.
  909. * @param SimpleFormTag $tag Tag to accept.
  910. * @access public
  911. */
  912. protected function acceptFormStart($tag) {
  913. $this->open_forms[] = new SimpleForm($tag, $this->page);
  914. }
  915. /**
  916. * Closes the most recently opened form.
  917. * @access public
  918. */
  919. protected function acceptFormEnd() {
  920. if (count($this->open_forms)) {
  921. $this->complete_forms[] = array_pop($this->open_forms);
  922. }
  923. }
  924. /**
  925. * Opens a frameset. A frameset may contain nested
  926. * frameset tags.
  927. * @param SimpleFramesetTag $tag Tag to accept.
  928. * @access public
  929. */
  930. protected function acceptFramesetStart($tag) {
  931. if (! $this->isLoadingFrames()) {
  932. $this->frameset = $tag;
  933. }
  934. $this->frameset_nesting_level++;
  935. }
  936. /**
  937. * Closes the most recently opened frameset.
  938. * @access public
  939. */
  940. protected function acceptFramesetEnd() {
  941. if ($this->isLoadingFrames()) {
  942. $this->frameset_nesting_level--;
  943. }
  944. }
  945. /**
  946. * Takes a single frame tag and stashes it in
  947. * the current frame set.
  948. * @param SimpleFrameTag $tag Tag to accept.
  949. * @access public
  950. */
  951. protected function acceptFrame($tag) {
  952. if ($this->isLoadingFrames()) {
  953. if ($tag->getAttribute('src')) {
  954. $this->loading_frames[] = $tag;
  955. }
  956. }
  957. }
  958. /**
  959. * Test to see if in the middle of reading
  960. * a frameset.
  961. * @return boolean True if inframeset.
  962. * @access private
  963. */
  964. protected function isLoadingFrames() {
  965. return $this->frameset and $this->frameset_nesting_level > 0;
  966. }
  967. /**
  968. * Marker for end of complete page. Any work in
  969. * progress can now be closed.
  970. * @access public
  971. */
  972. protected function acceptPageEnd() {
  973. while (count($this->open_forms)) {
  974. $this->complete_forms[] = array_pop($this->open_forms);
  975. }
  976. foreach ($this->left_over_labels as $label) {
  977. for ($i = 0, $count = count($this->complete_forms); $i < $count; $i++) {
  978. $this->complete_forms[$i]->attachLabelBySelector(
  979. new SimpleById($label->getFor()),
  980. $label->getText());
  981. }
  982. }
  983. $this->page->setForms($this->complete_forms);
  984. $this->page->setFrames($this->loading_frames);
  985. }
  986. }
  987. ?>