common.inc 263 KB
Newer Older
Dries's avatar
   
Dries committed
1
2
<?php

3
use Drupal\Component\Utility\NestedArray;
4
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
5
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
6
use Drupal\Core\Cache\CacheBackendInterface;
7
use Drupal\Core\Database\Database;
8
use Drupal\Core\Template\Attribute;
9

Dries's avatar
   
Dries committed
10
11
12
13
14
15
16
17
/**
 * @file
 * Common functions that many Drupal modules will need to reference.
 *
 * The functions that are critical and need to be available even when serving
 * a cached page are instead located in bootstrap.inc.
 */

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
 * @defgroup php_wrappers PHP wrapper functions
 * @{
 * Functions that are wrappers or custom implementations of PHP functions.
 *
 * Certain PHP functions should not be used in Drupal. Instead, Drupal's
 * replacement functions should be used.
 *
 * For example, for improved or more secure UTF8-handling, or RFC-compliant
 * handling of URLs in Drupal.
 *
 * For ease of use and memorizing, all these wrapper functions use the same name
 * as the original PHP function, but prefixed with "drupal_". Beware, however,
 * that not all wrapper functions support the same arguments as the original
 * functions.
 *
 * You should always use these wrapper functions in your code.
 *
 * Wrong:
 * @code
 *   $my_substring = substr($original_string, 0, 5);
 * @endcode
 *
 * Correct:
 * @code
 *   $my_substring = drupal_substr($original_string, 0, 5);
 * @endcode
 *
46
 * @}
47
48
 */

49
50
51
/**
 * Return status for saving which involved creating a new item.
 */
52
const SAVED_NEW = 1;
53
54
55
56

/**
 * Return status for saving which involved an update to an existing item.
 */
57
const SAVED_UPDATED = 2;
58
59
60
61

/**
 * Return status for saving which deleted an existing item.
 */
62
const SAVED_DELETED = 3;
63

64
/**
65
 * The default group for system CSS files added to the page.
66
 */
67
const CSS_SYSTEM = -100;
68
69

/**
70
 * The default group for module CSS files added to the page.
71
 */
72
const CSS_DEFAULT = 0;
73
74

/**
75
 * The default group for theme CSS files added to the page.
76
 */
77
const CSS_THEME = 100;
78

79
/**
80
 * The default group for JavaScript and jQuery libraries added to the page.
81
 */
82
const JS_LIBRARY = -100;
83
84

/**
85
 * The default group for module JavaScript code added to the page.
86
 */
87
const JS_DEFAULT = 0;
88
89

/**
90
 * The default group for theme JavaScript code added to the page.
91
 */
92
const JS_THEME = 100;
93

94
95
96
/**
 * The default group for JavaScript settings added to the page.
 */
97
const JS_SETTING = 200;
98

99
/**
100
101
102
 * Error code indicating that the request exceeded the specified timeout.
 *
 * @see drupal_http_request()
103
 */
104
const HTTP_REQUEST_TIMEOUT = -1;
105

106
/**
107
108
109
 * @defgroup block_caching Block Caching
 * @{
 * Constants that define each block's caching state.
110
111
 *
 * Modules specify the caching patterns for their blocks using binary
112
 * combinations of these constants in their hook_block_info().
113
114
115
116
 * DRUPAL_CACHE_PER_ROLE is used as a default when no caching pattern is
 * specified. Use DRUPAL_CACHE_CUSTOM to disable standard block cache and
 * implement
 *
117
118
119
120
121
 * The block cache is cleared when the 'content' cache tag is invalidated,
 * following the same pattern as the page cache (node, comment, user, taxonomy
 * added or updated...). Blocks requiring more fine-grained clearing might
 * consider disabling the built-in block cache (DRUPAL_NO_CACHE)
 * and roll their own.
122
123
124
125
126
 *
 * Note that user 1 is excluded from block caching.
 */

/**
127
128
129
130
131
132
133
 * The block should not get cached.
 *
 * This setting should be used:
 * - For simple blocks (notably those that do not perform any db query), where
 *   querying the db cache would be more expensive than directly generating the
 *   content.
 * - For blocks that change too frequently.
134
 */
135
const DRUPAL_NO_CACHE = -1;
136
137

/**
138
139
140
141
142
 * The block is handling its own caching in its hook_block_view().
 *
 * From the perspective of the block cache system, this is equivalent to
 * DRUPAL_NO_CACHE. Useful when time based expiration is needed or a site uses
 * a node access which invalidates standard block cache.
143
 */
144
const DRUPAL_CACHE_CUSTOM = -2;
145
146

/**
147
148
149
150
 * The block or element can change depending on the user's roles.
 *
 * This is the default setting for blocks, used when the block does not specify
 * anything.
151
 */
152
const DRUPAL_CACHE_PER_ROLE = 0x0001;
153
154

/**
155
156
 * The block or element can change depending on the user.
 *
157
158
159
 * This setting can be resource-consuming for sites with large number of users,
 * and thus should only be used when DRUPAL_CACHE_PER_ROLE is not sufficient.
 */
160
const DRUPAL_CACHE_PER_USER = 0x0002;
161
162
163
164

/**
 * The block or element can change depending on the page being viewed.
 */
165
const DRUPAL_CACHE_PER_PAGE = 0x0004;
166
167

