PageRenderTime 41ms CodeModel.GetById 10ms RepoModel.GetById 1ms app.codeStats 0ms

/system/vendor/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php

https://github.com/Toushi/flow
PHP | 302 lines | 192 code | 45 blank | 65 comment | 43 complexity | a2b48ace08b51d6799021158b58f98f0 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. * Locally shared variable references
  9. */
  10. protected $inputTokens, $inputIndex, $outputTokens, $currentNesting,
  11. $currentInjector, $injectors;
  12. public function execute($tokens, $config, $context) {
  13. $definition = $config->getHTMLDefinition();
  14. // local variables
  15. $result = array();
  16. $generator = new HTMLPurifier_Generator($config, $context);
  17. $escape_invalid_tags = $config->get('Core', 'EscapeInvalidTags');
  18. $e = $context->get('ErrorCollector', true);
  19. // member variables
  20. $this->currentNesting = array();
  21. $this->inputIndex = false;
  22. $this->inputTokens =& $tokens;
  23. $this->outputTokens =& $result;
  24. // context variables
  25. $context->register('CurrentNesting', $this->currentNesting);
  26. $context->register('InputIndex', $this->inputIndex);
  27. $context->register('InputTokens', $tokens);
  28. // -- begin INJECTOR --
  29. $this->injectors = array();
  30. $injectors = $config->getBatch('AutoFormat');
  31. $def_injectors = $definition->info_injector;
  32. $custom_injectors = $injectors['Custom'];
  33. unset($injectors['Custom']); // special case
  34. foreach ($injectors as $injector => $b) {
  35. $injector = "HTMLPurifier_Injector_$injector";
  36. if (!$b) continue;
  37. $this->injectors[] = new $injector;
  38. }
  39. foreach ($def_injectors as $injector) {
  40. // assumed to be objects
  41. $this->injectors[] = $injector;
  42. }
  43. foreach ($custom_injectors as $injector) {
  44. if (is_string($injector)) {
  45. $injector = "HTMLPurifier_Injector_$injector";
  46. $injector = new $injector;
  47. }
  48. $this->injectors[] = $injector;
  49. }
  50. // array index of the injector that resulted in an array
  51. // substitution. This enables processTokens() to know which
  52. // injectors are affected by the added tokens and which are
  53. // not (namely, the ones after the current injector are not
  54. // affected)
  55. $this->currentInjector = false;
  56. // give the injectors references to the definition and context
  57. // variables for performance reasons
  58. foreach ($this->injectors as $i => $injector) {
  59. $error = $injector->prepare($config, $context);
  60. if (!$error) continue;
  61. array_splice($this->injectors, $i, 1); // rm the injector
  62. trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
  63. }
  64. // warning: most foreach loops follow the convention $i => $injector.
  65. // Don't define these as loop-wide variables, please!
  66. // -- end INJECTOR --
  67. $token = false;
  68. $context->register('CurrentToken', $token);
  69. // isset is in loop because $tokens size changes during loop exec
  70. for ($this->inputIndex = 0; isset($tokens[$this->inputIndex]); $this->inputIndex++) {
  71. // if all goes well, this token will be passed through unharmed
  72. $token = $tokens[$this->inputIndex];
  73. //printTokens($tokens, $this->inputIndex);
  74. foreach ($this->injectors as $injector) {
  75. if ($injector->skip > 0) $injector->skip--;
  76. }
  77. // quick-check: if it's not a tag, no need to process
  78. if (empty( $token->is_tag )) {
  79. if ($token instanceof HTMLPurifier_Token_Text) {
  80. // injector handler code; duplicated for performance reasons
  81. foreach ($this->injectors as $i => $injector) {
  82. if (!$injector->skip) $injector->handleText($token);
  83. if (is_array($token)) {
  84. $this->currentInjector = $i;
  85. break;
  86. }
  87. }
  88. }
  89. $this->processToken($token, $config, $context);
  90. continue;
  91. }
  92. $info = $definition->info[$token->name]->child;
  93. // quick tag checks: anything that's *not* an end tag
  94. $ok = false;
  95. if ($info->type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
  96. // test if it claims to be a start tag but is empty
  97. $token = new HTMLPurifier_Token_Empty($token->name, $token->attr);
  98. $ok = true;
  99. } elseif ($info->type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
  100. // claims to be empty but really is a start tag
  101. $token = array(
  102. new HTMLPurifier_Token_Start($token->name, $token->attr),
  103. new HTMLPurifier_Token_End($token->name)
  104. );
  105. $ok = true;
  106. } elseif ($token instanceof HTMLPurifier_Token_Empty) {
  107. // real empty token
  108. $ok = true;
  109. } elseif ($token instanceof HTMLPurifier_Token_Start) {
  110. // start tag
  111. // ...unless they also have to close their parent
  112. if (!empty($this->currentNesting)) {
  113. $parent = array_pop($this->currentNesting);
  114. $parent_info = $definition->info[$parent->name];
  115. // this can be replaced with a more general algorithm:
  116. // if the token is not allowed by the parent, auto-close
  117. // the parent
  118. if (!isset($parent_info->child->elements[$token->name])) {
  119. if ($e) $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
  120. // close the parent, then re-loop to reprocess token
  121. $result[] = new HTMLPurifier_Token_End($parent->name);
  122. $this->inputIndex--;
  123. continue;
  124. }
  125. $this->currentNesting[] = $parent; // undo the pop
  126. }
  127. $ok = true;
  128. }
  129. // injector handler code; duplicated for performance reasons
  130. if ($ok) {
  131. foreach ($this->injectors as $i => $injector) {
  132. if (!$injector->skip) $injector->handleElement($token);
  133. if (is_array($token)) {
  134. $this->currentInjector = $i;
  135. break;
  136. }
  137. }
  138. $this->processToken($token, $config, $context);
  139. continue;
  140. }
  141. // sanity check: we should be dealing with a closing tag
  142. if (!$token instanceof HTMLPurifier_Token_End) continue;
  143. // make sure that we have something open
  144. if (empty($this->currentNesting)) {
  145. if ($escape_invalid_tags) {
  146. if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
  147. $result[] = new HTMLPurifier_Token_Text(
  148. $generator->generateFromToken($token)
  149. );
  150. } elseif ($e) {
  151. $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
  152. }
  153. continue;
  154. }
  155. // first, check for the simplest case: everything closes neatly
  156. $current_parent = array_pop($this->currentNesting);
  157. if ($current_parent->name == $token->name) {
  158. $result[] = $token;
  159. foreach ($this->injectors as $i => $injector) {
  160. $injector->notifyEnd($token);
  161. }
  162. continue;
  163. }
  164. // okay, so we're trying to close the wrong tag
  165. // undo the pop previous pop
  166. $this->currentNesting[] = $current_parent;
  167. // scroll back the entire nest, trying to find our tag.
  168. // (feature could be to specify how far you'd like to go)
  169. $size = count($this->currentNesting);
  170. // -2 because -1 is the last element, but we already checked that
  171. $skipped_tags = false;
  172. for ($i = $size - 2; $i >= 0; $i--) {
  173. if ($this->currentNesting[$i]->name == $token->name) {
  174. // current nesting is modified
  175. $skipped_tags = array_splice($this->currentNesting, $i);
  176. break;
  177. }
  178. }
  179. // we still didn't find the tag, so remove
  180. if ($skipped_tags === false) {
  181. if ($escape_invalid_tags) {
  182. $result[] = new HTMLPurifier_Token_Text(
  183. $generator->generateFromToken($token)
  184. );
  185. if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
  186. } elseif ($e) {
  187. $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
  188. }
  189. continue;
  190. }
  191. // okay, we found it, close all the skipped tags
  192. // note that skipped tags contains the element we need closed
  193. for ($i = count($skipped_tags) - 1; $i >= 0; $i--) {
  194. // please don't redefine $i!
  195. if ($i && $e && !isset($skipped_tags[$i]->armor['MakeWellFormed_TagClosedError'])) {
  196. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$i]);
  197. }
  198. $result[] = $new_token = new HTMLPurifier_Token_End($skipped_tags[$i]->name);
  199. foreach ($this->injectors as $injector) {
  200. $injector->notifyEnd($new_token);
  201. }
  202. }
  203. }
  204. $context->destroy('CurrentNesting');
  205. $context->destroy('InputTokens');
  206. $context->destroy('InputIndex');
  207. $context->destroy('CurrentToken');
  208. // we're at the end now, fix all still unclosed tags (this is
  209. // duplicated from the end of the loop with some slight modifications)
  210. // not using $skipped_tags since it would invariably be all of them
  211. if (!empty($this->currentNesting)) {
  212. for ($i = count($this->currentNesting) - 1; $i >= 0; $i--) {
  213. // please don't redefine $i!
  214. if ($e && !isset($this->currentNesting[$i]->armor['MakeWellFormed_TagClosedError'])) {
  215. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $this->currentNesting[$i]);
  216. }
  217. $result[] = $new_token = new HTMLPurifier_Token_End($this->currentNesting[$i]->name);
  218. foreach ($this->injectors as $injector) {
  219. $injector->notifyEnd($new_token);
  220. }
  221. }
  222. }
  223. unset($this->outputTokens, $this->injectors, $this->currentInjector,
  224. $this->currentNesting, $this->inputTokens, $this->inputIndex);
  225. return $result;
  226. }
  227. function processToken($token, $config, $context) {
  228. if (is_array($token)) {
  229. // the original token was overloaded by an injector, time
  230. // to some fancy acrobatics
  231. // $this->inputIndex is decremented so that the entire set gets
  232. // re-processed
  233. array_splice($this->inputTokens, $this->inputIndex--, 1, $token);
  234. // adjust the injector skips based on the array substitution
  235. if ($this->injectors) {
  236. $offset = count($token);
  237. for ($i = 0; $i <= $this->currentInjector; $i++) {
  238. // because of the skip back, we need to add one more
  239. // for uninitialized injectors. I'm not exactly
  240. // sure why this is the case, but I think it has to
  241. // do with the fact that we're decrementing skips
  242. // before re-checking text
  243. if (!$this->injectors[$i]->skip) $this->injectors[$i]->skip++;
  244. $this->injectors[$i]->skip += $offset;
  245. }
  246. }
  247. } elseif ($token) {
  248. // regular case
  249. $this->outputTokens[] = $token;
  250. if ($token instanceof HTMLPurifier_Token_Start) {
  251. $this->currentNesting[] = $token;
  252. } elseif ($token instanceof HTMLPurifier_Token_End) {
  253. array_pop($this->currentNesting); // not actually used
  254. }
  255. }
  256. }
  257. }