PageRenderTime 80ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 2ms

/includes/common.inc

https://bitbucket.org/hjain/trinet
PHP | 7966 lines | 3275 code | 529 blank | 4162 comment | 622 complexity | e56cf64dca7391404a7e2f2571bd6d2d MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0, AGPL-1.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * @file
  4. * Common functions that many Drupal modules will need to reference.
  5. *
  6. * The functions that are critical and need to be available even when serving
  7. * a cached page are instead located in bootstrap.inc.
  8. */
  9. /**
  10. * @defgroup php_wrappers PHP wrapper functions
  11. * @{
  12. * Functions that are wrappers or custom implementations of PHP functions.
  13. *
  14. * Certain PHP functions should not be used in Drupal. Instead, Drupal's
  15. * replacement functions should be used.
  16. *
  17. * For example, for improved or more secure UTF8-handling, or RFC-compliant
  18. * handling of URLs in Drupal.
  19. *
  20. * For ease of use and memorizing, all these wrapper functions use the same name
  21. * as the original PHP function, but prefixed with "drupal_". Beware, however,
  22. * that not all wrapper functions support the same arguments as the original
  23. * functions.
  24. *
  25. * You should always use these wrapper functions in your code.
  26. *
  27. * Wrong:
  28. * @code
  29. * $my_substring = substr($original_string, 0, 5);
  30. * @endcode
  31. *
  32. * Correct:
  33. * @code
  34. * $my_substring = drupal_substr($original_string, 0, 5);
  35. * @endcode
  36. *
  37. * @}
  38. */
  39. /**
  40. * Return status for saving which involved creating a new item.
  41. */
  42. define('SAVED_NEW', 1);
  43. /**
  44. * Return status for saving which involved an update to an existing item.
  45. */
  46. define('SAVED_UPDATED', 2);
  47. /**
  48. * Return status for saving which deleted an existing item.
  49. */
  50. define('SAVED_DELETED', 3);
  51. /**
  52. * The default group for system CSS files added to the page.
  53. */
  54. define('CSS_SYSTEM', -100);
  55. /**
  56. * The default group for module CSS files added to the page.
  57. */
  58. define('CSS_DEFAULT', 0);
  59. /**
  60. * The default group for theme CSS files added to the page.
  61. */
  62. define('CSS_THEME', 100);
  63. /**
  64. * The default group for JavaScript and jQuery libraries added to the page.
  65. */
  66. define('JS_LIBRARY', -100);
  67. /**
  68. * The default group for module JavaScript code added to the page.
  69. */
  70. define('JS_DEFAULT', 0);
  71. /**
  72. * The default group for theme JavaScript code added to the page.
  73. */
  74. define('JS_THEME', 100);
  75. /**
  76. * Error code indicating that the request exceeded the specified timeout.
  77. *
  78. * @see drupal_http_request()
  79. */
  80. define('HTTP_REQUEST_TIMEOUT', -1);
  81. /**
  82. * Constants defining cache granularity for blocks and renderable arrays.
  83. *
  84. * Modules specify the caching patterns for their blocks using binary
  85. * combinations of these constants in their hook_block_info():
  86. * $block[delta]['cache'] = DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE;
  87. * DRUPAL_CACHE_PER_ROLE is used as a default when no caching pattern is
  88. * specified. Use DRUPAL_CACHE_CUSTOM to disable standard block cache and
  89. * implement
  90. *
  91. * The block cache is cleared in cache_clear_all(), and uses the same clearing
  92. * policy than page cache (node, comment, user, taxonomy added or updated...).
  93. * Blocks requiring more fine-grained clearing might consider disabling the
  94. * built-in block cache (DRUPAL_NO_CACHE) and roll their own.
  95. *
  96. * Note that user 1 is excluded from block caching.
  97. */
  98. /**
  99. * The block should not get cached.
  100. *
  101. * This setting should be used:
  102. * - For simple blocks (notably those that do not perform any db query), where
  103. * querying the db cache would be more expensive than directly generating the
  104. * content.
  105. * - For blocks that change too frequently.
  106. */
  107. define('DRUPAL_NO_CACHE', -1);
  108. /**
  109. * The block is handling its own caching in its hook_block_view().
  110. *
  111. * From the perspective of the block cache system, this is equivalent to
  112. * DRUPAL_NO_CACHE. Useful when time based expiration is needed or a site uses
  113. * a node access which invalidates standard block cache.
  114. */
  115. define('DRUPAL_CACHE_CUSTOM', -2);
  116. /**
  117. * The block or element can change depending on the user's roles.
  118. *
  119. * This is the default setting for blocks, used when the block does not specify
  120. * anything.
  121. */
  122. define('DRUPAL_CACHE_PER_ROLE', 0x0001);
  123. /**
  124. * The block or element can change depending on the user.
  125. *
  126. * This setting can be resource-consuming for sites with large number of users,
  127. * and thus should only be used when DRUPAL_CACHE_PER_ROLE is not sufficient.
  128. */
  129. define('DRUPAL_CACHE_PER_USER', 0x0002);
  130. /**
  131. * The block or element can change depending on the page being viewed.
  132. */
  133. define('DRUPAL_CACHE_PER_PAGE', 0x0004);
  134. /**
  135. * The block or element is the same for every user and page that it is visible.
  136. */
  137. define('DRUPAL_CACHE_GLOBAL', 0x0008);
  138. /**
  139. * Adds content to a specified region.
  140. *
  141. * @param $region
  142. * Page region the content is added to.
  143. * @param $data
  144. * Content to be added.
  145. */
  146. function drupal_add_region_content($region = NULL, $data = NULL) {
  147. static $content = array();
  148. if (isset($region) && isset($data)) {
  149. $content[$region][] = $data;
  150. }
  151. return $content;
  152. }
  153. /**
  154. * Gets assigned content for a given region.
  155. *
  156. * @param $region
  157. * A specified region to fetch content for. If NULL, all regions will be
  158. * returned.
  159. * @param $delimiter
  160. * Content to be inserted between imploded array elements.
  161. */
  162. function drupal_get_region_content($region = NULL, $delimiter = ' ') {
  163. $content = drupal_add_region_content();
  164. if (isset($region)) {
  165. if (isset($content[$region]) && is_array($content[$region])) {
  166. return implode($delimiter, $content[$region]);
  167. }
  168. }
  169. else {
  170. foreach (array_keys($content) as $region) {
  171. if (is_array($content[$region])) {
  172. $content[$region] = implode($delimiter, $content[$region]);
  173. }
  174. }
  175. return $content;
  176. }
  177. }
  178. /**
  179. * Gets the name of the currently active install profile.
  180. *
  181. * When this function is called during Drupal's initial installation process,
  182. * the name of the profile that's about to be installed is stored in the global
  183. * installation state. At all other times, the standard Drupal systems variable
  184. * table contains the name of the current profile, and we can call
  185. * variable_get() to determine what one is active.
  186. *
  187. * @return $profile
  188. * The name of the install profile.
  189. */
  190. function drupal_get_profile() {
  191. global $install_state;
  192. if (isset($install_state['parameters']['profile'])) {
  193. $profile = $install_state['parameters']['profile'];
  194. }
  195. else {
  196. $profile = variable_get('install_profile', 'standard');
  197. }
  198. return $profile;
  199. }
  200. /**
  201. * Sets the breadcrumb trail for the current page.
  202. *
  203. * @param $breadcrumb
  204. * Array of links, starting with "home" and proceeding up to but not including
  205. * the current page.
  206. */
  207. function drupal_set_breadcrumb($breadcrumb = NULL) {
  208. $stored_breadcrumb = &drupal_static(__FUNCTION__);
  209. if (isset($breadcrumb)) {
  210. $stored_breadcrumb = $breadcrumb;
  211. }
  212. return $stored_breadcrumb;
  213. }
  214. /**
  215. * Gets the breadcrumb trail for the current page.
  216. */
  217. function drupal_get_breadcrumb() {
  218. $breadcrumb = drupal_set_breadcrumb();
  219. if (!isset($breadcrumb)) {
  220. $breadcrumb = menu_get_active_breadcrumb();
  221. }
  222. return $breadcrumb;
  223. }
  224. /**
  225. * Returns a string containing RDF namespace declarations for use in XML and
  226. * XHTML output.
  227. */
  228. function drupal_get_rdf_namespaces() {
  229. $xml_rdf_namespaces = array();
  230. // Serializes the RDF namespaces in XML namespace syntax.
  231. if (function_exists('rdf_get_namespaces')) {
  232. foreach (rdf_get_namespaces() as $prefix => $uri) {
  233. $xml_rdf_namespaces[] = 'xmlns:' . $prefix . '="' . $uri . '"';
  234. }
  235. }
  236. return count($xml_rdf_namespaces) ? "\n " . implode("\n ", $xml_rdf_namespaces) : '';
  237. }
  238. /**
  239. * Adds output to the HEAD tag of the HTML page.
  240. *
  241. * This function can be called as long the headers aren't sent. Pass no
  242. * arguments (or NULL for both) to retrieve the currently stored elements.
  243. *
  244. * @param $data
  245. * A renderable array. If the '#type' key is not set then 'html_tag' will be
  246. * added as the default '#type'.
  247. * @param $key
  248. * A unique string key to allow implementations of hook_html_head_alter() to
  249. * identify the element in $data. Required if $data is not NULL.
  250. *
  251. * @return
  252. * An array of all stored HEAD elements.
  253. *
  254. * @see theme_html_tag()
  255. */
  256. function drupal_add_html_head($data = NULL, $key = NULL) {
  257. $stored_head = &drupal_static(__FUNCTION__);
  258. if (!isset($stored_head)) {
  259. // Make sure the defaults, including Content-Type, come first.
  260. $stored_head = _drupal_default_html_head();
  261. }
  262. if (isset($data) && isset($key)) {
  263. if (!isset($data['#type'])) {
  264. $data['#type'] = 'html_tag';
  265. }
  266. $stored_head[$key] = $data;
  267. }
  268. return $stored_head;
  269. }
  270. /**
  271. * Returns elements that are always displayed in the HEAD tag of the HTML page.
  272. */
  273. function _drupal_default_html_head() {
  274. // Add default elements. Make sure the Content-Type comes first because the
  275. // IE browser may be vulnerable to XSS via encoding attacks from any content
  276. // that comes before this META tag, such as a TITLE tag.
  277. $elements['system_meta_content_type'] = array(
  278. '#type' => 'html_tag',
  279. '#tag' => 'meta',
  280. '#attributes' => array(
  281. 'http-equiv' => 'Content-Type',
  282. 'content' => 'text/html; charset=utf-8',
  283. ),
  284. // Security: This always has to be output first.
  285. '#weight' => -1000,
  286. );
  287. // Show Drupal and the major version number in the META GENERATOR tag.
  288. // Get the major version.
  289. list($version, ) = explode('.', VERSION);
  290. $elements['system_meta_generator'] = array(
  291. '#type' => 'html_tag',
  292. '#tag' => 'meta',
  293. '#attributes' => array(
  294. 'name' => 'Generator',
  295. 'content' => 'Drupal ' . $version . ' (http://drupal.org)',
  296. ),
  297. );
  298. // Also send the generator in the HTTP header.
  299. $elements['system_meta_generator']['#attached']['drupal_add_http_header'][] = array('X-Generator', $elements['system_meta_generator']['#attributes']['content']);
  300. return $elements;
  301. }
  302. /**
  303. * Retrieves output to be displayed in the HEAD tag of the HTML page.
  304. */
  305. function drupal_get_html_head() {
  306. $elements = drupal_add_html_head();
  307. drupal_alter('html_head', $elements);
  308. return drupal_render($elements);
  309. }
  310. /**
  311. * Adds a feed URL for the current page.
  312. *
  313. * This function can be called as long the HTML header hasn't been sent.
  314. *
  315. * @param $url
  316. * An internal system path or a fully qualified external URL of the feed.
  317. * @param $title
  318. * The title of the feed.
  319. */
  320. function drupal_add_feed($url = NULL, $title = '') {
  321. $stored_feed_links = &drupal_static(__FUNCTION__, array());
  322. if (isset($url)) {
  323. $stored_feed_links[$url] = theme('feed_icon', array('url' => $url, 'title' => $title));
  324. drupal_add_html_head_link(array(
  325. 'rel' => 'alternate',
  326. 'type' => 'application/rss+xml',
  327. 'title' => $title,
  328. // Force the URL to be absolute, for consistency with other <link> tags
  329. // output by Drupal.
  330. 'href' => url($url, array('absolute' => TRUE)),
  331. ));
  332. }
  333. return $stored_feed_links;
  334. }
  335. /**
  336. * Gets the feed URLs for the current page.
  337. *
  338. * @param $delimiter
  339. * A delimiter to split feeds by.
  340. */
  341. function drupal_get_feeds($delimiter = "\n") {
  342. $feeds = drupal_add_feed();
  343. return implode($feeds, $delimiter);
  344. }
  345. /**
  346. * @defgroup http_handling HTTP handling
  347. * @{
  348. * Functions to properly handle HTTP responses.
  349. */
  350. /**
  351. * Processes a URL query parameter array to remove unwanted elements.
  352. *
  353. * @param $query
  354. * (optional) An array to be processed. Defaults to $_GET.
  355. * @param $exclude
  356. * (optional) A list of $query array keys to remove. Use "parent[child]" to
  357. * exclude nested items. Defaults to array('q').
  358. * @param $parent
  359. * Internal use only. Used to build the $query array key for nested items.
  360. *
  361. * @return
  362. * An array containing query parameters, which can be used for url().
  363. */
  364. function drupal_get_query_parameters(array $query = NULL, array $exclude = array('q'), $parent = '') {
  365. // Set defaults, if none given.
  366. if (!isset($query)) {
  367. $query = $_GET;
  368. }
  369. // If $exclude is empty, there is nothing to filter.
  370. if (empty($exclude)) {
  371. return $query;
  372. }
  373. elseif (!$parent) {
  374. $exclude = array_flip($exclude);
  375. }
  376. $params = array();
  377. foreach ($query as $key => $value) {
  378. $string_key = ($parent ? $parent . '[' . $key . ']' : $key);
  379. if (isset($exclude[$string_key])) {
  380. continue;
  381. }
  382. if (is_array($value)) {
  383. $params[$key] = drupal_get_query_parameters($value, $exclude, $string_key);
  384. }
  385. else {
  386. $params[$key] = $value;
  387. }
  388. }
  389. return $params;
  390. }
  391. /**
  392. * Splits a URL-encoded query string into an array.
  393. *
  394. * @param $query
  395. * The query string to split.
  396. *
  397. * @return
  398. * An array of url decoded couples $param_name => $value.
  399. */
  400. function drupal_get_query_array($query) {
  401. $result = array();
  402. if (!empty($query)) {
  403. foreach (explode('&', $query) as $param) {
  404. $param = explode('=', $param);
  405. $result[$param[0]] = isset($param[1]) ? rawurldecode($param[1]) : '';
  406. }
  407. }
  408. return $result;
  409. }
  410. /**
  411. * Parses an array into a valid, rawurlencoded query string.
  412. *
  413. * This differs from http_build_query() as we need to rawurlencode() (instead of
  414. * urlencode()) all query parameters.
  415. *
  416. * @param $query
  417. * The query parameter array to be processed, e.g. $_GET.
  418. * @param $parent
  419. * Internal use only. Used to build the $query array key for nested items.
  420. *
  421. * @return
  422. * A rawurlencoded string which can be used as or appended to the URL query
  423. * string.
  424. *
  425. * @see drupal_get_query_parameters()
  426. * @ingroup php_wrappers
  427. */
  428. function drupal_http_build_query(array $query, $parent = '') {
  429. $params = array();
  430. foreach ($query as $key => $value) {
  431. $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key));
  432. // Recurse into children.
  433. if (is_array($value)) {
  434. $params[] = drupal_http_build_query($value, $key);
  435. }
  436. // If a query parameter value is NULL, only append its key.
  437. elseif (!isset($value)) {
  438. $params[] = $key;
  439. }
  440. else {
  441. // For better readability of paths in query strings, we decode slashes.
  442. $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
  443. }
  444. }
  445. return implode('&', $params);
  446. }
  447. /**
  448. * Prepares a 'destination' URL query parameter for use with drupal_goto().
  449. *
  450. * Used to direct the user back to the referring page after completing a form.
  451. * By default the current URL is returned. If a destination exists in the
  452. * previous request, that destination is returned. As such, a destination can
  453. * persist across multiple pages.
  454. *
  455. * @see drupal_goto()
  456. */
  457. function drupal_get_destination() {
  458. $destination = &drupal_static(__FUNCTION__);
  459. if (isset($destination)) {
  460. return $destination;
  461. }
  462. if (isset($_GET['destination'])) {
  463. $destination = array('destination' => $_GET['destination']);
  464. }
  465. else {
  466. $path = $_GET['q'];
  467. $query = drupal_http_build_query(drupal_get_query_parameters());
  468. if ($query != '') {
  469. $path .= '?' . $query;
  470. }
  471. $destination = array('destination' => $path);
  472. }
  473. return $destination;
  474. }
  475. /**
  476. * Parses a system URL string into an associative array suitable for url().
  477. *
  478. * This function should only be used for URLs that have been generated by the
  479. * system, resp. url(). It should not be used for URLs that come from external
  480. * sources, or URLs that link to external resources.
  481. *
  482. * The returned array contains a 'path' that may be passed separately to url().
  483. * For example:
  484. * @code
  485. * $options = drupal_parse_url($_GET['destination']);
  486. * $my_url = url($options['path'], $options);
  487. * $my_link = l('Example link', $options['path'], $options);
  488. * @endcode
  489. *
  490. * This is required, because url() does not support relative URLs containing a
  491. * query string or fragment in its $path argument. Instead, any query string
  492. * needs to be parsed into an associative query parameter array in
  493. * $options['query'] and the fragment into $options['fragment'].
  494. *
  495. * @param $url
  496. * The URL string to parse, f.e. $_GET['destination'].
  497. *
  498. * @return
  499. * An associative array containing the keys:
  500. * - 'path': The path of the URL. If the given $url is external, this includes
  501. * the scheme and host.
  502. * - 'query': An array of query parameters of $url, if existent.
  503. * - 'fragment': The fragment of $url, if existent.
  504. *
  505. * @see url()
  506. * @see drupal_goto()
  507. * @ingroup php_wrappers
  508. */
  509. function drupal_parse_url($url) {
  510. $options = array(
  511. 'path' => NULL,
  512. 'query' => array(),
  513. 'fragment' => '',
  514. );
  515. // External URLs: not using parse_url() here, so we do not have to rebuild
  516. // the scheme, host, and path without having any use for it.
  517. if (strpos($url, '://') !== FALSE) {
  518. // Split off everything before the query string into 'path'.
  519. $parts = explode('?', $url);
  520. $options['path'] = $parts[0];
  521. // If there is a query string, transform it into keyed query parameters.
  522. if (isset($parts[1])) {
  523. $query_parts = explode('#', $parts[1]);
  524. parse_str($query_parts[0], $options['query']);
  525. // Take over the fragment, if there is any.
  526. if (isset($query_parts[1])) {
  527. $options['fragment'] = $query_parts[1];
  528. }
  529. }
  530. }
  531. // Internal URLs.
  532. else {
  533. // parse_url() does not support relative URLs, so make it absolute. E.g. the
  534. // relative URL "foo/bar:1" isn't properly parsed.
  535. $parts = parse_url('http://example.com/' . $url);
  536. // Strip the leading slash that was just added.
  537. $options['path'] = substr($parts['path'], 1);
  538. if (isset($parts['query'])) {
  539. parse_str($parts['query'], $options['query']);
  540. }
  541. if (isset($parts['fragment'])) {
  542. $options['fragment'] = $parts['fragment'];
  543. }
  544. }
  545. // The 'q' parameter contains the path of the current page if clean URLs are
  546. // disabled. It overrides the 'path' of the URL when present, even if clean
  547. // URLs are enabled, due to how Apache rewriting rules work.
  548. if (isset($options['query']['q'])) {
  549. $options['path'] = $options['query']['q'];
  550. unset($options['query']['q']);
  551. }
  552. return $options;
  553. }
  554. /**
  555. * Encodes a Drupal path for use in a URL.
  556. *
  557. * For aesthetic reasons slashes are not escaped.
  558. *
  559. * Note that url() takes care of calling this function, so a path passed to that
  560. * function should not be encoded in advance.
  561. *
  562. * @param $path
  563. * The Drupal path to encode.
  564. */
  565. function drupal_encode_path($path) {
  566. return str_replace('%2F', '/', rawurlencode($path));
  567. }
  568. /**
  569. * Sends the user to a different Drupal page.
  570. *
  571. * This issues an on-site HTTP redirect. The function makes sure the redirected
  572. * URL is formatted correctly.
  573. *
  574. * Usually the redirected URL is constructed from this function's input
  575. * parameters. However you may override that behavior by setting a
  576. * destination in either the $_REQUEST-array (i.e. by using
  577. * the query string of an URI) This is used to direct the user back to
  578. * the proper page after completing a form. For example, after editing
  579. * a post on the 'admin/content'-page or after having logged on using the
  580. * 'user login'-block in a sidebar. The function drupal_get_destination()
  581. * can be used to help set the destination URL.
  582. *
  583. * Drupal will ensure that messages set by drupal_set_message() and other
  584. * session data are written to the database before the user is redirected.
  585. *
  586. * This function ends the request; use it instead of a return in your menu
  587. * callback.
  588. *
  589. * @param $path
  590. * A Drupal path or a full URL.
  591. * @param $options
  592. * An associative array of additional URL options to pass to url().
  593. * @param $http_response_code
  594. * Valid values for an actual "goto" as per RFC 2616 section 10.3 are:
  595. * - 301 Moved Permanently (the recommended value for most redirects)
  596. * - 302 Found (default in Drupal and PHP, sometimes used for spamming search
  597. * engines)
  598. * - 303 See Other
  599. * - 304 Not Modified
  600. * - 305 Use Proxy
  601. * - 307 Temporary Redirect (alternative to "503 Site Down for Maintenance")
  602. * Note: Other values are defined by RFC 2616, but are rarely used and poorly
  603. * supported.
  604. *
  605. * @see drupal_get_destination()
  606. * @see url()
  607. */
  608. function drupal_goto($path = '', array $options = array(), $http_response_code = 302) {
  609. // A destination in $_GET always overrides the function arguments.
  610. // We do not allow absolute URLs to be passed via $_GET, as this can be an attack vector.
  611. if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) {
  612. $destination = drupal_parse_url($_GET['destination']);
  613. $path = $destination['path'];
  614. $options['query'] = $destination['query'];
  615. $options['fragment'] = $destination['fragment'];
  616. }
  617. drupal_alter('drupal_goto', $path, $options, $http_response_code);
  618. // The 'Location' HTTP header must be absolute.
  619. $options['absolute'] = TRUE;
  620. $url = url($path, $options);
  621. header('Location: ' . $url, TRUE, $http_response_code);
  622. // The "Location" header sends a redirect status code to the HTTP daemon. In
  623. // some cases this can be wrong, so we make sure none of the code below the
  624. // drupal_goto() call gets executed upon redirection.
  625. drupal_exit($url);
  626. }
  627. /**
  628. * Delivers a "site is under maintenance" message to the browser.
  629. *
  630. * Page callback functions wanting to report a "site offline" message should
  631. * return MENU_SITE_OFFLINE instead of calling drupal_site_offline(). However,
  632. * functions that are invoked in contexts where that return value might not
  633. * bubble up to menu_execute_active_handler() should call drupal_site_offline().
  634. */
  635. function drupal_site_offline() {
  636. drupal_deliver_page(MENU_SITE_OFFLINE);
  637. }
  638. /**
  639. * Delivers a "page not found" error to the browser.
  640. *
  641. * Page callback functions wanting to report a "page not found" message should
  642. * return MENU_NOT_FOUND instead of calling drupal_not_found(). However,
  643. * functions that are invoked in contexts where that return value might not
  644. * bubble up to menu_execute_active_handler() should call drupal_not_found().
  645. */
  646. function drupal_not_found() {
  647. drupal_deliver_page(MENU_NOT_FOUND);
  648. }
  649. /**
  650. * Delivers an "access denied" error to the browser.
  651. *
  652. * Page callback functions wanting to report an "access denied" message should
  653. * return MENU_ACCESS_DENIED instead of calling drupal_access_denied(). However,
  654. * functions that are invoked in contexts where that return value might not
  655. * bubble up to menu_execute_active_handler() should call
  656. * drupal_access_denied().
  657. */
  658. function drupal_access_denied() {
  659. drupal_deliver_page(MENU_ACCESS_DENIED);
  660. }
  661. /**
  662. * Performs an HTTP request.
  663. *
  664. * This is a flexible and powerful HTTP client implementation. Correctly
  665. * handles GET, POST, PUT or any other HTTP requests. Handles redirects.
  666. *
  667. * @param $url
  668. * A string containing a fully qualified URI.
  669. * @param array $options
  670. * (optional) An array that can have one or more of the following elements:
  671. * - headers: An array containing request headers to send as name/value pairs.
  672. * - method: A string containing the request method. Defaults to 'GET'.
  673. * - data: A string containing the request body, formatted as
  674. * 'param=value&param=value&...'. Defaults to NULL.
  675. * - max_redirects: An integer representing how many times a redirect
  676. * may be followed. Defaults to 3.
  677. * - timeout: A float representing the maximum number of seconds the function
  678. * call may take. The default is 30 seconds. If a timeout occurs, the error
  679. * code is set to the HTTP_REQUEST_TIMEOUT constant.
  680. * - context: A context resource created with stream_context_create().
  681. *
  682. * @return object
  683. * An object that can have one or more of the following components:
  684. * - request: A string containing the request body that was sent.
  685. * - code: An integer containing the response status code, or the error code
  686. * if an error occurred.
  687. * - protocol: The response protocol (e.g. HTTP/1.1 or HTTP/1.0).
  688. * - status_message: The status message from the response, if a response was
  689. * received.
  690. * - redirect_code: If redirected, an integer containing the initial response
  691. * status code.
  692. * - redirect_url: If redirected, a string containing the URL of the redirect
  693. * target.
  694. * - error: If an error occurred, the error message. Otherwise not set.
  695. * - headers: An array containing the response headers as name/value pairs.
  696. * HTTP header names are case-insensitive (RFC 2616, section 4.2), so for
  697. * easy access the array keys are returned in lower case.
  698. * - data: A string containing the response body that was received.
  699. */
  700. function drupal_http_request($url, array $options = array()) {
  701. $result = new stdClass();
  702. // Parse the URL and make sure we can handle the schema.
  703. $uri = @parse_url($url);
  704. if ($uri == FALSE) {
  705. $result->error = 'unable to parse URL';
  706. $result->code = -1001;
  707. return $result;
  708. }
  709. if (!isset($uri['scheme'])) {
  710. $result->error = 'missing schema';
  711. $result->code = -1002;
  712. return $result;
  713. }
  714. timer_start(__FUNCTION__);
  715. // Merge the default options.
  716. $options += array(
  717. 'headers' => array(),
  718. 'method' => 'GET',
  719. 'data' => NULL,
  720. 'max_redirects' => 3,
  721. 'timeout' => 30.0,
  722. 'context' => NULL,
  723. );
  724. // stream_socket_client() requires timeout to be a float.
  725. $options['timeout'] = (float) $options['timeout'];
  726. switch ($uri['scheme']) {
  727. case 'http':
  728. case 'feed':
  729. $port = isset($uri['port']) ? $uri['port'] : 80;
  730. $socket = 'tcp://' . $uri['host'] . ':' . $port;
  731. // RFC 2616: "non-standard ports MUST, default ports MAY be included".
  732. // We don't add the standard port to prevent from breaking rewrite rules
  733. // checking the host that do not take into account the port number.
  734. $options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : '');
  735. break;
  736. case 'https':
  737. // Note: Only works when PHP is compiled with OpenSSL support.
  738. $port = isset($uri['port']) ? $uri['port'] : 443;
  739. $socket = 'ssl://' . $uri['host'] . ':' . $port;
  740. $options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : '');
  741. break;
  742. default:
  743. $result->error = 'invalid schema ' . $uri['scheme'];
  744. $result->code = -1003;
  745. return $result;
  746. }
  747. if (empty($options['context'])) {
  748. $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout']);
  749. }
  750. else {
  751. // Create a stream with context. Allows verification of a SSL certificate.
  752. $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout'], STREAM_CLIENT_CONNECT, $options['context']);
  753. }
  754. // Make sure the socket opened properly.
  755. if (!$fp) {
  756. // When a network error occurs, we use a negative number so it does not
  757. // clash with the HTTP status codes.
  758. $result->code = -$errno;
  759. $result->error = trim($errstr) ? trim($errstr) : t('Error opening socket @socket', array('@socket' => $socket));
  760. // Mark that this request failed. This will trigger a check of the web
  761. // server's ability to make outgoing HTTP requests the next time that
  762. // requirements checking is performed.
  763. // See system_requirements().
  764. variable_set('drupal_http_request_fails', TRUE);
  765. return $result;
  766. }
  767. // Construct the path to act on.
  768. $path = isset($uri['path']) ? $uri['path'] : '/';
  769. if (isset($uri['query'])) {
  770. $path .= '?' . $uri['query'];
  771. }
  772. // Merge the default headers.
  773. $options['headers'] += array(
  774. 'User-Agent' => 'Drupal (+http://drupal.org/)',
  775. );
  776. // Only add Content-Length if we actually have any content or if it is a POST
  777. // or PUT request. Some non-standard servers get confused by Content-Length in
  778. // at least HEAD/GET requests, and Squid always requires Content-Length in
  779. // POST/PUT requests.
  780. $content_length = strlen($options['data']);
  781. if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') {
  782. $options['headers']['Content-Length'] = $content_length;
  783. }
  784. // If the server URL has a user then attempt to use basic authentication.
  785. if (isset($uri['user'])) {
  786. $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : ''));
  787. }
  788. // If the database prefix is being used by SimpleTest to run the tests in a copied
  789. // database then set the user-agent header to the database prefix so that any
  790. // calls to other Drupal pages will run the SimpleTest prefixed database. The
  791. // user-agent is used to ensure that multiple testing sessions running at the
  792. // same time won't interfere with each other as they would if the database
  793. // prefix were stored statically in a file or database variable.
  794. $test_info = &$GLOBALS['drupal_test_info'];
  795. if (!empty($test_info['test_run_id'])) {
  796. $options['headers']['User-Agent'] = drupal_generate_test_ua($test_info['test_run_id']);
  797. }
  798. $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n";
  799. foreach ($options['headers'] as $name => $value) {
  800. $request .= $name . ': ' . trim($value) . "\r\n";
  801. }
  802. $request .= "\r\n" . $options['data'];
  803. $result->request = $request;
  804. // Calculate how much time is left of the original timeout value.
  805. $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000;
  806. if ($timeout > 0) {
  807. stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
  808. fwrite($fp, $request);
  809. }
  810. // Fetch response. Due to PHP bugs like http://bugs.php.net/bug.php?id=43782
  811. // and http://bugs.php.net/bug.php?id=46049 we can't rely on feof(), but
  812. // instead must invoke stream_get_meta_data() each iteration.
  813. $info = stream_get_meta_data($fp);
  814. $alive = !$info['eof'] && !$info['timed_out'];
  815. $response = '';
  816. while ($alive) {
  817. // Calculate how much time is left of the original timeout value.
  818. $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000;
  819. if ($timeout <= 0) {
  820. $info['timed_out'] = TRUE;
  821. break;
  822. }
  823. stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
  824. $chunk = fread($fp, 1024);
  825. $response .= $chunk;
  826. $info = stream_get_meta_data($fp);
  827. $alive = !$info['eof'] && !$info['timed_out'] && $chunk;
  828. }
  829. fclose($fp);
  830. if ($info['timed_out']) {
  831. $result->code = HTTP_REQUEST_TIMEOUT;
  832. $result->error = 'request timed out';
  833. return $result;
  834. }
  835. // Parse response headers from the response body.
  836. // Be tolerant of malformed HTTP responses that separate header and body with
  837. // \n\n or \r\r instead of \r\n\r\n.
  838. list($response, $result->data) = preg_split("/\r\n\r\n|\n\n|\r\r/", $response, 2);
  839. $response = preg_split("/\r\n|\n|\r/", $response);
  840. // Parse the response status line.
  841. list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3);
  842. $result->protocol = $protocol;
  843. $result->status_message = $status_message;
  844. $result->headers = array();
  845. // Parse the response headers.
  846. while ($line = trim(array_shift($response))) {
  847. list($name, $value) = explode(':', $line, 2);
  848. $name = strtolower($name);
  849. if (isset($result->headers[$name]) && $name == 'set-cookie') {
  850. // RFC 2109: the Set-Cookie response header comprises the token Set-
  851. // Cookie:, followed by a comma-separated list of one or more cookies.
  852. $result->headers[$name] .= ',' . trim($value);
  853. }
  854. else {
  855. $result->headers[$name] = trim($value);
  856. }
  857. }
  858. $responses = array(
  859. 100 => 'Continue',
  860. 101 => 'Switching Protocols',
  861. 200 => 'OK',
  862. 201 => 'Created',
  863. 202 => 'Accepted',
  864. 203 => 'Non-Authoritative Information',
  865. 204 => 'No Content',
  866. 205 => 'Reset Content',
  867. 206 => 'Partial Content',
  868. 300 => 'Multiple Choices',
  869. 301 => 'Moved Permanently',
  870. 302 => 'Found',
  871. 303 => 'See Other',
  872. 304 => 'Not Modified',
  873. 305 => 'Use Proxy',
  874. 307 => 'Temporary Redirect',
  875. 400 => 'Bad Request',
  876. 401 => 'Unauthorized',
  877. 402 => 'Payment Required',
  878. 403 => 'Forbidden',
  879. 404 => 'Not Found',
  880. 405 => 'Method Not Allowed',
  881. 406 => 'Not Acceptable',
  882. 407 => 'Proxy Authentication Required',
  883. 408 => 'Request Time-out',
  884. 409 => 'Conflict',
  885. 410 => 'Gone',
  886. 411 => 'Length Required',
  887. 412 => 'Precondition Failed',
  888. 413 => 'Request Entity Too Large',
  889. 414 => 'Request-URI Too Large',
  890. 415 => 'Unsupported Media Type',
  891. 416 => 'Requested range not satisfiable',
  892. 417 => 'Expectation Failed',
  893. 500 => 'Internal Server Error',
  894. 501 => 'Not Implemented',
  895. 502 => 'Bad Gateway',
  896. 503 => 'Service Unavailable',
  897. 504 => 'Gateway Time-out',
  898. 505 => 'HTTP Version not supported',
  899. );
  900. // RFC 2616 states that all unknown HTTP codes must be treated the same as the
  901. // base code in their class.
  902. if (!isset($responses[$code])) {
  903. $code = floor($code / 100) * 100;
  904. }
  905. $result->code = $code;
  906. switch ($code) {
  907. case 200: // OK
  908. case 304: // Not modified
  909. break;
  910. case 301: // Moved permanently
  911. case 302: // Moved temporarily
  912. case 307: // Moved temporarily
  913. $location = $result->headers['location'];
  914. $options['timeout'] -= timer_read(__FUNCTION__) / 1000;
  915. if ($options['timeout'] <= 0) {
  916. $result->code = HTTP_REQUEST_TIMEOUT;
  917. $result->error = 'request timed out';
  918. }
  919. elseif ($options['max_redirects']) {
  920. // Redirect to the new location.
  921. $options['max_redirects']--;
  922. $result = drupal_http_request($location, $options);
  923. $result->redirect_code = $code;
  924. }
  925. if (!isset($result->redirect_url)) {
  926. $result->redirect_url = $location;
  927. }
  928. break;
  929. default:
  930. $result->error = $status_message;
  931. }
  932. return $result;
  933. }
  934. /**
  935. * @} End of "HTTP handling".
  936. */
  937. /**
  938. * Strips slashes from a string or array of strings.
  939. *
  940. * Callback for array_walk() within fix_gpx_magic().
  941. *
  942. * @param $item
  943. * An individual string or array of strings from superglobals.
  944. */
  945. function _fix_gpc_magic(&$item) {
  946. if (is_array($item)) {
  947. array_walk($item, '_fix_gpc_magic');
  948. }
  949. else {
  950. $item = stripslashes($item);
  951. }
  952. }
  953. /**
  954. * Strips slashes from $_FILES items.
  955. *
  956. * Callback for array_walk() within fix_gpc_magic().
  957. *
  958. * The tmp_name key is skipped keys since PHP generates single backslashes for
  959. * file paths on Windows systems.
  960. *
  961. * @param $item
  962. * An item from $_FILES.
  963. * @param $key
  964. * The key for the item within $_FILES.
  965. *
  966. * @see http://php.net/manual/en/features.file-upload.php#42280
  967. */
  968. function _fix_gpc_magic_files(&$item, $key) {
  969. if ($key != 'tmp_name') {
  970. if (is_array($item)) {
  971. array_walk($item, '_fix_gpc_magic_files');
  972. }
  973. else {
  974. $item = stripslashes($item);
  975. }
  976. }
  977. }
  978. /**
  979. * Fixes double-escaping caused by "magic quotes" in some PHP installations.
  980. *
  981. * @see _fix_gpc_magic()
  982. * @see _fix_gpc_magic_files()
  983. */
  984. function fix_gpc_magic() {
  985. static $fixed = FALSE;
  986. if (!$fixed && ini_get('magic_quotes_gpc')) {
  987. array_walk($_GET, '_fix_gpc_magic');
  988. array_walk($_POST, '_fix_gpc_magic');
  989. array_walk($_COOKIE, '_fix_gpc_magic');
  990. array_walk($_REQUEST, '_fix_gpc_magic');
  991. array_walk($_FILES, '_fix_gpc_magic_files');
  992. }
  993. $fixed = TRUE;
  994. }
  995. /**
  996. * @defgroup validation Input validation
  997. * @{
  998. * Functions to validate user input.
  999. */
  1000. /**
  1001. * Verifies the syntax of the given e-mail address.
  1002. *
  1003. * Empty e-mail addresses are allowed. See RFC 2822 for details.
  1004. *
  1005. * @param $mail
  1006. * A string containing an e-mail address.
  1007. *
  1008. * @return
  1009. * TRUE if the address is in a valid format.
  1010. */
  1011. function valid_email_address($mail) {
  1012. return (bool)filter_var($mail, FILTER_VALIDATE_EMAIL);
  1013. }
  1014. /**
  1015. * Verifies the syntax of the given URL.
  1016. *
  1017. * This function should only be used on actual URLs. It should not be used for
  1018. * Drupal menu paths, which can contain arbitrary characters.
  1019. * Valid values per RFC 3986.
  1020. * @param $url
  1021. * The URL to verify.
  1022. * @param $absolute
  1023. * Whether the URL is absolute (beginning with a scheme such as "http:").
  1024. *
  1025. * @return
  1026. * TRUE if the URL is in a valid format.
  1027. */
  1028. function valid_url($url, $absolute = FALSE) {
  1029. if ($absolute) {
  1030. return (bool)preg_match("
  1031. /^ # Start at the beginning of the text
  1032. (?:ftp|https?|feed):\/\/ # Look for ftp, http, https or feed schemes
  1033. (?: # Userinfo (optional) which is typically
  1034. (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)* # a username or a username and password
  1035. (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@ # combination
  1036. )?
  1037. (?:
  1038. (?:[a-z0-9\-\.]|%[0-9a-f]{2})+ # A domain name or a IPv4 address
  1039. |(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\]) # or a well formed IPv6 address
  1040. )
  1041. (?::[0-9]+)? # Server port number (optional)
  1042. (?:[\/|\?]
  1043. (?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2}) # The path and query (optional)
  1044. *)?
  1045. $/xi", $url);
  1046. }
  1047. else {
  1048. return (bool)preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url);
  1049. }
  1050. }
  1051. /**
  1052. * @} End of "defgroup validation".
  1053. */
  1054. /**
  1055. * Registers an event for the current visitor to the flood control mechanism.
  1056. *
  1057. * @param $name
  1058. * The name of an event.
  1059. * @param $window
  1060. * Optional number of seconds before this event expires. Defaults to 3600 (1
  1061. * hour). Typically uses the same value as the flood_is_allowed() $window
  1062. * parameter. Expired events are purged on cron run to prevent the flood table
  1063. * from growing indefinitely.
  1064. * @param $identifier
  1065. * Optional identifier (defaults to the current user's IP address).
  1066. */
  1067. function flood_register_event($name, $window = 3600, $identifier = NULL) {
  1068. if (!isset($identifier)) {
  1069. $identifier = ip_address();
  1070. }
  1071. db_insert('flood')
  1072. ->fields(array(
  1073. 'event' => $name,
  1074. 'identifier' => $identifier,
  1075. 'timestamp' => REQUEST_TIME,
  1076. 'expiration' => REQUEST_TIME + $window,
  1077. ))
  1078. ->execute();
  1079. }
  1080. /**
  1081. * Makes the flood control mechanism forget an event for the current visitor.
  1082. *
  1083. * @param $name
  1084. * The name of an event.
  1085. * @param $identifier
  1086. * Optional identifier (defaults to the current user's IP address).
  1087. */
  1088. function flood_clear_event($name, $identifier = NULL) {
  1089. if (!isset($identifier)) {
  1090. $identifier = ip_address();
  1091. }
  1092. db_delete('flood')
  1093. ->condition('event', $name)
  1094. ->condition('identifier', $identifier)
  1095. ->execute();
  1096. }
  1097. /**
  1098. * Checks whether a user is allowed to proceed with the specified event.
  1099. *
  1100. * Events can have thresholds saying that each user can only do that event
  1101. * a certain number of times in a time window. This function verifies that the
  1102. * current user has not exceeded this threshold.
  1103. *
  1104. * @param $name
  1105. * The unique name of the event.
  1106. * @param $threshold
  1107. * The maximum number of times each user can do this event per time window.
  1108. * @param $window
  1109. * Number of seconds in the time window for this event (default is 3600
  1110. * seconds, or 1 hour).
  1111. * @param $identifier
  1112. * Unique identifier of the current user. Defaults to their IP address.
  1113. *
  1114. * @return
  1115. * TRUE if the user is allowed to proceed. FALSE if they have exceeded the
  1116. * threshold and should not be allowed to proceed.
  1117. */
  1118. function flood_is_allowed($name, $threshold, $window = 3600, $identifier = NULL) {
  1119. if (!isset($identifier)) {
  1120. $identifier = ip_address();
  1121. }
  1122. $number = db_query("SELECT COUNT(*) FROM {flood} WHERE event = :event AND identifier = :identifier AND timestamp > :timestamp", array(
  1123. ':event' => $name,
  1124. ':identifier' => $identifier,
  1125. ':timestamp' => REQUEST_TIME - $window))
  1126. ->fetchField();
  1127. return ($number < $threshold);
  1128. }
  1129. /**
  1130. * @defgroup sanitization Sanitization functions
  1131. * @{
  1132. * Functions to sanitize values.
  1133. *
  1134. * See http://drupal.org/writing-secure-code for information
  1135. * on writing secure code.
  1136. */
  1137. /**
  1138. * Strips dangerous protocols (e.g. 'javascript:') from a URI.
  1139. *
  1140. * This function must be called for all URIs within user-entered input prior
  1141. * to being output to an HTML attribute value. It is often called as part of
  1142. * check_url() or filter_xss(), but those functions return an HTML-encoded
  1143. * string, so this function can be called independently when the output needs to
  1144. * be a plain-text string for passing to t(), l(), drupal_attributes(), or
  1145. * another function that will call check_plain() separately.
  1146. *
  1147. * @param $uri
  1148. * A plain-text URI that might contain dangerous protocols.
  1149. *
  1150. * @return
  1151. * A plain-text URI stripped of dangerous protocols. As with all plain-text
  1152. * strings, this return value must not be output to an HTML page without
  1153. * check_plain() being called on it. However, it can be passed to functions
  1154. * expecting plain-text strings.
  1155. *
  1156. * @see check_url()
  1157. */
  1158. function drupal_strip_dangerous_protocols($uri) {
  1159. static $allowed_protocols;
  1160. if (!isset($allowed_protocols)) {
  1161. $allowed_protocols = array_flip(variable_get('filter_allowed_protocols', array('ftp', 'http', 'https', 'irc', 'mailto', 'news', 'nntp', 'rtsp', 'sftp', 'ssh', 'tel', 'telnet', 'webcal')));
  1162. }
  1163. // Iteratively remove any invalid protocol found.
  1164. do {
  1165. $before = $uri;
  1166. $colonpos = strpos($uri, ':');
  1167. if ($colonpos > 0) {
  1168. // We found a colon, possibly a protocol. Verify.
  1169. $protocol = substr($uri, 0, $colonpos);
  1170. // If a colon is preceded by a slash, question mark or hash, it cannot
  1171. // possibly be part of the URL scheme. This must be a relative URL, which
  1172. // inherits the (safe) protocol of the base document.
  1173. if (preg_match('![/?#]!', $protocol)) {
  1174. break;
  1175. }
  1176. // Check if this is a disallowed protocol. Per RFC2616, section 3.2.3
  1177. // (URI Comparison) scheme comparison must be case-insensitive.
  1178. if (!isset($allowed_protocols[strtolower($protocol)])) {
  1179. $uri = substr($uri, $colonpos + 1);
  1180. }
  1181. }
  1182. } while ($before != $uri);
  1183. return $uri;
  1184. }
  1185. /**
  1186. * Strips dangerous protocols from a URI and encodes it for output to HTML.
  1187. *
  1188. * @param $uri
  1189. * A plain-text URI that might contain dangerous protocols.
  1190. *
  1191. * @return
  1192. * A URI stripped of dangerous protocols and encoded for output to an HTML
  1193. * attribute value. Because it is already encoded, it should not be set as a
  1194. * value within a $attributes array passed to drupal_attributes(), because
  1195. * drupal_attributes() expects those values to be plain-text strings. To pass
  1196. * a filtered URI to drupal_attributes(), call
  1197. * drupal_strip_dangerous_protocols() instead.
  1198. *
  1199. * @see drupal_strip_dangerous_protocols()
  1200. */
  1201. function check_url($uri) {
  1202. return check_plain(drupal_strip_dangerous_protocols($uri));
  1203. }
  1204. /**
  1205. * Applies a very permissive XSS/HTML filter for admin-only use.
  1206. *
  1207. * Use only for fields where it is impractical to use the
  1208. * whole filter system, but where some (mainly inline) mark-up
  1209. * is desired (so check_plain() is not acceptable).
  1210. *
  1211. * Allows all tags that can be used inside an HTML body, save
  1212. * for scripts and styles.
  1213. */
  1214. function filter_xss_admin($string) {
  1215. return filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'command', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'menu', 'meter', 'nav', 'ol', 'output', 'p', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr'));
  1216. }
  1217. /**
  1218. * Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities.
  1219. *
  1220. * Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses.
  1221. * For examples of various XSS attacks, see: http://ha.ckers.org/xss.html.
  1222. *
  1223. * This code does four things:
  1224. * - Removes characters and constructs that can trick browsers.
  1225. * - Makes sure all HTML entities are well-formed.
  1226. * - Makes sure all HTML tags and attributes are well-formed.
  1227. * - Makes sure no HTML tags contain URLs with a disallowed protocol (e.g.
  1228. * javascript:).
  1229. *
  1230. * @param $string
  1231. * The string with raw HTML in it. It will be stripped of everything that can
  1232. * cause an XSS attack.
  1233. * @param $allowed_tags
  1234. * An array of allowed tags.
  1235. *
  1236. * @return
  1237. * An XSS safe version of $string, or an empty string if $string is not
  1238. * valid UTF-8.
  1239. *
  1240. * @see drupal_validate_utf8()
  1241. * @ingroup sanitization
  1242. */
  1243. function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) {
  1244. // Only operate on valid UTF-8 strings. This is necessary to prevent cross
  1245. // site scripting issues on Internet Explorer 6.
  1246. if (!drupal_validate_utf8($string)) {
  1247. return '';
  1248. }
  1249. // Store the text format.
  1250. _filter_xss_split($allowed_tags, TRUE);
  1251. // Remove NULL characters (ignored by some browsers).
  1252. $string = str_replace(chr(0), '', $string);
  1253. // Remove Netscape 4 JS entities.
  1254. $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string);
  1255. // Defuse all HTML entities.
  1256. $string = str_replace('&', '&amp;', $string);
  1257. // Change back only well-formed entities in our whitelist:
  1258. // Decimal numeric entities.
  1259. $string = preg_replace('/&amp;#([0-9]+;)/', '&#\1', $string);
  1260. // Hexadecimal numeric entities.
  1261. $string = preg_replace('/&amp;#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string);
  1262. // Named entities.
  1263. $string = preg_replace('/&amp;([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string);
  1264. return preg_replace_callback('%
  1265. (
  1266. <(?=[^a-zA-Z!/]) # a lone <
  1267. | # or
  1268. <!--.*?--> # a comment
  1269. | # or
  1270. <[^>]*(>|$) # a string that starts with a <, up until the > or the end of the string
  1271. | # or
  1272. > # just a >
  1273. )%x', '_filter_xss_split', $string);
  1274. }
  1275. /**
  1276. * Processes an HTML tag.
  1277. *
  1278. * @param $m
  1279. * An array with various meaning depending on the value of $store.
  1280. * If $store is TRUE then the array contains the allowed tags.
  1281. * If $store is FALSE then the array has one element, the HTML tag to process.
  1282. * @param $store
  1283. * Whether to store $m.
  1284. *
  1285. * @return
  1286. * If the element isn't allowed, an empty string. Otherwise, the cleaned up
  1287. * version of the HTML element.
  1288. */
  1289. function _filter_xss_split($m, $store = FALSE) {
  1290. static $allowed_html;
  1291. if ($store) {
  1292. $allowed_html = array_flip($m);
  1293. return;
  1294. }
  1295. $string = $m[1];
  1296. if (substr($string, 0, 1) != '<') {
  1297. // We matched a lone ">" character.
  1298. return '&gt;';
  1299. }
  1300. elseif (strlen($string) == 1) {
  1301. // We matched a lone "<" character.
  1302. return '&lt;';
  1303. }
  1304. if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?|(<!--.*?-->)$%', $string, $matches)) {
  1305. // Seriously malformed.
  1306. return '';
  1307. }
  1308. $slash = trim($matches[1]);
  1309. $elem = &$matches[2];
  1310. $attrlist = &$matches[3];
  1311. $comment = &$matches[4];
  1312. if ($comment) {
  1313. $elem = '!--';
  1314. }
  1315. if (!isset($allowed_html[strtolower($elem)])) {
  1316. // Disallowed HTML element.
  1317. return '';
  1318. }
  1319. if ($comment) {
  1320. return $comment;
  1321. }
  1322. if ($slash != '') {
  1323. return "</$elem>";
  1324. }
  1325. // Is there a closing XHTML slash at the end of the attributes?
  1326. $attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist, -1, $count);
  1327. $xhtml_slash = $count ? ' /' : '';
  1328. // Clean up attributes.
  1329. $attr2 = implode(' ', _filter_xss_attributes($attrlist));
  1330. $attr2 = preg_replace('/[<>]/', '', $attr2);
  1331. $attr2 = strlen($attr2) ? ' ' . $attr2 : '';
  1332. return "<$elem$attr2$xhtml_slash>";
  1333. }
  1334. /**
  1335. * Processes a string of HTML attributes.
  1336. *
  1337. * @return
  1338. * Cleaned up version of the HTML attributes.
  1339. */
  1340. function _filter_xss_attributes($attr) {
  1341. $attrarr = array();
  1342. $mode = 0;
  1343. $attrname = '';
  1344. while (strlen($attr) != 0) {
  1345. // Was the last operation successful?
  1346. $working = 0;
  1347. switch ($mode) {
  1348. case 0:
  1349. // Attribute name, href for instance.
  1350. if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) {
  1351. $attrname = strtolower($match[1]);
  1352. $skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on');
  1353. $working = $mode = 1;
  1354. $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr);
  1355. }
  1356. break;
  1357. case 1:
  1358. // Equals sign or valueless ("selected").
  1359. if (preg_match('/^\s*=\s*/', $attr)) {
  1360. $working = 1; $mode = 2;
  1361. $attr = preg_replace('/^\s*=\s*/', '', $attr);
  1362. break;
  1363. }
  1364. if (preg_match('/^\s+/', $attr)) {
  1365. $working = 1; $mode = 0;
  1366. if (!$skip) {
  1367. $attrarr[] = $attrname;
  1368. }
  1369. $attr = preg_replace('/^\s+/', '', $attr);
  1370. }
  1371. break;
  1372. case 2:
  1373. // Attribute value, a URL after href= for instance.
  1374. if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) {
  1375. $thisval = filter_xss_bad_protocol($match[1]);
  1376. if (!$skip) {
  1377. $attrarr[] = "$attrname=\"$thisval\"";
  1378. }
  1379. $working = 1;
  1380. $mode = 0;
  1381. $attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr);
  1382. break;
  1383. }
  1384. if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match)) {
  1385. $thisval = filter_xss_bad_protocol($match[1]);
  1386. if (!$skip) {
  1387. $attrarr[] = "$attrname='$thisval'";
  1388. }
  1389. $working = 1; $mode = 0;
  1390. $attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr);
  1391. break;
  1392. }
  1393. if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match)) {
  1394. $thisval = filter_xss_bad_protocol($match[1]);
  1395. if (!$skip) {
  1396. $attrarr[] = "$attrname=\"$thisval\"";
  1397. }
  1398. $working = 1; $mode = 0;
  1399. $attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr);
  1400. }
  1401. break;
  1402. }
  1403. if ($working == 0) {
  1404. // Not well formed; remove and try again.
  1405. $attr = preg_replace('/
  1406. ^
  1407. (
  1408. "[^"]*("|$) # - a string that starts with a double quote, up until the next double quote or the end of the string
  1409. | # or
  1410. \'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string
  1411. |

Large files files are truncated, but you can click here to view the full file