/**
168
 * The block or element is the same for every user and page that it is visible.
169
 */
170
const DRUPAL_CACHE_GLOBAL = 0x0008;
171

172
173
174
175
/**
 * @} End of "defgroup block_caching".
 */

176
177
178
179
180
181
182
183
184
/**
 * The delimiter used to split plural strings.
 *
 * This is the ETX (End of text) character and is used as a minimal means to
 * separate singular and plural variants in source and translation text. It
 * was found to be the most compatible delimiter for the supported databases.
 */
const LOCALE_PLURAL_DELIMITER = "\03";

185
/**
186
 * Adds content to a specified region.
187
188
 *
 * @param $region
189
 *   Page region the content is added to.
190
 * @param $data
191
 *   Content to be added.
192
 */
193
function drupal_add_region_content($region = NULL, $data = NULL) {
194
195
  static $content = array();

196
  if (isset($region) && isset($data)) {
197
198
199
200
201
202
    $content[$region][] = $data;
  }
  return $content;
}

/**
203
 * Gets assigned content for a given region.
204
205
 *
 * @param $region
206
207
 *   A specified region to fetch content for. If NULL, all regions will be
 *   returned.
208
 * @param $delimiter
209
 *   Content to be inserted between imploded array elements.
210
 */
211
212
function drupal_get_region_content($region = NULL, $delimiter = ' ') {
  $content = drupal_add_region_content();
213
214
  if (isset($region)) {
    if (isset($content[$region]) && is_array($content[$region])) {
Steven Wittens's avatar
Steven Wittens committed
215
      return implode($delimiter, $content[$region]);
216
    }
217
218
219
220
  }
  else {
    foreach (array_keys($content) as $region) {
      if (is_array($content[$region])) {
Steven Wittens's avatar
Steven Wittens committed
221
        $content[$region] = implode($delimiter, $content[$region]);
222
223
224
225
226
227
      }
    }
    return $content;
  }
}

228
/**
229
 * Gets the name of the currently active install profile.
230
231
232
233
 *
 * When this function is called during Drupal's initial installation process,
 * the name of the profile that's about to be installed is stored in the global
 * installation state. At all other times, the standard Drupal systems variable
234
235
 * table contains the name of the current profile, and we can call
 * variable_get() to determine what one is active.
236
237
238
239
240
241
242
243
244
245
246
 *
 * @return $profile
 *   The name of the install profile.
 */
function drupal_get_profile() {
  global $install_state;

  if (isset($install_state['parameters']['profile'])) {
    $profile = $install_state['parameters']['profile'];
  }
  else {
247
    $profile = variable_get('install_profile', 'standard');
248
249
250
251
252
253
  }

  return $profile;
}


Dries's avatar
   
Dries committed
254
/**
255
 * Sets the breadcrumb trail for the current page.
Dries's avatar
   
Dries committed
256
 *
Dries's avatar
   
Dries committed
257
258
259
 * @param $breadcrumb
 *   Array of links, starting with "home" and proceeding up to but not including
 *   the current page.
Kjartan's avatar
Kjartan committed
260
 */
Dries's avatar
   
Dries committed
261
function drupal_set_breadcrumb($breadcrumb = NULL) {
262
  $stored_breadcrumb = &drupal_static(__FUNCTION__);
Dries's avatar
   
Dries committed
263

264
  if (isset($breadcrumb)) {
Dries's avatar
   
Dries committed
265
266
267
268
269
    $stored_breadcrumb = $breadcrumb;
  }
  return $stored_breadcrumb;
}

Dries's avatar
   
Dries committed
270
/**
271
 * Gets the breadcrumb trail for the current page.
Dries's avatar
   
Dries committed
272
 */
Dries's avatar
   
Dries committed
273
274
275
function drupal_get_breadcrumb() {
  $breadcrumb = drupal_set_breadcrumb();

276
  if (!isset($breadcrumb)) {
Dries's avatar
   
Dries committed
277
278
279
280
281
282
    $breadcrumb = menu_get_active_breadcrumb();
  }

  return $breadcrumb;
}

Dries's avatar
Dries committed
283
/**
284
 * Adds output to the HEAD tag of the HTML page.
285
 *
286
 * This function can be called as long as the headers aren't sent. Pass no
287
288
289
290
291
292
293
294
295
296
297
298
299
 * arguments (or NULL for both) to retrieve the currently stored elements.
 *
 * @param $data
 *   A renderable array. If the '#type' key is not set then 'html_tag' will be
 *   added as the default '#type'.
 * @param $key
 *   A unique string key to allow implementations of hook_html_head_alter() to
 *   identify the element in $data. Required if $data is not NULL.
 *
 * @return
 *   An array of all stored HEAD elements.
 *
 * @see theme_html_tag()
Dries's avatar
Dries committed
300
 */
301
302
function drupal_add_html_head($data = NULL, $key = NULL) {
  $stored_head = &drupal_static(__FUNCTION__);
Dries's avatar
Dries committed
303

304
305
306
307
308
309
310
311
312
313
  if (!isset($stored_head)) {
    // Make sure the defaults, including Content-Type, come first.
    $stored_head = _drupal_default_html_head();
  }

  if (isset($data) && isset($key)) {
    if (!isset($data['#type'])) {
      $data['#type'] = 'html_tag';
    }
    $stored_head[$key] = $data;
Dries's avatar
Dries committed
314
315
316
317
  }
  return $stored_head;
}

