PageRenderTime 55ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/tests/WidgetTest/EscapeTest.php

https://github.com/putersham/widget
PHP | 492 lines | 378 code | 42 blank | 72 comment | 32 complexity | d5bf3dce1a5ba2156d8006ab7a1e531c MD5 | raw file
  1. <?php
  2. /**
  3. * Zend Framework (http://framework.zend.com/)
  4. *
  5. * @link http://github.com/zendframework/zf2 for the canonical source repository
  6. * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
  7. * @license http://framework.zend.com/license/new-bsd New BSD License
  8. */
  9. namespace WidgetTest;
  10. use Widget\Escape as Escaper;
  11. /**
  12. * The test case derived from https://raw.github.com/zendframework/zf2/master/tests/ZendTest/Escaper/EscaperTest.php
  13. */
  14. class EscapeTest extends TestCase
  15. {
  16. /**
  17. * All character encodings supported by htmlspecialchars()
  18. */
  19. protected $supportedEncodings = array(
  20. 'iso-8859-1', 'iso8859-1', 'iso-8859-5', 'iso8859-5',
  21. 'iso-8859-15', 'iso8859-15', 'utf-8', 'cp866',
  22. 'ibm866', '866', 'cp1251', 'windows-1251',
  23. 'win-1251', '1251', 'cp1252', 'windows-1252',
  24. '1252', 'koi8-r', 'koi8-ru', 'koi8r',
  25. 'big5', '950', 'gb2312', '936',
  26. 'big5-hkscs', 'shift_jis', 'sjis', 'sjis-win',
  27. 'cp932', '932', 'euc-jp', 'eucjp',
  28. 'eucjp-win', 'macroman'
  29. );
  30. protected $htmlSpecialChars = array(
  31. '\'' => '&#039;',
  32. '"' => '&quot;',
  33. '<' => '&lt;',
  34. '>' => '&gt;',
  35. '&' => '&amp;'
  36. );
  37. protected $htmlAttrSpecialChars = array(
  38. '\'' => '&#x27;',
  39. '"' => '&quot;',
  40. '<' => '&lt;',
  41. '>' => '&gt;',
  42. '&' => '&amp;',
  43. /* Characters beyond ASCII value 255 to unicode escape */
  44. 'Ā' => '&#x0100;',
  45. /* Immune chars excluded */
  46. ',' => ',',
  47. '.' => '.',
  48. '-' => '-',
  49. '_' => '_',
  50. /* Basic alnums exluded */
  51. 'a' => 'a',
  52. 'A' => 'A',
  53. 'z' => 'z',
  54. 'Z' => 'Z',
  55. '0' => '0',
  56. '9' => '9',
  57. /* Basic control characters and null */
  58. "\r" => '&#x0D;',
  59. "\n" => '&#x0A;',
  60. "\t" => '&#x09;',
  61. "\0" => '&#xFFFD;', // should use Unicode replacement char
  62. /* Encode chars as named entities where possible */
  63. '<' => '&lt;',
  64. '>' => '&gt;',
  65. '&' => '&amp;',
  66. '"' => '&quot;',
  67. /* Encode spaces for quoteless attribute protection */
  68. ' ' => '&#x20;',
  69. );
  70. protected $jsSpecialChars = array(
  71. /* HTML special chars - escape without exception to hex */
  72. '<' => '\\x3C',
  73. '>' => '\\x3E',
  74. '\'' => '\\x27',
  75. '"' => '\\x22',
  76. '&' => '\\x26',
  77. /* Characters beyond ASCII value 255 to unicode escape */
  78. 'Ā' => '\\u0100',
  79. /* Immune chars excluded */
  80. ',' => ',',
  81. '.' => '.',
  82. '_' => '_',
  83. /* Basic alnums exluded */
  84. 'a' => 'a',
  85. 'A' => 'A',
  86. 'z' => 'z',
  87. 'Z' => 'Z',
  88. '0' => '0',
  89. '9' => '9',
  90. /* Basic control characters and null */
  91. "\r" => '\\x0D',
  92. "\n" => '\\x0A',
  93. "\t" => '\\x09',
  94. "\0" => '\\x00',
  95. /* Encode spaces for quoteless attribute protection */
  96. ' ' => '\\x20',
  97. );
  98. protected $urlSpecialChars = array(
  99. /* HTML special chars - escape without exception to percent encoding */
  100. '<' => '%3C',
  101. '>' => '%3E',
  102. '\'' => '%27',
  103. '"' => '%22',
  104. '&' => '%26',
  105. /* Characters beyond ASCII value 255 to hex sequence */
  106. 'Ā' => '%C4%80',
  107. /* Punctuation and unreserved check */
  108. ',' => '%2C',
  109. '.' => '.',
  110. '_' => '_',
  111. '-' => '-',
  112. ':' => '%3A',
  113. ';' => '%3B',
  114. '!' => '%21',
  115. /* Basic alnums excluded */
  116. 'a' => 'a',
  117. 'A' => 'A',
  118. 'z' => 'z',
  119. 'Z' => 'Z',
  120. '0' => '0',
  121. '9' => '9',
  122. /* Basic control characters and null */
  123. "\r" => '%0D',
  124. "\n" => '%0A',
  125. "\t" => '%09',
  126. "\0" => '%00',
  127. /* PHP quirks from the past */
  128. ' ' => '%20',
  129. '~' => '~',
  130. '+' => '%2B',
  131. );
  132. protected $cssSpecialChars = array(
  133. /* HTML special chars - escape without exception to hex */
  134. '<' => '\\3C ',
  135. '>' => '\\3E ',
  136. '\'' => '\\27 ',
  137. '"' => '\\22 ',
  138. '&' => '\\26 ',
  139. /* Characters beyond ASCII value 255 to unicode escape */
  140. 'Ā' => '\\100 ',
  141. /* Immune chars excluded */
  142. ',' => '\\2C ',
  143. '.' => '\\2E ',
  144. '_' => '\\5F ',
  145. /* Basic alnums exluded */
  146. 'a' => 'a',
  147. 'A' => 'A',
  148. 'z' => 'z',
  149. 'Z' => 'Z',
  150. '0' => '0',
  151. '9' => '9',
  152. /* Basic control characters and null */
  153. "\r" => '\\D ',
  154. "\n" => '\\A ',
  155. "\t" => '\\9 ',
  156. "\0" => '\\0 ',
  157. /* Encode spaces for quoteless attribute protection */
  158. ' ' => '\\20 ',
  159. );
  160. public function setUp()
  161. {
  162. $this->escaper = new Escaper(array(
  163. 'encoding' => 'utf-8'
  164. ));
  165. }
  166. /**
  167. * @expectedException \InvalidArgumentException
  168. */
  169. public function testSettingEncodingToEmptyStringShouldThrowException()
  170. {
  171. $escaper = new Escaper(array(
  172. 'encoding' => ''
  173. ));
  174. }
  175. public function testSettingValidEncodingShouldNotThrowExceptions()
  176. {
  177. foreach ($this->supportedEncodings as $value) {
  178. $escaper = new Escaper(array(
  179. 'encoding' => $value
  180. ));
  181. }
  182. }
  183. /**
  184. * @expectedException \InvalidArgumentException
  185. */
  186. public function testSettingEncodingToInvalidValueShouldThrowException()
  187. {
  188. $escaper = new Escaper(array(
  189. 'encoding' => 'invalid-encoding'
  190. ));
  191. }
  192. public function testReturnsEncodingFromGetter()
  193. {
  194. $this->assertEquals('utf-8', $this->escaper->getEncoding());
  195. }
  196. public function testHtmlEscapingConvertsSpecialChars()
  197. {
  198. foreach ($this->htmlSpecialChars as $key => $value) {
  199. $this->assertEquals(
  200. $value,
  201. $this->escaper->escapeHtml($key),
  202. 'Failed to escape: ' . $key
  203. );
  204. // call by alias
  205. $this->assertEquals(
  206. $value,
  207. $this->escaper->html($key)
  208. );
  209. // call by widget
  210. $this->assertEquals(
  211. $value,
  212. $this->escape($key, 'html')
  213. );
  214. }
  215. }
  216. public function testHtmlAttributeEscapingConvertsSpecialChars()
  217. {
  218. foreach ($this->htmlAttrSpecialChars as $key => $value) {
  219. $this->assertEquals(
  220. $value,
  221. $this->escaper->escapeHtmlAttr($key),
  222. 'Failed to escape: ' . $key
  223. );
  224. // call by alias
  225. $this->assertEquals(
  226. $value,
  227. $this->escaper->attr($key)
  228. );
  229. // call by widget
  230. $this->assertEquals(
  231. $value,
  232. $this->escape($key, 'attr')
  233. );
  234. }
  235. }
  236. public function testJavascriptEscapingConvertsSpecialChars()
  237. {
  238. foreach ($this->jsSpecialChars as $key => $value) {
  239. $this->assertEquals(
  240. $value,
  241. $this->escaper->escapeJs($key),
  242. 'Failed to escape: ' . $key
  243. );
  244. }
  245. // call by alias
  246. $this->assertEquals(
  247. $value,
  248. $this->escaper->js($key)
  249. );
  250. // call by widget
  251. $this->assertEquals(
  252. $value,
  253. $this->escape($key, 'js')
  254. );
  255. }
  256. public function testJavascriptEscapingReturnsStringIfZeroLength()
  257. {
  258. $this->assertEquals('', $this->escaper->escapeJs(''));
  259. }
  260. public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits()
  261. {
  262. $this->assertEquals('123', $this->escaper->escapeJs('123'));
  263. }
  264. public function testCssEscapingConvertsSpecialChars()
  265. {
  266. foreach ($this->cssSpecialChars as $key => $value) {
  267. $this->assertEquals(
  268. $value,
  269. $this->escaper->escapeCss($key),
  270. 'Failed to escape: ' . $key
  271. );
  272. // call by alias
  273. $this->assertEquals(
  274. $value,
  275. $this->escaper->css($key)
  276. );
  277. // call by widget
  278. $this->assertEquals(
  279. $value,
  280. $this->escape($key, 'css')
  281. );
  282. }
  283. }
  284. public function testCssEscapingReturnsStringIfZeroLength()
  285. {
  286. $this->assertEquals('', $this->escaper->escapeCss(''));
  287. }
  288. public function testCssEscapingReturnsStringIfContainsOnlyDigits()
  289. {
  290. $this->assertEquals('123', $this->escaper->escapeCss('123'));
  291. }
  292. public function testUrlEscapingConvertsSpecialChars()
  293. {
  294. foreach ($this->urlSpecialChars as $key => $value) {
  295. $this->assertEquals(
  296. $value,
  297. $this->escaper->escapeUrl($key),
  298. 'Failed to escape: ' . $key
  299. );
  300. // call by alias
  301. $this->assertEquals(
  302. $value,
  303. $this->escaper->url($key)
  304. );
  305. // call by widget
  306. $this->assertEquals(
  307. $value,
  308. $this->escape($key, 'url')
  309. );
  310. }
  311. }
  312. /**
  313. * Range tests to confirm escaped range of characters is within OWASP recommendation
  314. */
  315. /**
  316. * Only testing the first few 2 ranges on this prot. function as that's all these
  317. * other range tests require
  318. */
  319. public function testUnicodeCodepointConversionToUtf8()
  320. {
  321. $expected = " ~ޙ";
  322. $codepoints = array(0x20, 0x7e, 0x799);
  323. $result = '';
  324. foreach ($codepoints as $value) {
  325. $result .= $this->codepointToUtf8($value);
  326. }
  327. $this->assertEquals($expected, $result);
  328. }
  329. /**
  330. * Convert a Unicode Codepoint to a literal UTF-8 character.
  331. *
  332. * @param int Unicode codepoint in hex notation
  333. * @return string UTF-8 literal string
  334. */
  335. protected function codepointToUtf8($codepoint)
  336. {
  337. if ($codepoint < 0x80) {
  338. return chr($codepoint);
  339. }
  340. if ($codepoint < 0x800) {
  341. return chr($codepoint >> 6 & 0x3f | 0xc0)
  342. . chr($codepoint & 0x3f | 0x80);
  343. }
  344. if ($codepoint < 0x10000) {
  345. return chr($codepoint >> 12 & 0x0f | 0xe0)
  346. . chr($codepoint >> 6 & 0x3f | 0x80)
  347. . chr($codepoint & 0x3f | 0x80);
  348. }
  349. if ($codepoint < 0x110000) {
  350. return chr($codepoint >> 18 & 0x07 | 0xf0)
  351. . chr($codepoint >> 12 & 0x3f | 0x80)
  352. . chr($codepoint >> 6 & 0x3f | 0x80)
  353. . chr($codepoint & 0x3f | 0x80);
  354. }
  355. throw new \Exception('Codepoint requested outside of Unicode range');
  356. }
  357. public function testJavascriptEscapingEscapesOwaspRecommendedRanges()
  358. {
  359. $immune = array(',', '.', '_'); // Exceptions to escaping ranges
  360. for ($chr=0; $chr < 0xFF; $chr++) {
  361. if ($chr >= 0x30 && $chr <= 0x39
  362. || $chr >= 0x41 && $chr <= 0x5A
  363. || $chr >= 0x61 && $chr <= 0x7A
  364. ) {
  365. $literal = $this->codepointToUtf8($chr);
  366. $this->assertEquals($literal, $this->escaper->escapeJs($literal));
  367. } else {
  368. $literal = $this->codepointToUtf8($chr);
  369. if (in_array($literal, $immune)) {
  370. $this->assertEquals($literal, $this->escaper->escapeJs($literal));
  371. } else {
  372. $this->assertNotEquals(
  373. $literal,
  374. $this->escaper->escapeJs($literal),
  375. $literal . ' should be escaped!'
  376. );
  377. }
  378. }
  379. }
  380. }
  381. public function testHtmlAttributeEscapingEscapesOwaspRecommendedRanges()
  382. {
  383. $immune = array(',', '.', '-', '_'); // Exceptions to escaping ranges
  384. for ($chr=0; $chr < 0xFF; $chr++) {
  385. if ($chr >= 0x30 && $chr <= 0x39
  386. || $chr >= 0x41 && $chr <= 0x5A
  387. || $chr >= 0x61 && $chr <= 0x7A
  388. ) {
  389. $literal = $this->codepointToUtf8($chr);
  390. $this->assertEquals($literal, $this->escaper->escapeHtmlAttr($literal));
  391. } else {
  392. $literal = $this->codepointToUtf8($chr);
  393. if (in_array($literal, $immune)) {
  394. $this->assertEquals($literal, $this->escaper->escapeHtmlAttr($literal));
  395. } else {
  396. $this->assertNotEquals(
  397. $literal,
  398. $this->escaper->escapeHtmlAttr($literal),
  399. $literal . ' should be escaped!'
  400. );
  401. }
  402. }
  403. }
  404. }
  405. public function testCssEscapingEscapesOwaspRecommendedRanges()
  406. {
  407. $immune = array(); // CSS has no exceptions to escaping ranges
  408. for ($chr=0; $chr < 0xFF; $chr++) {
  409. if ($chr >= 0x30 && $chr <= 0x39
  410. || $chr >= 0x41 && $chr <= 0x5A
  411. || $chr >= 0x61 && $chr <= 0x7A
  412. ) {
  413. $literal = $this->codepointToUtf8($chr);
  414. $this->assertEquals($literal, $this->escaper->escapeCss($literal));
  415. } else {
  416. $literal = $this->codepointToUtf8($chr);
  417. $this->assertNotEquals(
  418. $literal,
  419. $this->escaper->escapeCss($literal),
  420. $literal . ' should be escaped!'
  421. );
  422. }
  423. }
  424. }
  425. /**
  426. * @expectedException \InvalidArgumentException
  427. */
  428. public function testInvokeUnsupportedTypeShouldThrowException()
  429. {
  430. $this->escape('string', 'unsupport-type');
  431. }
  432. public function providerForEmptyVar()
  433. {
  434. return array(
  435. array(''),
  436. array(null),
  437. array(0),
  438. array('0'),
  439. );
  440. }
  441. /**
  442. * @dataProvider providerForEmptyVar
  443. */
  444. public function testEmptyVar($value)
  445. {
  446. foreach (array('html', 'js', 'css', 'url', 'attr') as $method) {
  447. $this->assertSame($value, $this->escaper->$method($value));
  448. }
  449. }
  450. }