URI.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. <?php
  2. /**
  3. * HTML Purifier's internal representation of a URI.
  4. * @note
  5. * Internal data-structures are completely escaped. If the data needs
  6. * to be used in a non-URI context (which is very unlikely), be sure
  7. * to decode it first. The URI may not necessarily be well-formed until
  8. * validate() is called.
  9. */
  10. class HTMLPurifier_URI
  11. {
  12. /**
  13. * @type string
  14. */
  15. public $scheme;
  16. /**
  17. * @type string
  18. */
  19. public $userinfo;
  20. /**
  21. * @type string
  22. */
  23. public $host;
  24. /**
  25. * @type int
  26. */
  27. public $port;
  28. /**
  29. * @type string
  30. */
  31. public $path;
  32. /**
  33. * @type string
  34. */
  35. public $query;
  36. /**
  37. * @type string
  38. */
  39. public $fragment;
  40. /**
  41. * @param string $scheme
  42. * @param string $userinfo
  43. * @param string $host
  44. * @param int $port
  45. * @param string $path
  46. * @param string $query
  47. * @param string $fragment
  48. * @note Automatically normalizes scheme and port
  49. */
  50. public function __construct($scheme, $userinfo, $host, $port, $path, $query, $fragment)
  51. {
  52. $this->scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme);
  53. $this->userinfo = $userinfo;
  54. $this->host = $host;
  55. $this->port = is_null($port) ? $port : (int)$port;
  56. $this->path = $path;
  57. $this->query = $query;
  58. $this->fragment = $fragment;
  59. }
  60. /**
  61. * Retrieves a scheme object corresponding to the URI's scheme/default
  62. * @param HTMLPurifier_Config $config
  63. * @param HTMLPurifier_Context $context
  64. * @return HTMLPurifier_URIScheme Scheme object appropriate for validating this URI
  65. */
  66. public function getSchemeObj($config, $context)
  67. {
  68. $registry = HTMLPurifier_URISchemeRegistry::instance();
  69. if ($this->scheme !== null) {
  70. $scheme_obj = $registry->getScheme($this->scheme, $config, $context);
  71. if (!$scheme_obj) {
  72. return false;
  73. } // invalid scheme, clean it out
  74. } else {
  75. // no scheme: retrieve the default one
  76. $def = $config->getDefinition('URI');
  77. $scheme_obj = $def->getDefaultScheme($config, $context);
  78. if (!$scheme_obj) {
  79. if ($def->defaultScheme !== null) {
  80. // something funky happened to the default scheme object
  81. trigger_error(
  82. 'Default scheme object "' . $def->defaultScheme . '" was not readable',
  83. E_USER_WARNING
  84. );
  85. } // suppress error if it's null
  86. return false;
  87. }
  88. }
  89. return $scheme_obj;
  90. }
  91. /**
  92. * Generic validation method applicable for all schemes. May modify
  93. * this URI in order to get it into a compliant form.
  94. * @param HTMLPurifier_Config $config
  95. * @param HTMLPurifier_Context $context
  96. * @return bool True if validation/filtering succeeds, false if failure
  97. */
  98. public function validate($config, $context)
  99. {
  100. // ABNF definitions from RFC 3986
  101. $chars_sub_delims = '!$&\'()*+,;=';
  102. $chars_gen_delims = ':/?#[]@';
  103. $chars_pchar = $chars_sub_delims . ':@';
  104. // validate host
  105. if (!is_null($this->host)) {
  106. $host_def = new HTMLPurifier_AttrDef_URI_Host();
  107. $this->host = $host_def->validate($this->host, $config, $context);
  108. if ($this->host === false) {
  109. $this->host = null;
  110. }
  111. }
  112. // validate scheme
  113. // NOTE: It's not appropriate to check whether or not this
  114. // scheme is in our registry, since a URIFilter may convert a
  115. // URI that we don't allow into one we do. So instead, we just
  116. // check if the scheme can be dropped because there is no host
  117. // and it is our default scheme.
  118. if (!is_null($this->scheme) && is_null($this->host) || $this->host === '') {
  119. // support for relative paths is pretty abysmal when the
  120. // scheme is present, so axe it when possible
  121. $def = $config->getDefinition('URI');
  122. if ($def->defaultScheme === $this->scheme) {
  123. $this->scheme = null;
  124. }
  125. }
  126. // validate username
  127. if (!is_null($this->userinfo)) {
  128. $encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':');
  129. $this->userinfo = $encoder->encode($this->userinfo);
  130. }
  131. // validate port
  132. if (!is_null($this->port)) {
  133. if ($this->port < 1 || $this->port > 65535) {
  134. $this->port = null;
  135. }
  136. }
  137. // validate path
  138. $segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/');
  139. if (!is_null($this->host)) { // this catches $this->host === ''
  140. // path-abempty (hier and relative)
  141. // http://www.example.com/my/path
  142. // //www.example.com/my/path (looks odd, but works, and
  143. // recognized by most browsers)
  144. // (this set is valid or invalid on a scheme by scheme
  145. // basis, so we'll deal with it later)
  146. // file:///my/path
  147. // ///my/path
  148. $this->path = $segments_encoder->encode($this->path);
  149. } elseif ($this->path !== '') {
  150. if ($this->path[0] === '/') {
  151. // path-absolute (hier and relative)
  152. // http:/my/path
  153. // /my/path
  154. if (strlen($this->path) >= 2 && $this->path[1] === '/') {
  155. // This could happen if both the host gets stripped
  156. // out
  157. // http://my/path
  158. // //my/path
  159. $this->path = '';
  160. } else {
  161. $this->path = $segments_encoder->encode($this->path);
  162. }
  163. } elseif (!is_null($this->scheme)) {
  164. // path-rootless (hier)
  165. // http:my/path
  166. // Short circuit evaluation means we don't need to check nz
  167. $this->path = $segments_encoder->encode($this->path);
  168. } else {
  169. // path-noscheme (relative)
  170. // my/path
  171. // (once again, not checking nz)
  172. $segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@');
  173. $c = strpos($this->path, '/');
  174. if ($c !== false) {
  175. $this->path =
  176. $segment_nc_encoder->encode(substr($this->path, 0, $c)) .
  177. $segments_encoder->encode(substr($this->path, $c));
  178. } else {
  179. $this->path = $segment_nc_encoder->encode($this->path);
  180. }
  181. }
  182. } else {
  183. // path-empty (hier and relative)
  184. $this->path = ''; // just to be safe
  185. }
  186. // qf = query and fragment
  187. $qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?');
  188. if (!is_null($this->query)) {
  189. $this->query = $qf_encoder->encode($this->query);
  190. }
  191. if (!is_null($this->fragment)) {
  192. $this->fragment = $qf_encoder->encode($this->fragment);
  193. }
  194. return true;
  195. }
  196. /**
  197. * Convert URI back to string
  198. * @return string URI appropriate for output
  199. */
  200. public function toString()
  201. {
  202. // reconstruct authority
  203. $authority = null;
  204. // there is a rendering difference between a null authority
  205. // (http:foo-bar) and an empty string authority
  206. // (http:///foo-bar).
  207. if (!is_null($this->host)) {
  208. $authority = '';
  209. if (!is_null($this->userinfo)) {
  210. $authority .= $this->userinfo . '@';
  211. }
  212. $authority .= $this->host;
  213. if (!is_null($this->port)) {
  214. $authority .= ':' . $this->port;
  215. }
  216. }
  217. // Reconstruct the result
  218. // One might wonder about parsing quirks from browsers after
  219. // this reconstruction. Unfortunately, parsing behavior depends
  220. // on what *scheme* was employed (file:///foo is handled *very*
  221. // differently than http:///foo), so unfortunately we have to
  222. // defer to the schemes to do the right thing.
  223. $result = '';
  224. if (!is_null($this->scheme)) {
  225. $result .= $this->scheme . ':';
  226. }
  227. if (!is_null($authority)) {
  228. $result .= '//' . $authority;
  229. }
  230. $result .= $this->path;
  231. if (!is_null($this->query)) {
  232. $result .= '?' . $this->query;
  233. }
  234. if (!is_null($this->fragment)) {
  235. $result .= '#' . $this->fragment;
  236. }
  237. return $result;
  238. }
  239. /**
  240. * Returns true if this URL might be considered a 'local' URL given
  241. * the current context. This is true when the host is null, or
  242. * when it matches the host supplied to the configuration.
  243. *
  244. * Note that this does not do any scheme checking, so it is mostly
  245. * only appropriate for metadata that doesn't care about protocol
  246. * security. isBenign is probably what you actually want.
  247. * @param HTMLPurifier_Config $config
  248. * @param HTMLPurifier_Context $context
  249. * @return bool
  250. */
  251. public function isLocal($config, $context)
  252. {
  253. if ($this->host === null) {
  254. return true;
  255. }
  256. $uri_def = $config->getDefinition('URI');
  257. if ($uri_def->host === $this->host) {
  258. return true;
  259. }
  260. return false;
  261. }
  262. /**
  263. * Returns true if this URL should be considered a 'benign' URL,
  264. * that is:
  265. *
  266. * - It is a local URL (isLocal), and
  267. * - It has a equal or better level of security
  268. * @param HTMLPurifier_Config $config
  269. * @param HTMLPurifier_Context $context
  270. * @return bool
  271. */
  272. public function isBenign($config, $context)
  273. {
  274. if (!$this->isLocal($config, $context)) {
  275. return false;
  276. }
  277. $scheme_obj = $this->getSchemeObj($config, $context);
  278. if (!$scheme_obj) {
  279. return false;
  280. } // conservative approach
  281. $current_scheme_obj = $config->getDefinition('URI')->getDefaultScheme($config, $context);
  282. if ($current_scheme_obj->secure) {
  283. if (!$scheme_obj->secure) {
  284. return false;
  285. }
  286. }
  287. return true;
  288. }
  289. }
  290. // vim: et sw=4 sts=4