Dries's avatar
   
Dries committed
318
/**
319
320
321
322
323
324
325
326
327
328
 * Returns elements that are always displayed in the HEAD tag of the HTML page.
 */
function _drupal_default_html_head() {
  // Add default elements. Make sure the Content-Type comes first because the
  // IE browser may be vulnerable to XSS via encoding attacks from any content
  // that comes before this META tag, such as a TITLE tag.
  $elements['system_meta_content_type'] = array(
    '#type' => 'html_tag',
    '#tag' => 'meta',
    '#attributes' => array(
329
      'charset' => 'utf-8',
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
    ),
    // Security: This always has to be output first.
    '#weight' => -1000,
  );
  // Show Drupal and the major version number in the META GENERATOR tag.
  // Get the major version.
  list($version, ) = explode('.', VERSION);
  $elements['system_meta_generator'] = array(
    '#type' => 'html_tag',
    '#tag' => 'meta',
    '#attributes' => array(
      'name' => 'Generator',
      'content' => 'Drupal ' . $version . ' (http://drupal.org)',
    ),
  );
  // Also send the generator in the HTTP header.
  $elements['system_meta_generator']['#attached']['drupal_add_http_header'][] = array('X-Generator', $elements['system_meta_generator']['#attributes']['content']);
  return $elements;
}

/**
351
 * Retrieves output to be displayed in the HEAD tag of the HTML page.
Dries's avatar
   
Dries committed
352
 */
Dries's avatar
Dries committed
353
function drupal_get_html_head() {
354
355
356
  $elements = drupal_add_html_head();
  drupal_alter('html_head', $elements);
  return drupal_render($elements);
Dries's avatar
Dries committed
357
358
}

359
/**
360
 * Adds a feed URL for the current page.
361
 *
362
363
 * This function can be called as long the HTML header hasn't been sent.
 *
364
 * @param $url
365
 *   An internal system path or a fully qualified external URL of the feed.
366
 * @param $title
367
 *   The title of the feed.
368
 */
369
function drupal_add_feed($url = NULL, $title = '') {
370
  $stored_feed_links = &drupal_static(__FUNCTION__, array());
371

372
  if (isset($url)) {
373
    $stored_feed_links[$url] = theme('feed_icon', array('url' => $url, 'title' => $title));
374

375
376
377
378
379
380
381
382
    drupal_add_html_head_link(array(
      'rel' => 'alternate',
      'type' => 'application/rss+xml',
      'title' => $title,
      // Force the URL to be absolute, for consistency with other <link> tags
      // output by Drupal.
      'href' => url($url, array('absolute' => TRUE)),
    ));
383
384
385
386
387
  }
  return $stored_feed_links;
}

/**
388
 * Gets the feed URLs for the current page.
389
390
 *
 * @param $delimiter
391
 *   A delimiter to split feeds by.
392
393
394
395
396
397
 */
function drupal_get_feeds($delimiter = "\n") {
  $feeds = drupal_add_feed();
  return implode($feeds, $delimiter);
}

Dries's avatar
   
Dries committed
398
/**
399
 * @defgroup http_handling HTTP handling
Dries's avatar
   
Dries committed
400
 * @{
Dries's avatar
   
Dries committed
401
 * Functions to properly handle HTTP responses.
Dries's avatar
   
Dries committed
402
403
 */

404
/**
405
 * Processes a URL query parameter array to remove unwanted elements.
406
407
 *
 * @param $query
408
 *   (optional) An array to be processed. Defaults to $_GET.
409
 * @param $exclude
410
 *   (optional) A list of $query array keys to remove. Use "parent[child]" to
411
 *   exclude nested items.
412
 * @param $parent
413
414
 *   Internal use only. Used to build the $query array key for nested items.
 *
415
 * @return
416
 *   An array containing query parameters, which can be used for url().
417
 */
418
function drupal_get_query_parameters(array $query = NULL, array $exclude = array(), $parent = '') {
419
420
421
422
423
424
425
426
427
428
429
  // Set defaults, if none given.
  if (!isset($query)) {
    $query = $_GET;
  }
  // If $exclude is empty, there is nothing to filter.
  if (empty($exclude)) {
    return $query;
  }
  elseif (!$parent) {
    $exclude = array_flip($exclude);
  }
430

431
  $params = array();
432
  foreach ($query as $key => $value) {
433
434
435
    $string_key = ($parent ? $parent . '[' . $key . ']' : $key);
    if (isset($exclude[$string_key])) {
      continue;
436
437
    }

438
439
440
441
442
    if (is_array($value)) {
      $params[$key] = drupal_get_query_parameters($value, $exclude, $string_key);
    }
    else {
      $params[$key] = $value;
443
    }
444
445
446
447
448
  }

  return $params;
}

449
/**
450
 * Splits a URL-encoded query string into an array.
451
452
453
454
455
 *
 * @param $query
 *   The query string to split.
 *
 * @return
456
 *   An array of URL decoded couples $param_name => $value.
457
458
459
460
461
462
 */
function drupal_get_query_array($query) {
  $result = array();
  if (!empty($query)) {
    foreach (explode('&', $query) as $param) {
      $param = explode('=', $param);
463
      $result[$param[0]] = isset($param[1]) ? rawurldecode($param[1]) : '';
464
465
466
467
468
    }
  }
  return $result;
}

