PageRenderTime 49ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/external_lib/HTMLPurifier/HTMLPurifier/Strategy/MakeWellFormed.php

https://github.com/modulargaming/kittokittokitto
PHP | 457 lines | 283 code | 59 blank | 115 comment | 79 complexity | 310ba416548021da80413cf04a90cce8 MD5 | raw file
  1. <?php
  2. /**
  3. * Takes tokens makes them well-formed (balance end tags, etc.)
  4. */
  5. class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
  6. {
  7. /**
  8. * Array stream of tokens being processed.
  9. */
  10. protected $tokens;
  11. /**
  12. * Current index in $tokens.
  13. */
  14. protected $t;
  15. /**
  16. * Current nesting of elements.
  17. */
  18. protected $stack;
  19. /**
  20. * Injectors active in this stream processing.
  21. */
  22. protected $injectors;
  23. /**
  24. * Current instance of HTMLPurifier_Config.
  25. */
  26. protected $config;
  27. /**
  28. * Current instance of HTMLPurifier_Context.
  29. */
  30. protected $context;
  31. public function execute($tokens, $config, $context) {
  32. $definition = $config->getHTMLDefinition();
  33. // local variables
  34. $generator = new HTMLPurifier_Generator($config, $context);
  35. $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
  36. $e = $context->get('ErrorCollector', true);
  37. $t = false; // token index
  38. $i = false; // injector index
  39. $token = false; // the current token
  40. $reprocess = false; // whether or not to reprocess the same token
  41. $stack = array();
  42. // member variables
  43. $this->stack =& $stack;
  44. $this->t =& $t;
  45. $this->tokens =& $tokens;
  46. $this->config = $config;
  47. $this->context = $context;
  48. // context variables
  49. $context->register('CurrentNesting', $stack);
  50. $context->register('InputIndex', $t);
  51. $context->register('InputTokens', $tokens);
  52. $context->register('CurrentToken', $token);
  53. // -- begin INJECTOR --
  54. $this->injectors = array();
  55. $injectors = $config->getBatch('AutoFormat');
  56. $def_injectors = $definition->info_injector;
  57. $custom_injectors = $injectors['Custom'];
  58. unset($injectors['Custom']); // special case
  59. foreach ($injectors as $injector => $b) {
  60. // XXX: Fix with a legitimate lookup table of enabled filters
  61. if (strpos($injector, '.') !== false) continue;
  62. $injector = "HTMLPurifier_Injector_$injector";
  63. if (!$b) continue;
  64. $this->injectors[] = new $injector;
  65. }
  66. foreach ($def_injectors as $injector) {
  67. // assumed to be objects
  68. $this->injectors[] = $injector;
  69. }
  70. foreach ($custom_injectors as $injector) {
  71. if (is_string($injector)) {
  72. $injector = "HTMLPurifier_Injector_$injector";
  73. $injector = new $injector;
  74. }
  75. $this->injectors[] = $injector;
  76. }
  77. // give the injectors references to the definition and context
  78. // variables for performance reasons
  79. foreach ($this->injectors as $ix => $injector) {
  80. $error = $injector->prepare($config, $context);
  81. if (!$error) continue;
  82. array_splice($this->injectors, $ix, 1); // rm the injector
  83. trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
  84. }
  85. // -- end INJECTOR --
  86. // a note on punting:
  87. // In order to reduce code duplication, whenever some code needs
  88. // to make HTML changes in order to make things "correct", the
  89. // new HTML gets sent through the purifier, regardless of its
  90. // status. This means that if we add a start token, because it
  91. // was totally necessary, we don't have to update nesting; we just
  92. // punt ($reprocess = true; continue;) and it does that for us.
  93. // isset is in loop because $tokens size changes during loop exec
  94. for (
  95. $t = 0;
  96. $t == 0 || isset($tokens[$t - 1]);
  97. // only increment if we don't need to reprocess
  98. $reprocess ? $reprocess = false : $t++
  99. ) {
  100. // check for a rewind
  101. if (is_int($i) && $i >= 0) {
  102. // possibility: disable rewinding if the current token has a
  103. // rewind set on it already. This would offer protection from
  104. // infinite loop, but might hinder some advanced rewinding.
  105. $rewind_to = $this->injectors[$i]->getRewind();
  106. if (is_int($rewind_to) && $rewind_to < $t) {
  107. if ($rewind_to < 0) $rewind_to = 0;
  108. while ($t > $rewind_to) {
  109. $t--;
  110. $prev = $tokens[$t];
  111. // indicate that other injectors should not process this token,
  112. // but we need to reprocess it
  113. unset($prev->skip[$i]);
  114. $prev->rewind = $i;
  115. if ($prev instanceof HTMLPurifier_Token_Start) array_pop($this->stack);
  116. elseif ($prev instanceof HTMLPurifier_Token_End) $this->stack[] = $prev->start;
  117. }
  118. }
  119. $i = false;
  120. }
  121. // handle case of document end
  122. if (!isset($tokens[$t])) {
  123. // kill processing if stack is empty
  124. if (empty($this->stack)) break;
  125. // peek
  126. $top_nesting = array_pop($this->stack);
  127. $this->stack[] = $top_nesting;
  128. // send error
  129. if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) {
  130. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting);
  131. }
  132. // append, don't splice, since this is the end
  133. $tokens[] = new HTMLPurifier_Token_End($top_nesting->name);
  134. // punt!
  135. $reprocess = true;
  136. continue;
  137. }
  138. $token = $tokens[$t];
  139. //echo '<br>'; printTokens($tokens, $t); printTokens($this->stack);
  140. // quick-check: if it's not a tag, no need to process
  141. if (empty($token->is_tag)) {
  142. if ($token instanceof HTMLPurifier_Token_Text) {
  143. foreach ($this->injectors as $i => $injector) {
  144. if (isset($token->skip[$i])) continue;
  145. if ($token->rewind !== null && $token->rewind !== $i) continue;
  146. $injector->handleText($token);
  147. $this->processToken($token, $i);
  148. $reprocess = true;
  149. break;
  150. }
  151. }
  152. // another possibility is a comment
  153. continue;
  154. }
  155. if (isset($definition->info[$token->name])) {
  156. $type = $definition->info[$token->name]->child->type;
  157. } else {
  158. $type = false; // Type is unknown, treat accordingly
  159. }
  160. // quick tag checks: anything that's *not* an end tag
  161. $ok = false;
  162. if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
  163. // claims to be a start tag but is empty
  164. $token = new HTMLPurifier_Token_Empty($token->name, $token->attr);
  165. $ok = true;
  166. } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
  167. // claims to be empty but really is a start tag
  168. $this->swap(new HTMLPurifier_Token_End($token->name));
  169. $this->insertBefore(new HTMLPurifier_Token_Start($token->name, $token->attr));
  170. // punt (since we had to modify the input stream in a non-trivial way)
  171. $reprocess = true;
  172. continue;
  173. } elseif ($token instanceof HTMLPurifier_Token_Empty) {
  174. // real empty token
  175. $ok = true;
  176. } elseif ($token instanceof HTMLPurifier_Token_Start) {
  177. // start tag
  178. // ...unless they also have to close their parent
  179. if (!empty($this->stack)) {
  180. $parent = array_pop($this->stack);
  181. $this->stack[] = $parent;
  182. if (isset($definition->info[$parent->name])) {
  183. $elements = $definition->info[$parent->name]->child->getAllowedElements($config);
  184. $autoclose = !isset($elements[$token->name]);
  185. } else {
  186. $autoclose = false;
  187. }
  188. $carryover = false;
  189. if ($autoclose && $definition->info[$parent->name]->formatting) {
  190. $carryover = true;
  191. }
  192. if ($autoclose) {
  193. // errors need to be updated
  194. $new_token = new HTMLPurifier_Token_End($parent->name);
  195. $new_token->start = $parent;
  196. if ($carryover) {
  197. $element = clone $parent;
  198. $element->armor['MakeWellFormed_TagClosedError'] = true;
  199. $element->carryover = true;
  200. $this->processToken(array($new_token, $token, $element));
  201. } else {
  202. $this->insertBefore($new_token);
  203. }
  204. if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) {
  205. if (!$carryover) {
  206. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
  207. } else {
  208. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent);
  209. }
  210. }
  211. $reprocess = true;
  212. continue;
  213. }
  214. }
  215. $ok = true;
  216. }
  217. if ($ok) {
  218. foreach ($this->injectors as $i => $injector) {
  219. if (isset($token->skip[$i])) continue;
  220. if ($token->rewind !== null && $token->rewind !== $i) continue;
  221. $injector->handleElement($token);
  222. $this->processToken($token, $i);
  223. $reprocess = true;
  224. break;
  225. }
  226. if (!$reprocess) {
  227. // ah, nothing interesting happened; do normal processing
  228. $this->swap($token);
  229. if ($token instanceof HTMLPurifier_Token_Start) {
  230. $this->stack[] = $token;
  231. } elseif ($token instanceof HTMLPurifier_Token_End) {
  232. throw new HTMLPurifier_Exception('Improper handling of end tag in start code; possible error in MakeWellFormed');
  233. }
  234. }
  235. continue;
  236. }
  237. // sanity check: we should be dealing with a closing tag
  238. if (!$token instanceof HTMLPurifier_Token_End) {
  239. throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier');
  240. }
  241. // make sure that we have something open
  242. if (empty($this->stack)) {
  243. if ($escape_invalid_tags) {
  244. if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
  245. $this->swap(new HTMLPurifier_Token_Text(
  246. $generator->generateFromToken($token)
  247. ));
  248. } else {
  249. $this->remove();
  250. if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
  251. }
  252. $reprocess = true;
  253. continue;
  254. }
  255. // first, check for the simplest case: everything closes neatly.
  256. // Eventually, everything passes through here; if there are problems
  257. // we modify the input stream accordingly and then punt, so that
  258. // the tokens get processed again.
  259. $current_parent = array_pop($this->stack);
  260. if ($current_parent->name == $token->name) {
  261. $token->start = $current_parent;
  262. foreach ($this->injectors as $i => $injector) {
  263. if (isset($token->skip[$i])) continue;
  264. if ($token->rewind !== null && $token->rewind !== $i) continue;
  265. $injector->handleEnd($token);
  266. $this->processToken($token, $i);
  267. $this->stack[] = $current_parent;
  268. $reprocess = true;
  269. break;
  270. }
  271. continue;
  272. }
  273. // okay, so we're trying to close the wrong tag
  274. // undo the pop previous pop
  275. $this->stack[] = $current_parent;
  276. // scroll back the entire nest, trying to find our tag.
  277. // (feature could be to specify how far you'd like to go)
  278. $size = count($this->stack);
  279. // -2 because -1 is the last element, but we already checked that
  280. $skipped_tags = false;
  281. for ($j = $size - 2; $j >= 0; $j--) {
  282. if ($this->stack[$j]->name == $token->name) {
  283. $skipped_tags = array_slice($this->stack, $j);
  284. break;
  285. }
  286. }
  287. // we didn't find the tag, so remove
  288. if ($skipped_tags === false) {
  289. if ($escape_invalid_tags) {
  290. $this->swap(new HTMLPurifier_Token_Text(
  291. $generator->generateFromToken($token)
  292. ));
  293. if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
  294. } else {
  295. $this->remove();
  296. if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
  297. }
  298. $reprocess = true;
  299. continue;
  300. }
  301. // do errors, in REVERSE $j order: a,b,c with </a></b></c>
  302. $c = count($skipped_tags);
  303. if ($e) {
  304. for ($j = $c - 1; $j > 0; $j--) {
  305. // notice we exclude $j == 0, i.e. the current ending tag, from
  306. // the errors...
  307. if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) {
  308. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]);
  309. }
  310. }
  311. }
  312. // insert tags, in FORWARD $j order: c,b,a with </a></b></c>
  313. $replace = array($token);
  314. for ($j = 1; $j < $c; $j++) {
  315. // ...as well as from the insertions
  316. $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name);
  317. $new_token->start = $skipped_tags[$j];
  318. array_unshift($replace, $new_token);
  319. if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) {
  320. $element = clone $skipped_tags[$j];
  321. $element->carryover = true;
  322. $element->armor['MakeWellFormed_TagClosedError'] = true;
  323. $replace[] = $element;
  324. }
  325. }
  326. $this->processToken($replace);
  327. $reprocess = true;
  328. continue;
  329. }
  330. $context->destroy('CurrentNesting');
  331. $context->destroy('InputTokens');
  332. $context->destroy('InputIndex');
  333. $context->destroy('CurrentToken');
  334. unset($this->injectors, $this->stack, $this->tokens, $this->t);
  335. return $tokens;
  336. }
  337. /**
  338. * Processes arbitrary token values for complicated substitution patterns.
  339. * In general:
  340. *
  341. * If $token is an array, it is a list of tokens to substitute for the
  342. * current token. These tokens then get individually processed. If there
  343. * is a leading integer in the list, that integer determines how many
  344. * tokens from the stream should be removed.
  345. *
  346. * If $token is a regular token, it is swapped with the current token.
  347. *
  348. * If $token is false, the current token is deleted.
  349. *
  350. * If $token is an integer, that number of tokens (with the first token
  351. * being the current one) will be deleted.
  352. *
  353. * @param $token Token substitution value
  354. * @param $injector Injector that performed the substitution; default is if
  355. * this is not an injector related operation.
  356. */
  357. protected function processToken($token, $injector = -1) {
  358. // normalize forms of token
  359. if (is_object($token)) $token = array(1, $token);
  360. if (is_int($token)) $token = array($token);
  361. if ($token === false) $token = array(1);
  362. if (!is_array($token)) throw new HTMLPurifier_Exception('Invalid token type from injector');
  363. if (!is_int($token[0])) array_unshift($token, 1);
  364. if ($token[0] === 0) throw new HTMLPurifier_Exception('Deleting zero tokens is not valid');
  365. // $token is now an array with the following form:
  366. // array(number nodes to delete, new node 1, new node 2, ...)
  367. $delete = array_shift($token);
  368. $old = array_splice($this->tokens, $this->t, $delete, $token);
  369. if ($injector > -1) {
  370. // determine appropriate skips
  371. $oldskip = isset($old[0]) ? $old[0]->skip : array();
  372. foreach ($token as $object) {
  373. $object->skip = $oldskip;
  374. $object->skip[$injector] = true;
  375. }
  376. }
  377. }
  378. /**
  379. * Inserts a token before the current token. Cursor now points to this token
  380. */
  381. private function insertBefore($token) {
  382. array_splice($this->tokens, $this->t, 0, array($token));
  383. }
  384. /**
  385. * Removes current token. Cursor now points to new token occupying previously
  386. * occupied space.
  387. */
  388. private function remove() {
  389. array_splice($this->tokens, $this->t, 1);
  390. }
  391. /**
  392. * Swap current token with new token. Cursor points to new token (no change).
  393. */
  394. private function swap($token) {
  395. $this->tokens[$this->t] = $token;
  396. }
  397. }
  398. // vim: et sw=4 sts=4