469
/**
470
 * Parses an array into a valid, rawurlencoded query string.
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
 *
 * This differs from http_build_query() as we need to rawurlencode() (instead of
 * urlencode()) all query parameters.
 *
 * @param $query
 *   The query parameter array to be processed, e.g. $_GET.
 * @param $parent
 *   Internal use only. Used to build the $query array key for nested items.
 *
 * @return
 *   A rawurlencoded string which can be used as or appended to the URL query
 *   string.
 *
 * @see drupal_get_query_parameters()
 * @ingroup php_wrappers
 */
function drupal_http_build_query(array $query, $parent = '') {
  $params = array();

  foreach ($query as $key => $value) {
    $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key));
492

493
    // Recurse into children.
494
    if (is_array($value)) {
495
496
497
498
499
      $params[] = drupal_http_build_query($value, $key);
    }
    // If a query parameter value is NULL, only append its key.
    elseif (!isset($value)) {
      $params[] = $key;
500
501
    }
    else {
502
503
      // For better readability of paths in query strings, we decode slashes.
      $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
504
505
506
507
508
509
    }
  }

  return implode('&', $params);
}

510
/**
511
 * Prepares a 'destination' URL query parameter for use with drupal_goto().
512
 *
513
514
515
516
 * Used to direct the user back to the referring page after completing a form.
 * By default the current URL is returned. If a destination exists in the
 * previous request, that destination is returned. As such, a destination can
 * persist across multiple pages.
517
 *
518
519
520
521
522
523
 * @return
 *   An associative array containing the key:
 *   - destination: The path provided via the destination query string or, if
 *     not available, the current path.
 *
 * @see current_path()
524
525
526
 * @see drupal_goto()
 */
function drupal_get_destination() {
527
528
529
530
531
532
  $destination = &drupal_static(__FUNCTION__);

  if (isset($destination)) {
    return $destination;
  }

533
  if (isset($_GET['destination'])) {
534
    $destination = array('destination' => $_GET['destination']);
535
536
  }
  else {
537
    $path = current_path();
538
    $query = drupal_http_build_query(drupal_get_query_parameters());
539
    if ($query != '') {
540
      $path .= '?' . $query;
541
    }
542
543
544
545
546
547
    $destination = array('destination' => $path);
  }
  return $destination;
}

/**
548
 * Parses a system URL string into an associative array suitable for url().
549
550
 *
 * This function should only be used for URLs that have been generated by the
551
552
 * system, such as via url(). It should not be used for URLs that come from
 * external sources, or URLs that link to external resources.
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
 *
 * The returned array contains a 'path' that may be passed separately to url().
 * For example:
 * @code
 *   $options = drupal_parse_url($_GET['destination']);
 *   $my_url = url($options['path'], $options);
 *   $my_link = l('Example link', $options['path'], $options);
 * @endcode
 *
 * This is required, because url() does not support relative URLs containing a
 * query string or fragment in its $path argument. Instead, any query string
 * needs to be parsed into an associative query parameter array in
 * $options['query'] and the fragment into $options['fragment'].
 *
 * @param $url
 *   The URL string to parse, f.e. $_GET['destination'].
 *
 * @return
 *   An associative array containing the keys:
 *   - 'path': The path of the URL. If the given $url is external, this includes
 *     the scheme and host.
 *   - 'query': An array of query parameters of $url, if existent.
 *   - 'fragment': The fragment of $url, if existent.
 *
 * @see url()
 * @see drupal_goto()
 * @ingroup php_wrappers
 */
function drupal_parse_url($url) {
  $options = array(
    'path' => NULL,
    'query' => array(),
    'fragment' => '',
  );

  // External URLs: not using parse_url() here, so we do not have to rebuild
  // the scheme, host, and path without having any use for it.
  if (strpos($url, '://') !== FALSE) {
    // Split off everything before the query string into 'path'.
    $parts = explode('?', $url);
    $options['path'] = $parts[0];
    // If there is a query string, transform it into keyed query parameters.
    if (isset($parts[1])) {
      $query_parts = explode('#', $parts[1]);
      parse_str($query_parts[0], $options['query']);
      // Take over the fragment, if there is any.
      if (isset($query_parts[1])) {
        $options['fragment'] = $query_parts[1];
      }
    }
  }
  // Internal URLs.
  else {
606
607
608
609
610
    // parse_url() does not support relative URLs, so make it absolute. E.g. the
    // relative URL "foo/bar:1" isn't properly parsed.
    $parts = parse_url('http://example.com/' . $url);
    // Strip the leading slash that was just added.
    $options['path'] = substr($parts['path'], 1);
611
612
613
614
615
616
617
618
619
620
621
622
    if (isset($parts['query'])) {
      parse_str($parts['query'], $options['query']);
    }
    if (isset($parts['fragment'])) {
      $options['fragment'] = $parts['fragment'];
    }
  }

  return $options;
}

/**
623
 * Encodes a Drupal path for use in a URL.
624
 *
625
 * For aesthetic reasons slashes are not escaped.
626
 *
627
628
 * Note that url() takes care of calling this function, so a path passed to that
 * function should not be encoded in advance.
629
630
 *
 * @param $path
631
 *   The Drupal path to encode.
632
633
 */
function drupal_encode_path($path) {
634
  return str_replace('%2F', '/', rawurlencode($path));
635
636
}

Kjartan's avatar
Kjartan committed
637
/**
638
 * Sends the user to a different Drupal page.
Kjartan's avatar
Kjartan committed
639
 *
Dries's avatar
   
Dries committed
640
641
 * This issues an on-site HTTP redirect. The function makes sure the redirected
 * URL is formatted correctly.
Kjartan's avatar
Kjartan committed
642
 *
643
644
645
646
647
648
 * If a destination was specified in the current request's URI (i.e.,
 * $_GET['destination']) then it will override the $path and $options values
 * passed to this function. This provides the flexibility to build a link to
 * user/login and override the default redirection so that the user is
 * redirected to a specific path after logging in:
 * @code
649
650
 *   $query = array('destination' => "node/$node->nid");
 *   $link = l(t('Log in'), 'user/login', array('query' => $query));
651
 * @endcode
652
 *
653
654
 * Drupal will ensure that messages set by drupal_set_message() and other
 * session data are written to the database before the user is redirected.
Dries's avatar
   
Dries committed
655
 *
656
657
 * This function ends the request; use it instead of a return in your menu
 * callback.
Dries's avatar
   
Dries committed
658
659
 *
 * @param $path
660
661
 *   (optional) A Drupal path or a full URL, which will be passed to url() to
 *   compute the redirect for the URL.
662
 * @param $options
663
 *   (optional) An associative array of additional URL options to pass to url().
664
 * @param $http_response_code
665
666
 *   (optional) The HTTP status code to use for the redirection, defaults to
 *   302. The valid values for 3xx redirection status codes are defined in
667
668
669
670
671
672
673
674
675
676
 *   @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3 RFC 2616 @endlink
 *   and the
 *   @link http://tools.ietf.org/html/draft-reschke-http-status-308-07 draft for the new HTTP status codes: @endlink
 *   - 301: Moved Permanently (the recommended value for most redirects).
 *   - 302: Found (default in Drupal and PHP, sometimes used for spamming search
 *     engines).
 *   - 303: See Other.
 *   - 304: Not Modified.
 *   - 305: Use Proxy.
 *   - 307: Temporary Redirect.
677
 *
678
 * @see drupal_get_destination()
679
 * @see url()
Kjartan's avatar
Kjartan committed
680
 */
681
682
function drupal_goto($path = '', array $options = array(), $http_response_code = 302) {
  // A destination in $_GET always overrides the function arguments.
683
684
  // We do not allow absolute URLs to be passed via $_GET, as this can be an attack vector.
  if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) {
685
    $destination = drupal_parse_url($_GET['destination']);
686
687
688
    $path = $destination['path'];
    $options['query'] = $destination['query'];
    $options['fragment'] = $destination['fragment'];
689
690
  }

691
692
693
694
  drupal_alter('drupal_goto', $path, $options, $http_response_code);

  // The 'Location' HTTP header must be absolute.
  $options['absolute'] = TRUE;
695

696
  $url = url($path, $options);
Kjartan's avatar
Kjartan committed
697

698
  header('Location: ' . $url, TRUE, $http_response_code);
699
700

  // The "Location" header sends a redirect status code to the HTTP daemon. In
701
702
  // some cases this can be wrong, so we make sure none of the code below the
  // drupal_goto() call gets executed upon redirection.
703
  drupal_exit($url);
Kjartan's avatar
Kjartan committed
704
705
}

Dries's avatar
   
Dries committed
706
/**
707
 * Performs an HTTP request.
Dries's avatar
   
Dries committed
708
 *
709
710
 * This is a flexible and powerful HTTP client implementation. Correctly
 * handles GET, POST, PUT or any other HTTP requests. Handles redirects.
Dries's avatar
   
Dries committed
711
712
713
 *
 * @param $url
 *   A string containing a fully qualified URI.
714
715
716
717
718
719
720
721
722
723
724
725
 * @param array $options
 *   (optional) An array that can have one or more of the following elements:
 *   - headers: An array containing request headers to send as name/value pairs.
 *   - method: A string containing the request method. Defaults to 'GET'.
 *   - data: A string containing the request body, formatted as
 *     'param=value&param=value&...'. Defaults to NULL.
 *   - max_redirects: An integer representing how many times a redirect
 *     may be followed. Defaults to 3.
 *   - timeout: A float representing the maximum number of seconds the function
 *     call may take. The default is 30 seconds. If a timeout occurs, the error
 *     code is set to the HTTP_REQUEST_TIMEOUT constant.
 *   - context: A context resource created with stream_context_create().
726
 *
727
728
729
730
731
732
733
734
735
736
 * @return object
 *   An object that can have one or more of the following components:
 *   - request: A string containing the request body that was sent.
 *   - code: An integer containing the response status code, or the error code
 *     if an error occurred.
 *   - protocol: The response protocol (e.g. HTTP/1.1 or HTTP/1.0).
 *   - status_message: The status message from the response, if a response was
 *     received.
 *   - redirect_code: If redirected, an integer containing the initial response
 *     status code.
737
738
 *   - redirect_url: If redirected, a string containing the URL of the redirect
 *     target.
739
740
741
742
743
 *   - error: If an error occurred, the error message. Otherwise not set.
 *   - headers: An array containing the response headers as name/value pairs.
 *     HTTP header names are case-insensitive (RFC 2616, section 4.2), so for
 *     easy access the array keys are returned in lower case.
 *   - data: A string containing the response body that was received.
Dries's avatar
   
Dries committed
744
 */
745
function drupal_http_request($url, array $options = array()) {
746
  $result = new stdClass();
Dries's avatar
   
Dries committed
747

748
749
750
751
  // Parse the URL and make sure we can handle the schema.
  $uri = @parse_url($url);

  if ($uri == FALSE) {
752
    $result->error = 'unable to parse URL';
753
    $result->code = -1001;
754
755
756
    return $result;
  }

757
758
759
760
761
  if (!isset($uri['scheme'])) {
    $result->error = 'missing schema';
    $result->code = -1002;
    return $result;
  }
762

763
764
765
766
767
768
769
770
  timer_start(__FUNCTION__);

  // Merge the default options.
  $options += array(
    'headers' => array(),
    'method' => 'GET',
    'data' => NULL,
    'max_redirects' => 3,
771
772
    'timeout' => 30.0,
    'context' => NULL,
773
  );
774
775
776
777
778
779

  // Merge the default headers.
  $options['headers'] += array(
    'User-Agent' => 'Drupal (+http://drupal.org/)',
  );

780
781
  // stream_socket_client() requires timeout to be a float.
  $options['timeout'] = (float) $options['timeout'];
782

783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
  // Use a proxy if one is defined and the host is not on the excluded list.
  $proxy_server = variable_get('proxy_server', '');
  if ($proxy_server && _drupal_http_use_proxy($uri['host'])) {
    // Set the scheme so we open a socket to the proxy server.
    $uri['scheme'] = 'proxy';
    // Set the path to be the full URL.
    $uri['path'] = $url;
    // Since the URL is passed as the path, we won't use the parsed query.
    unset($uri['query']);

    // Add in username and password to Proxy-Authorization header if needed.
    if ($proxy_username = variable_get('proxy_username', '')) {
      $proxy_password = variable_get('proxy_password', '');
      $options['headers']['Proxy-Authorization'] = 'Basic ' . base64_encode($proxy_username . (!empty($proxy_password) ? ":" . $proxy_password : ''));
    }
    // Some proxies reject requests with any User-Agent headers, while others
    // require a specific one.
    $proxy_user_agent = variable_get('proxy_user_agent', '');
    // The default value matches neither condition.
    if ($proxy_user_agent === NULL) {
      unset($options['headers']['User-Agent']);
    }
    elseif ($proxy_user_agent) {
      $options['headers']['User-Agent'] = $proxy_user_agent;
    }
  }

Dries's avatar
   
Dries committed
810
  switch ($uri['scheme']) {
811
812
813
814
815
816
817
818
    case 'proxy':
      // Make the socket connection to a proxy server.
      $socket = 'tcp://' . $proxy_server . ':' . variable_get('proxy_port', 8080);
      // The Host header still needs to match the real request.
      $options['headers']['Host'] = $uri['host'];
      $options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : '';
      break;

Dries's avatar
   
Dries committed
819
    case 'http':
820
    case 'feed':
Dries's avatar
Dries committed
821
      $port = isset($uri['port']) ? $uri['port'] : 80;
822
823
824
825
826
      $socket = 'tcp://' . $uri['host'] . ':' . $port;
      // RFC 2616: "non-standard ports MUST, default ports MAY be included".
      // We don't add the standard port to prevent from breaking rewrite rules
      // checking the host that do not take into account the port number.
      $options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : '');
Dries's avatar
   
Dries committed
827
      break;
828

Dries's avatar
   
Dries committed
829
    case 'https':
830
      // Note: Only works when PHP is compiled with OpenSSL support.
Dries's avatar
Dries committed
831
      $port = isset($uri['port']) ? $uri['port'] : 443;
832
833
      $socket = 'ssl://' . $uri['host'] . ':' . $port;
      $options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : '');
Dries's avatar
   
Dries committed
834
      break;
835

Dries's avatar
   
Dries committed
836
    default:
837
      $result->error = 'invalid schema ' . $uri['scheme'];
838
      $result->code = -1003;
Dries's avatar
   
Dries committed
839
840
841
      return $result;
  }

842
843
844
845
846
847
848
849
  if (empty($options['context'])) {
    $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout']);
  }
  else {
    // Create a stream with context. Allows verification of a SSL certificate.
    $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout'], STREAM_CLIENT_CONNECT, $options['context']);
  }

Dries's avatar
   
Dries committed
850
  // Make sure the socket opened properly.
Dries's avatar
   
Dries committed
851
  if (!$fp) {
852
853
    // When a network error occurs, we use a negative number so it does not
    // clash with the HTTP status codes.
854
    $result->code = -$errno;
855
    $result->error = trim($errstr) ? trim($errstr) : t('Error opening socket @socket', array('@socket' => $socket));
Dries's avatar
   
Dries committed
856
857
858
    return $result;
  }

Dries's avatar
   
Dries committed
859
  // Construct the path to act on.
Dries's avatar
Dries committed
860
861
  $path = isset($uri['path']) ? $uri['path'] : '/';
  if (isset($uri['query'])) {
862
    $path .= '?' . $uri['query'];
Dries's avatar
   
Dries committed
863
864
  }

865
866
867
868
  // Only add Content-Length if we actually have any content or if it is a POST
  // or PUT request. Some non-standard servers get confused by Content-Length in
  // at least HEAD/GET requests, and Squid always requires Content-Length in
  // POST/PUT requests.
869
870
871
  $content_length = strlen($options['data']);
  if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') {
    $options['headers']['Content-Length'] = $content_length;
872
873
874
  }

  // If the server URL has a user then attempt to use basic authentication.
875
  if (isset($uri['user'])) {
876
    $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : ''));
877
878
  }

879
880
881
882
883
884
  // If the database prefix is being used by SimpleTest to run the tests in a copied
  // database then set the user-agent header to the database prefix so that any
  // calls to other Drupal pages will run the SimpleTest prefixed database. The
  // user-agent is used to ensure that multiple testing sessions running at the
  // same time won't interfere with each other as they would if the database
  // prefix were stored statically in a file or database variable.
885
886
887
  $test_info = &$GLOBALS['drupal_test_info'];
  if (!empty($test_info['test_run_id'])) {
    $options['headers']['User-Agent'] = drupal_generate_test_ua($test_info['test_run_id']);
888
889
  }

890
  $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n";
891
  foreach ($options['headers'] as $name => $value) {
892
    $request .= $name . ': ' . trim($value) . "\r\n";
Dries's avatar
   
Dries committed
893
  }
894
  $request .= "\r\n" . $options['data'];
Dries's avatar
   
Dries committed
895
  $result->request = $request;
896
897
898
899
900
901
  // Calculate how much time is left of the original timeout value.
  $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000;
  if ($timeout > 0) {
    stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
    fwrite($fp, $request);
  }
Dries's avatar
   
Dries committed
902

903
904
905
906
907
  // Fetch response. Due to PHP bugs like http://bugs.php.net/bug.php?id=43782
  // and http://bugs.php.net/bug.php?id=46049 we can't rely on feof(), but
  // instead must invoke stream_get_meta_data() each iteration.
  $info = stream_get_meta_data($fp);
  $alive = !$info['eof'] && !$info['timed_out'];
908
  $response = '';
909
910

  while ($alive) {
911
912
913
    // Calculate how much time is left of the original timeout value.
    $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000;
    if ($timeout <= 0) {
914
915
      $info['timed_out'] = TRUE;
      break;
916
917
    }
    stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
918
919
920
921
    $chunk = fread($fp, 1024);
    $response .= $chunk;
    $info = stream_get_meta_data($fp);
    $alive = !$info['eof'] && !$info['timed_out'] && $chunk;
Dries's avatar
   
Dries committed
922
923
924
  }
  fclose($fp);

925
926
927
928
929
  if ($info['timed_out']) {
    $result->code = HTTP_REQUEST_TIMEOUT;
    $result->error = 'request timed out';
    return $result;
  }
930
  // Parse response headers from the response body.
931
932
933
  // Be tolerant of malformed HTTP responses that separate header and body with
  // \n\n or \r\r instead of \r\n\r\n.
  list($response, $result->data) = preg_split("/\r\n\r\n|\n\n|\r\r/", $response, 2);
934
  $response = preg_split("/\r\n|\n|\r/", $response);
935

936
  // Parse the response status line.
937
938
939
940
  list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3);
  $result->protocol = $protocol;
  $result->status_message = $status_message;

Dries's avatar
   
Dries committed
941
942
  $result->headers = array();

943
944
  // Parse the response headers.
  while ($line = trim(array_shift($response))) {
945
946
947
    list($name, $value) = explode(':', $line, 2);
    $name = strtolower($name);
    if (isset($result->headers[$name]) && $name == 'set-cookie') {
948
949
      // RFC 2109: the Set-Cookie response header comprises the token Set-
      // Cookie:, followed by a comma-separated list of one or more cookies.
950
      $result->headers[$name] .= ',' . trim($value);
951
952
    }
    else {
953
      $result->headers[$name] = trim($value);
954
    }
Dries's avatar
   
Dries committed
955
956
957
  }

  $responses = array(
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
    100 => 'Continue',
    101 => 'Switching Protocols',
    200 => 'OK',
    201 => 'Created',
    202 => 'Accepted',
    203 => 'Non-Authoritative Information',
    204 => 'No Content',
    205 => 'Reset Content',
    206 => 'Partial Content',
    300 => 'Multiple Choices',
    301 => 'Moved Permanently',
    302 => 'Found',
    303 => 'See Other',
    304 => 'Not Modified',
    305 => 'Use Proxy',
    307 => 'Temporary Redirect',
    400 => 'Bad Request',
    401 => 'Unauthorized',
    402 => 'Payment Required',
    403 => 'Forbidden',
    404 => 'Not Found',
    405 => 'Method Not Allowed',
    406 => 'Not Acceptable',
    407 => 'Proxy Authentication Required',
    408 => 'Request Time-out',
    409 => 'Conflict',
    410 => 'Gone',
    411 => 'Length Required',
    412 => 'Precondition Failed',
    413 => 'Request Entity Too Large',
    414 => 'Request-URI Too Large',
    415 => 'Unsupported Media Type',
    416 => 'Requested range not satisfiable',
    417 => 'Expectation Failed',
    500 => 'Internal Server Error',
    501 => 'Not Implemented',
    502 => 'Bad Gateway',
    503 => 'Service Unavailable',
    504 => 'Gateway Time-out',
    505 => 'HTTP Version not supported',
Dries's avatar
   
Dries committed
998
  );
999
1000
  // RFC 2616 states that all unknown HTTP codes must be treated the same as the
  // base code in their class.
Dries's avatar
   
Dries committed
1001
1002
1003
  if (!isset($responses[$code])) {
    $code = floor($code / 100) * 100;
  }
1004
  $result->code = $code;
Dries's avatar
   
Dries committed
1005
1006
1007
1008
1009
1010
1011
1012

  switch ($code) {
    case 200: // OK
    case 304: // Not modified
      break;
    case 301: // Moved permanently
    case 302: // Moved temporarily
    case 307: // Moved temporarily
1013
      $location = $result->headers['location'];
1014
1015
1016
1017
1018
1019
      $options['timeout'] -= timer_read(__FUNCTION__) / 1000;
      if ($options['timeout'] <= 0) {
        $result->code = HTTP_REQUEST_TIMEOUT;
        $result->error = 'request timed out';
      }
      elseif ($options['max_redirects']) {
1020
1021
1022
        // Redirect to the new location.
        $options['max_redirects']--;
        $result = drupal_http_request($location, $options);
1023
        $result->redirect_code = $code;
Dries's avatar
   
Dries committed
1024
      }
1025
1026
1027
      if (!isset($result->redirect_url)) {
        $result->redirect_url = $location;
      }
Dries's avatar
   
Dries committed
1028
1029
      break;
    default:
1030
      $result->error = $status_message;
Dries's avatar
   
Dries committed
1031
1032
1033
1034
  }

  return $result;
}
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046

/**
 * Helper function for determining hosts excluded from needing a proxy.
 *
 * @return
 *   TRUE if a proxy should be used for this host.
 */
function _drupal_http_use_proxy($host) {
  $proxy_exceptions = variable_get('proxy_exceptions', array('localhost', '127.0.0.1'));
  return !in_array(strtolower($host), $proxy_exceptions, TRUE);
}

Dries's avatar
   
Dries committed
1047
/**
1048
 * @} End of "defgroup http_handling".
Dries's avatar
   
Dries committed
1049
 */
Dries's avatar
   
Dries committed
1050

Kjartan's avatar
Kjartan committed
1051
/**
Dries's avatar
   
Dries committed
1052
 * @defgroup validation Input validation
Dries's avatar
   
Dries committed
1053
 * @{
Dries's avatar
   
Dries committed
1054
 * Functions to validate user input.
Kjartan's avatar
Kjartan committed
1055
1056
 */

1057
/**
1058
 * Verifies the syntax of the given e-mail address.
Dries's avatar
   
Dries committed
1059
 *
1060
 * See @link http://tools.ietf.org/html/rfc5321 RFC 5321 @endlink for details.
1061
 *
Dries's avatar
   
Dries committed
1062
 * @param $mail
1063
 *   A string containing an e-mail address.
1064
 *
Dries's avatar
   
Dries committed
1065
 * @return
Dries's avatar
   
Dries committed
1066
 *   TRUE if the address is in a valid format.
1067
 */
Dries's avatar
   
Dries committed
1068
function valid_email_address($mail) {
1069
  return (bool)filter_var($mail, FILTER_VALIDATE_EMAIL);
1070
1071
}

Dries's avatar
   
Dries committed
1072
/**
1073
 * Verifies the syntax of the given URL.
Dries's avatar
   
Dries committed
1074
 *
1075
1076
 * This function should only be used on actual URLs. It should not be used for
 * Drupal menu paths, which can contain arbitrary characters.
1077
 * Valid values per RFC 3986.
Dries's avatar
   
Dries committed
1078
 * @param $url
Dries's avatar
   
Dries committed
1079
 *   The URL to verify.
Dries's avatar
   
Dries committed
1080
 * @param $absolute
Dries's avatar
   
Dries committed
1081
 *   Whether the URL is absolute (beginning with a scheme such as "http:").
1082
 *
Dries's avatar
   
Dries committed
1083
 * @return
Dries's avatar
   
Dries committed
1084
 *   TRUE if the URL is in a valid format.
Dries's avatar
   
Dries committed
1085
 */
Dries's avatar
   
Dries committed
1086
function valid_url($url, $absolute = FALSE) {
1087
  if ($absolute) {
1088
    return (bool)preg_match("
1089
      /^                                                      # Start at the beginning of the text
1090
      (?:ftp|https?|feed):\/\/                                # Look for ftp, http, https or feed schemes
1091
1092
1093
      (?:                                                     # Userinfo (optional) which is typically
        (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)*      # a username or a username and password
        (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@          # combination
1094
      )?
1095
1096
1097
1098
1099
1100
      (?:
        (?:[a-z0-9\-\.]|%[0-9a-f]{2})+                        # A domain name or a IPv4 address
        |(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\])         # or a well formed IPv6 address
      )
      (?::[0-9]+)?                                            # Server port number (optional)
      (?:[\/|\?]
1101
        (?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})   # The path and query (optional)
1102
      *)?
1103
    $/xi", $url);
1104
1105
  }
  else {
1106
    return (bool)preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url);